diff --git a/modules/core/graph/resolvers/branches.js b/modules/core/graph/resolvers/branches.js index 4b2f20c20..cddb9fce6 100644 --- a/modules/core/graph/resolvers/branches.js +++ b/modules/core/graph/resolvers/branches.js @@ -22,7 +22,7 @@ const { } = require( '../../services/branches' ) module.exports = { - Query: {}, + Query: { }, Stream: { async branches( parent, args, context, info ) { throw new ApolloError( 'not implemented' ) diff --git a/modules/core/graph/resolvers/streams.js b/modules/core/graph/resolvers/streams.js index b3a672e94..6a44f2d21 100644 --- a/modules/core/graph/resolvers/streams.js +++ b/modules/core/graph/resolvers/streams.js @@ -1,7 +1,17 @@ 'use strict' const { AuthorizationError, ApolloError } = require( 'apollo-server-express' ) const appRoot = require( 'app-root-path' ) -const { createStream, getStream, updateStream, deleteStream, getUserStreams, getStreamUsers, grantPermissionsStream, revokePermissionsStream } = require( '../../services/streams' ) +const { + createStream, + getStream, + updateStream, + deleteStream, + getUserStreams, + getUserStreamsCount, + getStreamUsers, + grantPermissionsStream, + revokePermissionsStream +} = require( '../../services/streams' ) const { validateServerRole, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` ) module.exports = { @@ -24,9 +34,12 @@ module.exports = { async streams( parent, args, context, info ) { // Return only the user's public streams if parent.id !== context.userId let publicOnly = parent.id !== context.userId - let streams = await getUserStreams( { userId: parent.id, offset: args.offset, limit: args.limit, publicOnly } ) - // TODO: Implement offsets in service, not in friggin array slice - return { totalCount: streams.length, streams: streams.slice( args.offset, args.offset + args.limit ) } + + let totalCount = await getUserStreamsCount( { userId: parent.id } ) + + let { cursor, streams } = await getUserStreams( { userId: parent.id, limit: args.limit, cursor: args.cursor, publicOnly: publicOnly } ) + + return { totalCount, cursor: cursor, streams: streams } } }, Mutation: { @@ -70,4 +83,4 @@ module.exports = { return await revokePermissionsStream( { ...args } ) } } -} \ No newline at end of file +} diff --git a/modules/core/graph/resolvers/user.js b/modules/core/graph/resolvers/user.js index 0f4d49147..e6808373a 100644 --- a/modules/core/graph/resolvers/user.js +++ b/modules/core/graph/resolvers/user.js @@ -60,4 +60,4 @@ module.exports = { return true } } -} \ No newline at end of file +} diff --git a/modules/core/graph/schemas/streams.graphql b/modules/core/graph/schemas/streams.graphql index 1e87ae39a..a4c61f373 100644 --- a/modules/core/graph/schemas/streams.graphql +++ b/modules/core/graph/schemas/streams.graphql @@ -1,5 +1,5 @@ extend type Query { - stream(id: String!): Stream + stream( id: String! ): Stream } type Stream { @@ -9,24 +9,24 @@ type Stream { isPublic: Boolean! createdAt: String! updatedAt: String! - users: [User]! + users: [ User ]! } extend type User { """ All the streams that a user has access to. """ - streams(offset: Int! = 0, limit: Int! = 100): StreamCollection + streams( limit: Int! = 20, cursor: String ): StreamCollectionUser """ The role this user has on a specific stream (can be populated when accessing a stream's users). """ role: String } -type StreamCollection { +type StreamCollectionUser { totalCount: Int! - streams: [Stream] cursor: String + streams: [ Stream ] } extend type Mutation { @@ -43,7 +43,7 @@ extend type Mutation { """ streamDelete( id: String! ): Boolean! """ - Grants permissions to an user on a given stream. + Grants permissions to an user on a given stream. """ streamGrantPermission( streamId: String!, userId: String!, role: String! ): Boolean """ @@ -64,4 +64,4 @@ input StreamUpdateInput { name: String description: String isPublic: Boolean -} \ No newline at end of file +} diff --git a/modules/core/services/commits.js b/modules/core/services/commits.js index 4a7cdfa85..b87c5f15a 100644 --- a/modules/core/services/commits.js +++ b/modules/core/services/commits.js @@ -121,7 +121,8 @@ module.exports = { async getCommitsByStreamId( { streamId, limit, cursor } ) { limit = limit || 20 - let query = StreamCommits( ).columns( [ 'commitId', 'message', 'referencedObject', { author: 'name' }, { authorId: 'users.id' }, 'commits.createdAt' ] ).select( ) + let query = StreamCommits( ) + .columns( [ 'commitId', 'message', 'referencedObject', { author: 'name' }, { authorId: 'users.id' }, 'commits.createdAt' ] ).select( ) .join( 'commits', 'commits.id', 'stream_commits.commitId' ) .join( 'users', 'commits.author', 'users.id' ) .where( 'streamId', streamId ) @@ -163,4 +164,4 @@ module.exports = { let [ res ] = await Commits( ).count( ).where( 'author', userId ) return parseInt( res.count ) } -} \ No newline at end of file +} diff --git a/modules/core/services/streams.js b/modules/core/services/streams.js index 7444e1f06..91bb101f1 100644 --- a/modules/core/services/streams.js +++ b/modules/core/services/streams.js @@ -62,7 +62,7 @@ module.exports = { throw new Error( 'Stream has only one ownership link left - cannot revoke permissions.' ) // TODO: below behaviour not correct. Flow: - // Count owners + // 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( ) @@ -85,22 +85,37 @@ module.exports = { return true }, - async deleteStream( { streamId } ) { + async deleteStream( { streamId } ) { return await Streams( ).where( { id: streamId } ).del( ) }, - async getUserStreams( { userId, offset, limit, publicOnly } ) { - offset = offset || 0 + async getUserStreams( { userId, limit, cursor, publicOnly } ) { limit = limit || 100 publicOnly = publicOnly !== false //defaults to true if not provided - let query = { userId: userId } if ( publicOnly ) query.isPublic = true - return Acl( ).where( query ) - .rightJoin( 'streams', { 'streams.id': 'stream_acl.resourceId' } ) - .limit( limit ).offset( offset ) + let query = Acl( ) + .columns( [ { id: 'streams.id' }, 'name', 'description', 'isPublic', 'createdAt', 'updatedAt' ] ).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 ) + + query.orderBy( 'streams.updatedAt', 'desc' ).limit( limit ) + + let rows = await query + return { streams: rows, cursor: rows.length > 0 ? rows[ rows.length - 1 ].updatedAt : null } + }, + + async getUserStreamsCount( { userId } ) { + let [ res ] = await Acl( ).count( ).where( { userId: userId } ) + return parseInt( res.count ) }, async getStreamUsers( { streamId } ) { @@ -108,4 +123,4 @@ module.exports = { .rightJoin( 'users', { 'users.id': 'stream_acl.userId' } ) .select( 'role', 'username', 'name', 'id' ) } -} \ No newline at end of file +} diff --git a/modules/core/tests/graph.spec.js b/modules/core/tests/graph.spec.js index 57056538f..c2ad6bfb6 100644 --- a/modules/core/tests/graph.spec.js +++ b/modules/core/tests/graph.spec.js @@ -80,7 +80,6 @@ describe( 'GraphQL API Core', ( ) => { expect( res1.body.data.apiTokenCreate ).to.be.a( 'string' ) token1 = `Bearer ${res1.body.data.apiTokenCreate}` - const res2 = await sendRequest( userA.token, { query: `mutation { apiTokenCreate(name:"Token 1", scopes: ["streams:write", "streams:read", "users:email"]) }` } ) token2 = `Bearer ${res2.body.data.apiTokenCreate}` @@ -414,12 +413,14 @@ describe( 'GraphQL API Core', ( ) => { it( 'Should retrieve my streams', async ( ) => { - const res = await sendRequest( userA.token, { query: `{ user { streamCollection { totalCount streams { id name role } } } }` } ) + const res = await sendRequest( userA.token, { query: `{ user { streams { totalCount streams { id name } } } }` } ) + // console.log( res.body.errors[0].locations ) + console.log( res.body.data ) expect( res ).to.be.json expect( res.body.errors ).to.not.exist - expect( res.body.data.user.streamCollection.totalCount ).to.equal( 3 ) + expect( res.body.data.user.streams.totalCount ).to.equal( 3 ) - let streams = res.body.data.user.streamCollection.streams + let streams = res.body.data.user.streams.streams let s1 = streams.find( s => s.name === 'TS1 (u A) Private UPDATED' ) expect( s1 ).to.exist } ) @@ -541,6 +542,7 @@ describe( 'GraphQL API Core', ( ) => { } ) } ) + describe( 'Objects', ( ) => { let myCommit let myObjs @@ -665,6 +667,7 @@ describe( 'GraphQL API Core', ( ) => { } ) describe( 'Server Info', ( ) => { + it( 'Should return a valid server information object', async ( ) => { let q = ` query{ diff --git a/modules/shared/index.js b/modules/shared/index.js index 4b019f053..5d18026b4 100644 --- a/modules/shared/index.js +++ b/modules/shared/index.js @@ -6,7 +6,7 @@ const knex = require( `${appRoot}/db/knex` ) const { validateToken } = require( `${appRoot}/modules/core/services/tokens` ) /* - + Graphql server context helper */ @@ -41,14 +41,14 @@ async function contextMiddleware( req, res, next ) { } /* - + Keeps track of all the available roles on this server. It's seeded by the methods below. */ let roles /* - + Validates a user's server-bound role (admin, normal user, etc.) */ @@ -72,7 +72,7 @@ async function validateServerRole( context, requiredRole ) { } /* - + Graphql scope validator */ @@ -85,7 +85,7 @@ async function validateScopes( scopes, scope ) { } /* - + Graphql authorization: checks user id against access control lists */ @@ -94,7 +94,7 @@ async function authorizeResolver( userId, resourceId, requiredRole ) { if ( !roles ) roles = await knex( 'user_roles' ).select( '*' ) - // TODO: Cache these results with a TTL of 1 mins or so, it's pointless to query the db every time we get a ping. + // TODO: Cache these results with a TTL of 1 mins or so, it's pointless to query the db every time we get a ping. let role = roles.find( r => r.name === requiredRole ) @@ -126,4 +126,4 @@ module.exports = { validateServerRole, validateScopes, authorizeResolver -} \ No newline at end of file +} diff --git a/package.json b/package.json index 229ba6fb1..649997d82 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev:frontend": "cd frontend && npm run serve", "build:frontend": "cd frontend && npm run build", "dev:server": "NODE_ENV=development POSTGRES_URL=postgres://localhost/speckle2_dev DEBUG=www:server,speckle:* nodemon ./bin/www --watch . --watch ./bin/www -e js,graphql,env", + "dev:server:test": "NODE_ENV=test POSTGRES_URL=postgres://localhost/speckle2_dev DEBUG=www:server,speckle:* nodemon ./bin/www --watch . --watch ./bin/www -e js,graphql,env", "test:server": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test POSTGRES_URL=postgres://localhost/speckle2_test nyc nyc --reporter html --reporter lcovonly mocha -s 0 --timeout 2000 --exit", "test:server:watch": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test POSTGRES_URL=postgres://localhost/speckle2_test mocha --watch -s 0 --exit", "test:server:graph": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test POSTGRES_URL=postgres://localhost/speckle2_test mocha ./modules/core/tests/graph.spec.js --watch -s 0 --exit --no-config",