refactor(gql): services & resolvers around user streams, branches, etc

This commit is contained in:
Dimitrie Stefanescu
2020-07-20 17:27:39 +01:00
parent 2b33ab5aa2
commit 6fd72ad0fa
9 changed files with 69 additions and 36 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ const {
} = require( '../../services/branches' )
module.exports = {
Query: {},
Query: { },
Stream: {
async branches( parent, args, context, info ) {
throw new ApolloError( 'not implemented' )
+18 -5
View File
@@ -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 } )
}
}
}
}
+1 -1
View File
@@ -60,4 +60,4 @@ module.exports = {
return true
}
}
}
}
+7 -7
View File
@@ -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
}
}
+3 -2
View File
@@ -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 )
}
}
}
+24 -9
View File
@@ -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' )
}
}
}
+7 -4
View File
@@ -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{
+7 -7
View File
@@ -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
}
}
+1
View File
@@ -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",