Merge pull request #14 from Speckle-Next/matteo/gql

Matteo/gql
This commit is contained in:
Matteo Cominetti
2020-07-29 23:48:25 +01:00
committed by GitHub
17 changed files with 252 additions and 108 deletions
+1
View File
@@ -7,3 +7,4 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = both
+41 -8
View File
@@ -7,13 +7,46 @@
"parserOptions": {
"ecmaVersion": 11
},
"ignorePatterns": [ "modules/*/tests/*", "node_modules/*", "frontend/*"],
"ignorePatterns": [
"modules/*/tests/*",
"node_modules/*",
"frontend/*"
],
"rules": {
"arrow-spacing": [ 2, { "before": true, "after": true } ],
"array-bracket-spacing": [ 2, "always" ],
"block-spacing": [ 2, "always" ],
"camelcase": [ 1, { "properties": "always" } ],
"space-in-parens": [ 2, "always" ],
"keyword-spacing": 2
"arrow-spacing": [
2,
{
"before": true,
"after": true
}
],
"array-bracket-spacing": [
2,
"always"
],
"block-spacing": [
2,
"always"
],
"camelcase": [
1,
{
"properties": "always"
}
],
"space-in-parens": [
2,
"always"
],
"keyword-spacing": 2,
"semi": "off",
"indent": [
"error",
2
],
"padded-blocks": [
"error",
"never"
]
}
}
}
+2
View File
@@ -30,6 +30,8 @@ module.exports = {
Stream: {
async branches( parent, args, context, info ) {
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let { items, cursor, totalCount } = await getBranchesByStreamId( { streamId: parent.id, limit: args.limit, cursor: args.cursor } )
return { totalCount, cursor, items }
+6 -1
View File
@@ -32,6 +32,8 @@ module.exports = {
Stream: {
async commits( parent, args, context, info ) {
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let { commits: items, cursor } = await getCommitsByStreamId( { streamId: parent.id, limit: args.limit, cursor: args.cursor } )
let totalCount = await getCommitsTotalCountByStreamId( { streamId: parent.id } )
@@ -47,9 +49,10 @@ module.exports = {
User: {
async commits( parent, args, context, info ) {
let publicOnly = context.userId !== parent.id
let totalCount = await getCommitsTotalCountByUserId( { userId: parent.id } )
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let { commits: items, cursor } = await getCommitsByUserId( { userId: parent.id, limit: args.limit, cursor: args.cursor, publicOnly } )
return { items, cursor, totalCount }
@@ -58,6 +61,8 @@ module.exports = {
},
Branch: {
async commits( parent, args, context, info ) {
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let { commits, cursor } = await getCommitsByBranchId( { branchId: parent.id, limit: args.limit, cursor: args.cursor } )
let totalCount = await getCommitsTotalCountByBranchId( { branchId: parent.id } )
+14 -1
View File
@@ -22,6 +22,17 @@ module.exports = {
let stream = await getStream( { streamId: args.id } )
return stream
},
async streams( parent, args, context, info ) {
await validateScopes( context.scopes, 'streams:read' )
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let totalCount = await getUserStreamsCount( {userId: context.userId, publicOnly: false, searchQuery: args.query} )
let {cursor, streams} = await getUserStreams( {userId: context.userId, limit: args.limit, cursor: args.cursor, publicOnly: false, searchQuery: args.query} )
return {totalCount, cursor: cursor, items: streams}
}
},
Stream: {
@@ -35,7 +46,9 @@ module.exports = {
User: {
async streams( parent, args, context, info ) {
// Return only the user's public streams if parent.id !== context.userId
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
// Return only the user's public streams if parent.id !== context.userId
let publicOnly = parent.id !== context.userId
let totalCount = await getUserStreamsCount( { userId: parent.id, publicOnly } )
@@ -1,7 +1,7 @@
'use strict'
const appRoot = require( 'app-root-path' )
const { ApolloError, AuthenticationError, UserInputError } = require( 'apollo-server-express' )
const { createUser, getUser, getUserByEmail, getUserRole, updateUser, deleteUser, validatePasssword } = require( '../../services/users' )
const { createUser, getUser, getUserByEmail, getUserRole, updateUser, deleteUser, searchUsers, validatePasssword } = require( '../../services/users' )
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../../services/tokens' )
const { validateServerRole, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` )
const setupCheck = require( `${appRoot}/setupcheck` )
@@ -10,12 +10,11 @@ const zxcvbn = require( 'zxcvbn' )
module.exports = {
Query: {
async _( ) {
async _() {
return `Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn.`
},
async user( parent, args, context, info ) {
await validateServerRole( context, 'server:user' )
if ( !args.id )
@@ -30,6 +29,22 @@ module.exports = {
return await getUser( args.id || context.userId )
},
async userSearch( parent, args, context, info ) {
await validateServerRole( context, 'server:user' )
await validateScopes( context.scopes, 'profile:read' )
await validateScopes( context.scopes, 'users:read' )
if ( args.query.length < 3 )
throw new UserInputError( 'Search query must be at least 3 carachters.' )
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' )
let {cursor, users} = await searchUsers( args.query, args.limit, args.cursor )
return {cursor: cursor, items: users}
},
async userPwdStrength( parent, args, context, info ) {
let res = zxcvbn( args.pwd )
return { score: res.score, feedback: res.feedback }
@@ -64,6 +79,8 @@ module.exports = {
},
Mutation: {
async userEdit( parent, args, context, info ) {
await validateServerRole( context, 'server:user' )
@@ -71,4 +88,4 @@ module.exports = {
return true
}
}
}
}
@@ -1,12 +1,12 @@
extend type Stream {
commits( limit: Int! = 20, cursor: String ): CommitCollection
commits( limit: Int! = 25, cursor: String ): CommitCollection
commit( id: String! ): Commit
branches( limit: Int! = 100, cursor: String ): BranchCollection
branches( limit: Int! = 25, cursor: String ): BranchCollection
branch( name: String! ): Branch
}
extend type User {
commits( limit: Int! = 20, cursor: String ): CommitCollectionUser
commits( limit: Int! = 25, cursor: String ): CommitCollectionUser
}
type Branch {
@@ -14,7 +14,7 @@ type Branch {
name: String!
author: User!
description: String!
commits( limit: Int! = 20, cursor: String ): CommitCollection
commits( limit: Int! = 25, cursor: String ): CommitCollection
}
type Commit {
+7 -2
View File
@@ -1,5 +1,9 @@
extend type Query {
stream( id: String! ): Stream
"""
All the streams of the current user, pass in the `query` parameter to seach by name, description or ID.
"""
streams( query: String!, limit: Int! = 25, cursor: String ): StreamCollection
}
type Stream {
@@ -16,7 +20,7 @@ extend type User {
"""
All the streams that a user has access to.
"""
streams( limit: Int! = 20, cursor: String ): StreamCollectionUser
streams( limit: Int! = 25, cursor: String ): StreamCollection
}
type StreamCollaborator {
@@ -25,12 +29,13 @@ type StreamCollaborator {
role: String!
}
type StreamCollectionUser {
type StreamCollection {
totalCount: Int!
cursor: String
items: [ Stream ]
}
extend type Mutation {
"""
Creates a new stream.
+16
View File
@@ -3,6 +3,7 @@ extend type Query {
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
"""
user( id: String ): User
userSearch( query: String!, limit: Int! = 25, cursor: String ): UserSearchResultCollection
userPwdStrength( pwd: String! ): JSONObject
}
@@ -22,6 +23,21 @@ type User {
role: String
}
type UserSearchResultCollection {
cursor: String
items: [ UserSearchResult ]
}
type UserSearchResult {
id: String!
username: String
name: String
bio: String
company: String
avatar: String
verified: Boolean
}
extend type Mutation {
"""
Edits a user's profile.
@@ -6,50 +6,50 @@ exports.up = async knex => {
debug( 'Setting up core module scopes.' )
let coreModuleScopes = [ {
name: 'server:setup',
description: 'Edit server information.'
},
{
name: 'tokens:read',
description: `Access your api tokens.`
},
{
name: 'tokens:write',
description: `Create and delete api tokens on your behalf.`
},
{
name: 'apps:authorize',
description: 'Grant third party applications access rights on your behalf to the api.'
},
{
name: 'apps:create',
description: 'Register a third party application.'
},
{
name: 'streams:read',
description: 'Read your streams & and any associated information (branches, tags, comments, objects, etc.)'
},
{
name: 'streams:write',
description: 'Create streams on your behalf and read your streams & any associated information (any associated information (branches, tags, comments, objects, etc.)'
},
{
name: 'profile:read',
description: `Read your profile information`
},
{
name: 'profile:email',
description: `Access your email.`
},
name: 'server:setup',
description: 'Edit server information.'
},
{
name: 'tokens:read',
description: `Access your api tokens.`
},
{
name: 'tokens:write',
description: `Create and delete api tokens on your behalf.`
},
{
name: 'apps:authorize',
description: 'Grant third party applications access rights on your behalf to the api.'
},
{
name: 'apps:create',
description: 'Register a third party application.'
},
{
name: 'streams:read',
description: 'Read your streams & and any associated information (branches, tags, comments, objects, etc.)'
},
{
name: 'streams:write',
description: 'Create streams on your behalf and read your streams & any associated information (any associated information (branches, tags, comments, objects, etc.)'
},
{
name: 'profile:read',
description: `Read your profile information`
},
{
name: 'profile:email',
description: `Access your email.`
},
{
name: 'users:read',
description: `Read other users' profile on your behalf.`
},
{
name: 'users:email',
description: 'Access the emails of other users.'
}
{
name: 'users:read',
description: `Read other users' profile on your behalf.`
},
{
name: 'users:email',
description: 'Access the emails of other users.'
}
]
await knex( 'scopes' ).insert( coreModuleScopes )
+1 -2
View File
@@ -10,7 +10,6 @@ const BranchCommits = ( ) => knex( 'branch_commits' )
module.exports = {
async createBranch( { name, description, streamId, authorId } ) {
let branch = {}
branch.id = crs( { length: 10 } )
branch.streamId = streamId
@@ -32,7 +31,7 @@ module.exports = {
},
async getBranchesByStreamId( { streamId, limit, cursor } ) {
limit = limit || 100
limit = limit || 25
let query = Branches( ).select( '*' ).where( { streamId: streamId } )
if ( cursor )
+7 -8
View File
@@ -15,7 +15,6 @@ const { getBranchesByStreamId, getBranchByNameAndStreamId } = require( './branch
module.exports = {
async createCommitByBranchId( { streamId, branchId, objectId, authorId, message, previousCommitIds } ) {
// Create main table entry
let [ id ] = await Commits( ).returning( 'id' ).insert( {
id: crs( { length: 10 } ),
@@ -84,7 +83,7 @@ module.exports = {
},
async getCommitsByBranchId( { branchId, limit, cursor } ) {
limit = limit || 20
limit = limit || 25
let query = BranchCommits( ).columns( [ { id: 'commitId' }, 'message', 'referencedObject', { authorName: 'name' }, { authorId: 'users.id' }, 'commits.createdAt' ] ).select( )
.join( 'commits', 'commits.id', 'branch_commits.commitId' )
.join( 'users', 'commits.author', 'users.id' )
@@ -124,7 +123,7 @@ module.exports = {
* @return {[type]} [description]
*/
async getCommitsByStreamId( { streamId, limit, cursor } ) {
limit = limit || 20
limit = limit || 25
let query = StreamCommits( )
.columns( [ { id: 'commitId' }, 'message', 'referencedObject', { authorName: 'name' }, { authorId: 'users.id' }, 'commits.createdAt' ] ).select( )
.join( 'commits', 'commits.id', 'stream_commits.commitId' )
@@ -142,15 +141,15 @@ module.exports = {
},
async getCommitsByUserId( { userId, limit, cursor, publicOnly } ) {
limit = limit || 20
limit = limit || 25
publicOnly = publicOnly !== false
let query =
Commits( )
.columns( [ { id: 'commitId' }, 'message', 'referencedObject', 'commits.createdAt', { streamId: 'stream_commits.streamId' }, { streamName: 'streams.name' } ] ).select( )
.join( 'stream_commits', 'commits.id', 'stream_commits.commitId' )
.join( 'streams', 'stream_commits.streamId', 'streams.id' )
.where( 'author', userId )
.columns( [ { id: 'commitId' }, 'message', 'referencedObject', 'commits.createdAt', { streamId: 'stream_commits.streamId' }, { streamName: 'streams.name' } ] ).select( )
.join( 'stream_commits', 'commits.id', 'stream_commits.commitId' )
.join( 'streams', 'stream_commits.streamId', 'streams.id' )
.where( 'author', userId )
if ( publicOnly )
query.andWhere( 'streams.isPublic', true )
+21 -8
View File
@@ -89,8 +89,8 @@ module.exports = {
return await Streams( ).where( { id: streamId } ).del( )
},
async getUserStreams( { userId, limit, cursor, publicOnly } ) {
limit = limit || 100
async getUserStreams( { userId, limit, cursor, publicOnly, searchQuery } ) {
limit = limit || 25
publicOnly = publicOnly !== false //defaults to true if not provided
let query = Acl( )
@@ -104,14 +104,20 @@ module.exports = {
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 getUserStreamsCount( { userId, publicOnly } ) {
async getUserStreamsCount( {userId, publicOnly, searchQuery } ) {
publicOnly = publicOnly !== false //defaults to true if not provided
let query = Acl( ).count( )
@@ -121,6 +127,13 @@ module.exports = {
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 )
},
@@ -128,10 +141,10 @@ module.exports = {
async getStreamUsers( { streamId } ) {
let query =
Acl( ).columns( { role: 'stream_acl.role' }, 'id', 'name' ).select( )
.where( { resourceId: streamId } )
.rightJoin( 'users', { 'users.id': 'stream_acl.userId' } )
.select( 'stream_acl.role', 'username', 'name', 'id' )
.orderBy( 'stream_acl.role' )
.where( { resourceId: streamId } )
.rightJoin( 'users', { 'users.id': 'stream_acl.userId' } )
.select( 'stream_acl.role', 'username', 'name', 'id' )
.orderBy( 'stream_acl.role' )
return await query
}
+45 -25
View File
@@ -4,73 +4,73 @@ const crs = require( 'crypto-random-string' )
const appRoot = require( 'app-root-path' )
const knex = require( `${appRoot}/db/knex` )
const Users = ( ) => knex( 'users' )
const ServerRoles = ( ) => knex( 'server_acl' )
const Users = () => knex( 'users' )
const Acl = () => knex( 'server_acl' )
module.exports = {
/*
Users
Users
*/
*/
async createUser( user ) {
let [ { count } ] = await ServerRoles( ).where( { role: 'server:admin' } ).count( )
let [ {count} ] = await Acl().where( {role: 'server:admin'} ).count()
user.id = crs( { length: 10 } )
user.id = crs( {length: 10} )
if ( user.password ) {
user.passwordDigest = await bcrypt.hash( user.password, 10 )
}
delete user.password
let usr = await Users( ).select( 'id' ).where( { email: user.email } ).first( )
let usr = await Users().select( 'id' ).where( {email: user.email} ).first()
if ( usr ) throw new Error( 'Email taken. Try logging in?' )
let res = await Users( ).returning( 'id' ).insert( user )
let res = await Users().returning( 'id' ).insert( user )
if ( parseInt( count ) === 0 ) {
await ServerRoles( ).insert( { userId: res[ 0 ], role: 'server:admin' } )
await Acl().insert( {userId: res[0], role: 'server:admin'} )
} else {
await ServerRoles( ).insert( { userId: res[ 0 ], role: 'server:user' } )
await Acl().insert( {userId: res[0], role: 'server:user'} )
}
return res[ 0 ]
return res[0]
},
async findOrCreateUser( { user, rawProfile } ) {
let existingUser = await Users( ).select( 'id' ).where( { email: user.email } ).first( )
async findOrCreateUser( {user, rawProfile} ) {
let existingUser = await Users().select( 'id' ).where( {email: user.email} ).first()
if ( existingUser )
return existingUser
user.password = crs( { length: 20 } )
user.password = crs( {length: 20} )
user.verified = true // because we trust the external identity provider, no?
return { id: await module.exports.createUser( user ) }
return {id: await module.exports.createUser( user )}
},
async getUserById( { userId } ) {
let user = await Users( ).where( { id: userId } ).select( '*' ).first( )
async getUserById( {userId} ) {
let user = await Users().where( {id: userId} ).select( '*' ).first()
delete user.passwordDigest
return user
},
// TODO: deprecate
async getUser( id ) {
let user = await Users( ).where( { id: id } ).select( '*' ).first( )
let user = await Users().where( {id: id} ).select( '*' ).first()
delete user.passwordDigest
return user
},
async getUserByEmail( { email } ) {
let user = await Users( ).where( { email: email } ).select( '*' ).first( )
async getUserByEmail( {email} ) {
let user = await Users().where( {email: email} ).select( '*' ).first()
delete user.passwordDigest
return user
},
async getUserRole( id ) {
let { role } = await ServerRoles( ).where( { userId: id } ).select( 'role' ).first( )
let {role} = await Acl().where( {userId: id} ).select( 'role' ).first()
return role
},
@@ -79,15 +79,35 @@ module.exports = {
delete user.passwordDigest
delete user.password
delete user.email
await Users( ).where( { id: id } ).update( user )
await Users().where( {id: id} ).update( user )
},
async validatePasssword( { email, password } ) {
let { passwordDigest } = await Users( ).where( { email: email } ).select( 'passwordDigest' ).first( )
async searchUsers( searchQuery, limit, cursor ) {
limit = limit || 25
let query = Users()
.select( 'id', 'username', 'name', 'bio', 'company', 'verified', 'avatar', 'createdAt' )
.where( queryBuilder => {
queryBuilder.where( {email: searchQuery} ) //match full email or partial username / name
queryBuilder.orWhere( 'username', 'ILIKE', `%${searchQuery}%` )
queryBuilder.orWhere( 'name', 'ILIKE', `%${searchQuery}%` )
} )
if ( cursor )
query.andWhere( 'users.createdAt', '<', cursor )
query.orderBy( 'users.createdAt', 'desc' ).limit( limit )
let rows = await query
return {users: rows, cursor: rows.length > 0 ? rows[rows.length - 1].createdAt.toISOString() : null}
},
async validatePasssword( {email, password} ) {
let {passwordDigest} = await Users().where( {email: email} ).select( 'passwordDigest' ).first()
return bcrypt.compare( password, passwordDigest )
},
async deleteUser( id ) {
throw new Error( 'not implemented' )
}
}
}
+8 -1
View File
@@ -65,13 +65,20 @@ describe( 'Streams', ( ) => {
expect( stream.description ).to.equal( 'Wooot' )
} )
it( 'Should get all streams for a user', async ( ) => {
it( 'Should get all streams of a user', async ( ) => {
let { streams, cursor } = await getUserStreams( { userId: userOne.id } )
// console.log( res )
expect( streams ).to.have.lengthOf( 2 )
expect( cursor ).to.exist
} )
it('Should search all streams of a user', async () => {
let {streams, cursor} = await getUserStreams({userId: userOne.id, searchQuery: "woo"})
// console.log( res )
expect(streams).to.have.lengthOf(1)
expect(cursor).to.exist
})
it( 'Should delete a stream', async ( ) => {
const id = await createStream( { name: 'mayfly', description: 'wonderful', ownerId: userOne.id } )
let all = await getUserStreams( { userId: userOne.id } )
+7 -1
View File
@@ -11,7 +11,7 @@ chai.use( chaiHttp )
const knex = require( `${appRoot}/db/knex` )
const { createUser, getUser, updateUser, deleteUser, validatePasssword } = require( '../services/users' )
const {createUser, getUser, searchUsers, updateUser, deleteUser, validatePasssword } = require( '../services/users' )
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' )
describe( 'Actors & Tokens', ( ) => {
@@ -61,6 +61,12 @@ describe( 'Actors & Tokens', ( ) => {
expect( actor ).to.not.have.property( 'passwordDigest' )
} )
it('Should search and get an users', async () => {
let {users} = await searchUsers("gates", 20, null)
expect(users).to.have.lengthOf(1)
expect(users[0].name).to.equal("Bill Gates")
})
it( 'Should update an actor', async ( ) => {
let updatedActor = { ...myTestActor }
updatedActor.username = 'didimitrie'
+8
View File
@@ -28,6 +28,14 @@ When pushing commits to this repo, please follow the following guidelines:
- Install [commitizen](https://www.npmjs.com/package/commitizen#commitizen-for-contributors) globally (`npm i -g commitizen`).
- When ready to commit, type in the commandline `git cz` & follow the prompts.
- Install eslint globally `npm i -g eslint`
- if using VS code install the `eslint` extension
- we also recommend setting it to run on save by adding the following VS Code setting
```
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
```
## Modules