Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matteo/gql #14

Merged
merged 10 commits into from
Jul 29, 2020
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = both
49 changes: 41 additions & 8 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
}
24 changes: 24 additions & 0 deletions modules/core/graph/resolvers/streams.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ 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 )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discussion around max limits & default limits:

  • max limits should be implemented everywhere that returns a collection (so we don't get stuff like limit: 1000000 to potentially crash UIs
  • default limits: should be a wee bit more conservative, depending on the perceived user-land usage:
    • e.g. commits: we can potentially display a longer list, so it can be higher (50?)
    • e.g. streams: i don't think more than 20-ish? 25? would comfortably fit on a page before you need to switch to either a search or a "next page".

Another note, if we want to be lenient, we could just return the default max limit rather than throw an error, but I guess it's good to inform the dev 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, changed all the defaults to 25. Will keep the error as IMHO it's a better experience than having to open the docs and check why out of my ~123 streams only 100 are showing (happened to me in he past).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deal!

throw new UserInputError( 'Cannot return more than 100 results.' )

let totalCount = await getUserStreamsCount( {userId: context.userId, publicOnly: false} )

let {cursor, streams} = await getUserStreams( {userId: context.userId, limit: args.limit, cursor: args.cursor, publicOnly: false} )
return {totalCount, cursor: cursor, items: streams}
},
async streamSearch( parent, args, context, info ) {
await validateScopes( context.scopes, 'streams:read' )

if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 results.' )

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: {
Expand All @@ -35,6 +57,8 @@ module.exports = {
User: {

async streams( parent, args, context, info ) {
if ( args.limit && args.limit > 100 )
throw new UserInputError( 'Cannot return more than 100 results.' )
// 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 } )
Expand Down
Original file line number Diff line number Diff line change
@@ -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` )
Expand All @@ -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 )
Expand All @@ -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 results.' )

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 }
Expand Down Expand Up @@ -64,11 +79,13 @@ module.exports = {

},



Mutation: {
async userEdit( parent, args, context, info ) {
await validateServerRole( context, 'server:user' )
await updateUser( context.userId, args.user )
return true
}
}
}
}
7 changes: 5 additions & 2 deletions modules/core/graph/schemas/streams.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
extend type Query {
stream( id: String! ): Stream
streams( limit: Int! = 100, cursor: String ): StreamCollection
streamSearch( query: String!, limit: Int! = 100, cursor: String ): StreamCollection
}

type Stream {
Expand All @@ -16,7 +18,7 @@ extend type User {
"""
All the streams that a user has access to.
"""
streams( limit: Int! = 20, cursor: String ): StreamCollectionUser
streams( limit: Int! = 20, cursor: String ): StreamCollection
}

type StreamCollaborator {
Expand All @@ -25,12 +27,13 @@ type StreamCollaborator {
role: String!
}

type StreamCollectionUser {
type StreamCollection {
totalCount: Int!
cursor: String
items: [ Stream ]
}


extend type Mutation {
"""
Creates a new stream.
Expand Down
18 changes: 18 additions & 0 deletions modules/core/graph/schemas/user.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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! = 100, cursor: String ): UserSearchResultCollection
userPwdStrength( pwd: String! ): JSONObject
}

Expand All @@ -22,6 +23,23 @@ 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
profiles: JSONObject
role: String
}

extend type Mutation {
"""
Edits a user's profile.
Expand Down
1 change: 0 additions & 1 deletion modules/core/services/branches.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 22 additions & 9 deletions modules/core/services/streams.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ module.exports = {
return await Streams( ).where( { id: streamId } ).del( )
},

async getUserStreams( { userId, limit, cursor, publicOnly } ) {
async getUserStreams( {userId, limit, cursor, publicOnly, searchQuery } ) {
limit = limit || 100
publicOnly = publicOnly !== false //defaults to true if not provided

let likeQuery = "%" + searchQuery + "%"
let query = Acl( )
.columns( [ { id: 'streams.id' }, 'name', 'description', 'isPublic', 'createdAt', 'updatedAt', 'role' ] ).select( )
.join( 'streams', 'stream_acl.resourceId', 'streams.id' )
Expand All @@ -104,34 +104,47 @@ module.exports = {
if ( publicOnly )
query.andWhere( 'streams.isPublic', true )

if ( searchQuery )
query.andWhere( function () {
this.where( 'name', 'ILIKE', likeQuery )
.orWhere( 'description', 'ILIKE', likeQuery )
.orWhere( 'id', 'ILIKE', likeQuery ) //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 likeQuery = "%" + searchQuery + "%"
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', likeQuery )
.orWhere( 'description', 'ILIKE', likeQuery )
.orWhere( 'id', 'ILIKE', likeQuery ) //potentially useless?
} )

let [ res ] = await query
return parseInt( res.count )
},

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
}
Expand Down
Loading