'use strict' const crs = require( 'crypto-random-string' ) const appRoot = require( 'app-root-path' ) const knex = require( `${appRoot}/db/knex` ) const Streams = ( ) => knex( 'streams' ) const Acl = ( ) => knex( 'stream_acl' ) const debug = require( 'debug' ) const { createBranch } = require( './branches' ) module.exports = { async createStream( { name, description, isPublic, ownerId } ) { let stream = { id: crs( { length: 10 } ), name: name || generateStreamName(), description: description || '', isPublic: isPublic !== false, updatedAt: knex.fn.now( ) } // Create the stream & set up permissions let [ { id: streamId } ] = await Streams( ).returning( 'id' ).insert( stream ) await Acl( ).insert( { userId: ownerId, resourceId: streamId, role: 'stream:owner' } ) // Create a default main branch await createBranch( { name: 'main', description: 'default branch', streamId: streamId, authorId: ownerId } ) return streamId }, async getStream( { streamId, userId } ) { let stream = await Streams( ).where( { id: streamId } ).select( '*' ).first( ) if ( !userId ) return stream let acl = await Acl().where( { resourceId: streamId, userId: userId } ).select( 'role' ).first() if ( acl ) stream.role = acl.role return stream }, async updateStream( { streamId, name, description, isPublic } ) { let [ { id } ] = await Streams( ) .returning( 'id' ) .where( { id: streamId } ) .update( { name, description, isPublic, updatedAt: knex.fn.now() } ) return id }, async grantPermissionsStream( { streamId, userId, role } ) { // upserts the existing role (sets a new one!) // TODO: check if we're removing the last owner (ie, does the stream still have an owner after this operation)? let query = Acl( ).insert( { userId: userId, resourceId: streamId, role: role } ).toString( ) + ' on conflict on constraint stream_acl_pkey do update set role=excluded.role' await knex.raw( query ) // update stream updated at await Streams().where( { id: streamId } ).update( { updatedAt: knex.fn.now() } ) return true }, async revokePermissionsStream( { streamId, userId } ) { let streamAclEntriesCount = Acl( ).count( { resourceId: streamId } ) // TODO: check if streamAclEntriesCount === 1 then throw big boo-boo (can't delete last ownership link) if ( streamAclEntriesCount === 1 ) throw new Error( 'Stream has only one ownership link left - cannot revoke permissions.' ) // TODO: below behaviour not correct. Flow: // Count owners // If owner count > 1, then proceed to delete, otherwise throw an error (can't delete last owner - delete stream) let aclEntry = await Acl( ).where( { resourceId: streamId, userId: userId } ).select( '*' ).first( ) if ( aclEntry.role === 'stream:owner' ) { let ownersCount = Acl( ).count( { resourceId: streamId, role: 'stream:owner' } ) if ( ownersCount === 1 ) throw new Error( 'Could not revoke permissions for user' ) else { await Acl( ).where( { resourceId: streamId, userId: userId } ).del( ) return true } } let delCount = await Acl( ).where( { resourceId: streamId, userId: userId } ).del( ) if ( delCount === 0 ) throw new Error( 'Could not revoke permissions for user' ) // update stream updated at await Streams().where( { id: streamId } ).update( { updatedAt: knex.fn.now() } ) return true }, async deleteStream( { streamId } ) { debug( 'speckle:db' )( 'Deleting stream ' + streamId ) // Delete stream commits (not automatically cascaded) await knex.raw( ` DELETE FROM commits WHERE id IN ( SELECT sc."commitId" FROM streams s INNER JOIN stream_commits sc ON s.id = sc."streamId" WHERE s.id = ? ) `, [ streamId ] ) return await Streams( ).where( { id: streamId } ).del( ) }, async getUserStreams( { userId, limit, cursor, publicOnly, searchQuery } ) { limit = limit || 25 publicOnly = publicOnly !== false //defaults to true if not provided let query = Acl( ) .columns( [ { id: 'streams.id' }, 'name', 'description', 'isPublic', 'createdAt', 'updatedAt', 'role' ] ).select( ) .join( 'streams', 'stream_acl.resourceId', 'streams.id' ) .where( 'stream_acl.userId', userId ) if ( cursor ) query.andWhere( 'streams.updatedAt', '<', cursor ) if ( publicOnly ) query.andWhere( 'streams.isPublic', true ) if ( searchQuery ) query.andWhere( function () { this.where( 'name', 'ILIKE', `%${ searchQuery }%` ) .orWhere( 'description', 'ILIKE', `%${searchQuery}%` ) .orWhere( 'id', 'ILIKE', `%${searchQuery}%` ) //potentially useless? } ) query.orderBy( 'streams.updatedAt', 'desc' ).limit( limit ) let rows = await query return { streams: rows, cursor: rows.length > 0 ? rows[ rows.length - 1 ].updatedAt.toISOString( ) : null } }, async getStreams( { offset, limit, orderBy, visibility, searchQuery } ) { let query = knex .column( 'streams.*', knex.raw( 'coalesce(sum(pg_column_size(objects.data)),0) as size' ) ) .select() .from( 'streams' ) .leftJoin( 'objects', 'streams.id', 'objects.streamId' ) .groupBy( 'streams.id' ) let countQuery = Streams( ) if ( searchQuery ) { const whereFunc = function () { this.where( 'streams.name', 'ILIKE', `%${ searchQuery }%` ) .orWhere( 'streams.description', 'ILIKE', `%${searchQuery}%` ) } query.where( whereFunc ) countQuery.where( whereFunc ) } if ( visibility && visibility !== 'all' ) { if ( ![ 'private', 'public' ].includes( visibility ) ) throw new Error( 'Stream visibility should be either private, public or all' ) let isPublic = visibility === 'public' const publicFunc = function() { this.where( { isPublic } ) } query.andWhere( publicFunc ) countQuery.andWhere( publicFunc ) } let [ res ] = await countQuery.count( ) let count = parseInt( res.count ) if ( !count ) return { streams: [], totalCount: 0 } orderBy = orderBy || 'updatedAt,desc' let [ columnName, order ] = orderBy.split( ',' ) let rows = await query.orderBy( `${columnName}`, order ).offset( offset ).limit( limit ) return { streams: rows, totalCount: count } }, async getUserStreamsCount( { userId, publicOnly, searchQuery } ) { publicOnly = publicOnly !== false //defaults to true if not provided let query = Acl( ).count( ) .join( 'streams', 'stream_acl.resourceId', 'streams.id' ) .where( { userId: userId } ) if ( publicOnly ) query.andWhere( 'streams.isPublic', true ) if ( searchQuery ) query.andWhere( function () { this.where( 'name', 'ILIKE', `%${searchQuery}%` ) .orWhere( 'description', 'ILIKE', `%${searchQuery}%` ) .orWhere( 'id', 'ILIKE', `%${searchQuery}%` ) //potentially useless? } ) let [ res ] = await query return parseInt( res.count ) }, async getStreamUsers( { streamId } ) { let query = Acl( ).columns( { role: 'stream_acl.role' }, 'id', 'name', 'company', 'avatar' ).select( ) .where( { resourceId: streamId } ) .rightJoin( 'users', { 'users.id': 'stream_acl.userId' } ) .select( 'stream_acl.role', 'name', 'id', 'company', 'avatar' ) .orderBy( 'stream_acl.role' ) return await query }, } const adjectives = [ 'Tall', 'Curved', 'Stacked', 'Purple', 'Pink', 'Rectangular', 'Circular', 'Oval', 'Shiny', 'Speckled', 'Blue', 'Stretched', 'Round', 'Spherical', 'Majestic', 'Symmetrical' ] const nouns = [ 'Building', 'House', 'Treehouse', 'Tower', 'Tunnel', 'Bridge', 'Pyramid', 'Structure', 'Edifice', 'Palace', 'Castle', 'Villa' ] const generateStreamName = () => { return `${adjectives[Math.floor( Math.random()*adjectives.length )]} ${nouns[Math.floor( Math.random()*nouns.length )]}` }