diff --git a/packages/authx/package.json b/packages/authx/package.json index 7bd41875..34607353 100644 --- a/packages/authx/package.json +++ b/packages/authx/package.json @@ -9,6 +9,7 @@ "@types/auth-header": "^1.0.1", "@types/form-data": "^2.2.1", "@types/graphql-api-koa": "^2.0.0", + "@types/graphql-relay": "^0.4.9", "@types/jsonwebtoken": "^8.3.2", "@types/koa": "^2.0.48", "@types/koa-router": "^7.0.40", @@ -20,6 +21,7 @@ "graphql": "^14.3.1", "graphql-api-koa": "^2.0.0", "graphql-playground-middleware-koa": "^1.6.12", + "graphql-relay": "^0.6.0", "jsonwebtoken": "^8.5.0", "koa": "^2.7.0", "koa-body": "^4.1.0", diff --git a/packages/authx/src/graphql/GraphQLAuthorityConnection.ts b/packages/authx/src/graphql/GraphQLAuthorityConnection.ts new file mode 100644 index 00000000..9f02ee8a --- /dev/null +++ b/packages/authx/src/graphql/GraphQLAuthorityConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLAuthorityEdge } from "./GraphQLAuthorityEdge"; + +export const GraphQLAuthorityConnection = new GraphQLObjectType({ + name: "AuthorityConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLAuthorityEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLAuthorityEdge.ts b/packages/authx/src/graphql/GraphQLAuthorityEdge.ts new file mode 100644 index 00000000..261694c1 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLAuthorityEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLAuthority } from "./GraphQLAuthority"; + +export const GraphQLAuthorityEdge = new GraphQLObjectType({ + name: "AuthorityEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLAuthority } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLAuthorizationConnection.ts b/packages/authx/src/graphql/GraphQLAuthorizationConnection.ts new file mode 100644 index 00000000..41dd4d32 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLAuthorizationConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLAuthorizationEdge } from "./GraphQLAuthorizationEdge"; + +export const GraphQLAuthorizationConnection = new GraphQLObjectType({ + name: "AuthorizationConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLAuthorizationEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLAuthorizationEdge.ts b/packages/authx/src/graphql/GraphQLAuthorizationEdge.ts new file mode 100644 index 00000000..2a19bc56 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLAuthorizationEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLAuthorization } from "./GraphQLAuthorization"; + +export const GraphQLAuthorizationEdge = new GraphQLObjectType({ + name: "AuthorizationEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLAuthorization } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLClient.ts b/packages/authx/src/graphql/GraphQLClient.ts index 2ea1d36f..fc180382 100644 --- a/packages/authx/src/graphql/GraphQLClient.ts +++ b/packages/authx/src/graphql/GraphQLClient.ts @@ -7,9 +7,15 @@ import { GraphQLObjectType } from "graphql"; +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { Client } from "../model"; import { Context } from "../Context"; -import { GraphQLUser } from "./GraphQLUser"; +import { GraphQLUserConnection } from "./GraphQLUserConnection"; import { filter } from "../util/filter"; export const GraphQLClient = new GraphQLObjectType({ @@ -36,14 +42,22 @@ export const GraphQLClient = new GraphQLObjectType({ }, urls: { type: new GraphQLList(GraphQLString) }, users: { - type: new GraphQLList(GraphQLUser), - async resolve(client, args, { realm, authorization: a, tx }: Context) { + type: GraphQLUserConnection, + args: connectionArgs, + async resolve( + client, + args: ConnectionArguments, + { realm, authorization: a, tx }: Context + ) { return a && (await client.isAccessibleBy(realm, a, tx, "read.assignments")) - ? filter(await client.users(tx), user => - user.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter(await client.users(tx), user => + user.isAccessibleBy(realm, a, tx) + ), + args ) - : []; + : null; } } }) diff --git a/packages/authx/src/graphql/GraphQLClientConnection.ts b/packages/authx/src/graphql/GraphQLClientConnection.ts new file mode 100644 index 00000000..77f00179 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLClientConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLClientEdge } from "./GraphQLClientEdge"; + +export const GraphQLClientConnection = new GraphQLObjectType({ + name: "ClientConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLClientEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLClientEdge.ts b/packages/authx/src/graphql/GraphQLClientEdge.ts new file mode 100644 index 00000000..2a4ab3c8 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLClientEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLClient } from "./GraphQLClient"; + +export const GraphQLClientEdge = new GraphQLObjectType({ + name: "ClientEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLClient } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLCredentialConnection.ts b/packages/authx/src/graphql/GraphQLCredentialConnection.ts new file mode 100644 index 00000000..1e11b041 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLCredentialConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLCredentialEdge } from "./GraphQLCredentialEdge"; + +export const GraphQLCredentialConnection = new GraphQLObjectType({ + name: "CredentialConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLCredentialEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLCredentialEdge.ts b/packages/authx/src/graphql/GraphQLCredentialEdge.ts new file mode 100644 index 00000000..abfc052a --- /dev/null +++ b/packages/authx/src/graphql/GraphQLCredentialEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLCredential } from "./GraphQLCredential"; + +export const GraphQLCredentialEdge = new GraphQLObjectType({ + name: "CredentialEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLCredential } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLGrant.ts b/packages/authx/src/graphql/GraphQLGrant.ts index 147a8483..55695af2 100644 --- a/packages/authx/src/graphql/GraphQLGrant.ts +++ b/packages/authx/src/graphql/GraphQLGrant.ts @@ -7,11 +7,17 @@ import { GraphQLObjectType } from "graphql"; +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { Grant, Client, User } from "../model"; import { Context } from "../Context"; import { GraphQLClient } from "./GraphQLClient"; import { GraphQLUser } from "./GraphQLUser"; -import { GraphQLAuthorization } from "./GraphQLAuthorization"; +import { GraphQLAuthorizationConnection } from "./GraphQLAuthorizationConnection"; import { filter } from "../util/filter"; export const GraphQLGrant: GraphQLObjectType< @@ -86,13 +92,21 @@ export const GraphQLGrant: GraphQLObjectType< } }, authorizations: { - type: new GraphQLList(GraphQLAuthorization), - async resolve(grant, args, { realm, authorization: a, tx }: Context) { + type: GraphQLAuthorizationConnection, + args: connectionArgs, + async resolve( + grant, + args: ConnectionArguments, + { realm, authorization: a, tx }: Context + ) { return a - ? filter(await grant.authorizations(tx), authorization => - authorization.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter(await grant.authorizations(tx), authorization => + authorization.isAccessibleBy(realm, a, tx) + ), + args ) - : []; + : null; } } }) diff --git a/packages/authx/src/graphql/GraphQLGrantConnection.ts b/packages/authx/src/graphql/GraphQLGrantConnection.ts new file mode 100644 index 00000000..98d56825 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLGrantConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLGrantEdge } from "./GraphQLGrantEdge"; + +export const GraphQLGrantConnection = new GraphQLObjectType({ + name: "GrantConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLGrantEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLGrantEdge.ts b/packages/authx/src/graphql/GraphQLGrantEdge.ts new file mode 100644 index 00000000..a3312e96 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLGrantEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLGrant } from "./GraphQLGrant"; + +export const GraphQLGrantEdge = new GraphQLObjectType({ + name: "GrantEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLGrant } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLPageInfo.ts b/packages/authx/src/graphql/GraphQLPageInfo.ts new file mode 100644 index 00000000..1d6641b2 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLPageInfo.ts @@ -0,0 +1,13 @@ +import { GraphQLBoolean, GraphQLObjectType, GraphQLString } from "graphql"; + +export const GraphQLPageInfo = new GraphQLObjectType({ + name: "PageInfo", + description: + "See: https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo", + fields: () => ({ + hasPreviousPage: { type: GraphQLBoolean }, + hasNextPage: { type: GraphQLBoolean }, + startCursor: { type: GraphQLString }, + endCursor: { type: GraphQLString } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLRole.ts b/packages/authx/src/graphql/GraphQLRole.ts index 22ccecaf..2701f003 100644 --- a/packages/authx/src/graphql/GraphQLRole.ts +++ b/packages/authx/src/graphql/GraphQLRole.ts @@ -7,9 +7,15 @@ import { GraphQLObjectType } from "graphql"; +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { Role } from "../model"; import { Context } from "../Context"; -import { GraphQLUser } from "./GraphQLUser"; +import { GraphQLUserConnection } from "./GraphQLUserConnection"; import { filter } from "../util/filter"; export const GraphQLRole = new GraphQLObjectType({ @@ -23,14 +29,22 @@ export const GraphQLRole = new GraphQLObjectType({ name: { type: GraphQLString }, description: { type: GraphQLString }, users: { - type: new GraphQLList(GraphQLUser), - async resolve(role, args, { realm, authorization: a, tx }: Context) { + type: GraphQLUserConnection, + args: connectionArgs, + async resolve( + role, + args: ConnectionArguments, + { realm, authorization: a, tx }: Context + ) { return a && (await role.isAccessibleBy(realm, a, tx, "read.assignments")) - ? filter(await role.users(tx), user => - user.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter(await role.users(tx), user => + user.isAccessibleBy(realm, a, tx) + ), + args ) - : []; + : null; } }, scopes: { diff --git a/packages/authx/src/graphql/GraphQLRoleConnection.ts b/packages/authx/src/graphql/GraphQLRoleConnection.ts new file mode 100644 index 00000000..d7971182 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLRoleConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLRoleEdge } from "./GraphQLRoleEdge"; + +export const GraphQLRoleConnection = new GraphQLObjectType({ + name: "RoleConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLRoleEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLRoleEdge.ts b/packages/authx/src/graphql/GraphQLRoleEdge.ts new file mode 100644 index 00000000..93131881 --- /dev/null +++ b/packages/authx/src/graphql/GraphQLRoleEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLRole } from "./GraphQLRole"; + +export const GraphQLRoleEdge = new GraphQLObjectType({ + name: "RoleEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLRole } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLUser.ts b/packages/authx/src/graphql/GraphQLUser.ts index 22c51701..79e79a25 100644 --- a/packages/authx/src/graphql/GraphQLUser.ts +++ b/packages/authx/src/graphql/GraphQLUser.ts @@ -1,20 +1,26 @@ import { GraphQLID, - GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLBoolean, GraphQLString } from "graphql"; +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { User, Grant } from "../model"; import { Context } from "../Context"; -import { GraphQLAuthorization } from "./GraphQLAuthorization"; -import { GraphQLCredential } from "./GraphQLCredential"; -import { GraphQLRole } from "./GraphQLRole"; +import { GraphQLRoleConnection } from "./GraphQLRoleConnection"; import { GraphQLUserType } from "./GraphQLUserType"; import { GraphQLGrant } from "./GraphQLGrant"; -import { GraphQLClient } from "./GraphQLClient"; +import { GraphQLGrantConnection } from "./GraphQLGrantConnection"; +import { GraphQLClientConnection } from "./GraphQLClientConnection"; +import { GraphQLAuthorizationConnection } from "./GraphQLAuthorizationConnection"; +import { GraphQLCredentialConnection } from "./GraphQLCredentialConnection"; import { filter } from "../util/filter"; export const GraphQLUser: GraphQLObjectType< @@ -33,42 +39,55 @@ export const GraphQLUser: GraphQLObjectType< }, type: { type: GraphQLUserType }, authorizations: { - type: new GraphQLList(GraphQLAuthorization), + type: GraphQLAuthorizationConnection, description: "List all of the user's authorizations.", - async resolve(user, args, { realm, authorization: a, tx }: Context) { + args: connectionArgs, + async resolve( + user, + args: ConnectionArguments, + { realm, authorization: a, tx }: Context + ) { return a - ? filter(await user.authorizations(tx), authorization => - authorization.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter(await user.authorizations(tx), authorization => + authorization.isAccessibleBy(realm, a, tx) + ), + args ) : null; } }, credentials: { - type: new GraphQLList(GraphQLCredential), + type: GraphQLCredentialConnection, description: "List all of the user's credentials.", + args: connectionArgs, async resolve( user, - args, + args: ConnectionArguments, { realm, authorization: a, tx, strategies: { credentialMap } }: Context ) { return a - ? filter(await user.credentials(tx, credentialMap), credential => - credential.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter( + await user.credentials(tx, credentialMap), + credential => credential.isAccessibleBy(realm, a, tx) + ), + args ) : null; } }, grants: { - type: new GraphQLList(GraphQLGrant), + type: GraphQLGrantConnection, description: "List all of the user's grants.", - async resolve( - user, - args, - { realm, authorization: a, tx }: Context - ): Promise { + args: connectionArgs, + async resolve(user, args, { realm, authorization: a, tx }: Context) { return a - ? filter(await user.grants(tx), grant => - grant.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter(await user.grants(tx), grant => + grant.isAccessibleBy(realm, a, tx) + ), + args ) : null; } @@ -93,26 +112,34 @@ export const GraphQLUser: GraphQLObjectType< } }, roles: { - type: new GraphQLList(GraphQLRole), + type: GraphQLRoleConnection, description: "List all roles to which the user is assigned.", + args: connectionArgs, async resolve(user, args, { realm, authorization: a, tx }: Context) { return a - ? filter( - await user.roles(tx), - async role => - (await role.isAccessibleBy(realm, a, tx)) && - (await role.isAccessibleBy(realm, a, tx, "read.assignments")) + ? connectionFromArray( + await filter( + await user.roles(tx), + async role => + (await role.isAccessibleBy(realm, a, tx)) && + (await role.isAccessibleBy(realm, a, tx, "read.assignments")) + ), + args ) : null; } }, clients: { - type: new GraphQLList(GraphQLClient), + type: GraphQLClientConnection, description: "List all roles to which the user is assigned.", + args: connectionArgs, async resolve(user, args, { realm, authorization: a, tx }: Context) { return a - ? filter(await user.clients(tx), client => - client.isAccessibleBy(realm, a, tx) + ? connectionFromArray( + await filter(await user.clients(tx), client => + client.isAccessibleBy(realm, a, tx) + ), + args ) : null; } diff --git a/packages/authx/src/graphql/GraphQLUserConnection.ts b/packages/authx/src/graphql/GraphQLUserConnection.ts new file mode 100644 index 00000000..c28c865b --- /dev/null +++ b/packages/authx/src/graphql/GraphQLUserConnection.ts @@ -0,0 +1,11 @@ +import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from "graphql"; +import { GraphQLPageInfo } from "./GraphQLPageInfo"; +import { GraphQLUserEdge } from "./GraphQLUserEdge"; + +export const GraphQLUserConnection = new GraphQLObjectType({ + name: "UserConnection", + fields: () => ({ + pageInfo: { type: new GraphQLNonNull(GraphQLPageInfo) }, + edges: { type: new GraphQLList(GraphQLUserEdge) } + }) +}); diff --git a/packages/authx/src/graphql/GraphQLUserEdge.ts b/packages/authx/src/graphql/GraphQLUserEdge.ts new file mode 100644 index 00000000..a62c9b3b --- /dev/null +++ b/packages/authx/src/graphql/GraphQLUserEdge.ts @@ -0,0 +1,10 @@ +import { GraphQLObjectType, GraphQLString } from "graphql"; +import { GraphQLUser } from "./GraphQLUser"; + +export const GraphQLUserEdge = new GraphQLObjectType({ + name: "UserEdge", + fields: () => ({ + cursor: { type: GraphQLString }, + node: { type: GraphQLUser } + }) +}); diff --git a/packages/authx/src/graphql/query/authorities.ts b/packages/authx/src/graphql/query/authorities.ts index 42a10aa1..13223e75 100644 --- a/packages/authx/src/graphql/query/authorities.ts +++ b/packages/authx/src/graphql/query/authorities.ts @@ -1,41 +1,32 @@ -import { - GraphQLBoolean, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLFieldConfig -} from "graphql"; -import { GraphQLAuthority } from "../GraphQLAuthority"; +import { GraphQLBoolean, GraphQLFieldConfig } from "graphql"; +import { GraphQLAuthorityConnection } from "../GraphQLAuthorityConnection"; import { Context } from "../../Context"; import { Authority } from "../../model"; +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + export const authorities: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { - type: new GraphQLList(new GraphQLNonNull(GraphQLAuthority)), + type: GraphQLAuthorityConnection, description: "List all authorities.", args: { + ...connectionArgs, includeDisabled: { type: GraphQLBoolean, defaultValue: false, description: "Include disabled authorities in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." } }, - async resolve(source, args, context): Promise[]> { + async resolve(source, args, context) { const { tx, strategies: { authorityMap } @@ -55,6 +46,9 @@ export const authorities: GraphQLFieldConfig< return []; } - return Authority.read(tx, ids.rows.map(({ id }) => id), authorityMap); + return connectionFromArray( + await Authority.read(tx, ids.rows.map(({ id }) => id), authorityMap), + args + ); } }; diff --git a/packages/authx/src/graphql/query/authorizations.ts b/packages/authx/src/graphql/query/authorizations.ts index c421b496..c93213ea 100644 --- a/packages/authx/src/graphql/query/authorizations.ts +++ b/packages/authx/src/graphql/query/authorizations.ts @@ -5,6 +5,13 @@ import { GraphQLNonNull, GraphQLFieldConfig } from "graphql"; + +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { GraphQLAuthorization } from "../GraphQLAuthorization"; import { Context } from "../../Context"; import { Authorization } from "../../model"; @@ -12,31 +19,22 @@ import { filter } from "../../util/filter"; export const authorizations: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { type: new GraphQLList(new GraphQLNonNull(GraphQLAuthorization)), description: "List all authorizations.", args: { + ...connectionArgs, includeDisabled: { type: GraphQLBoolean, defaultValue: false, - description: "Include disabled authorizations in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." + description: "Include disabled authorities in results." } }, - async resolve(source, args, context): Promise { + async resolve(source, args, context) { const { tx, authorization: a, realm } = context; if (!a) return []; @@ -58,8 +56,12 @@ export const authorizations: GraphQLFieldConfig< tx, ids.rows.map(({ id }) => id) ); - return filter(authorizations, authorization => - authorization.isAccessibleBy(realm, a, tx) + + return connectionFromArray( + await filter(authorizations, authorization => + authorization.isAccessibleBy(realm, a, tx) + ), + args ); } }; diff --git a/packages/authx/src/graphql/query/clients.ts b/packages/authx/src/graphql/query/clients.ts index d27f30e2..f4799ad3 100644 --- a/packages/authx/src/graphql/query/clients.ts +++ b/packages/authx/src/graphql/query/clients.ts @@ -1,10 +1,16 @@ import { GraphQLBoolean, - GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLFieldConfig } from "graphql"; + +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { GraphQLClient } from "../GraphQLClient"; import { Context } from "../../Context"; import { Client } from "../../model"; @@ -12,31 +18,22 @@ import { filter } from "../../util/filter"; export const clients: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { type: new GraphQLList(new GraphQLNonNull(GraphQLClient)), description: "List all clients.", args: { + ...connectionArgs, includeDisabled: { type: GraphQLBoolean, defaultValue: false, description: "Include disabled clients in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." } }, - async resolve(source, args, context): Promise { + async resolve(source, args, context) { const { tx, authorization: a, realm } = context; if (!a) return []; @@ -55,6 +52,10 @@ export const clients: GraphQLFieldConfig< } const clients = await Client.read(tx, ids.rows.map(({ id }) => id)); - return filter(clients, client => client.isAccessibleBy(realm, a, tx)); + + return connectionFromArray( + await filter(clients, client => client.isAccessibleBy(realm, a, tx)), + args + ); } }; diff --git a/packages/authx/src/graphql/query/credentials.ts b/packages/authx/src/graphql/query/credentials.ts index e9b71512..8731ae75 100644 --- a/packages/authx/src/graphql/query/credentials.ts +++ b/packages/authx/src/graphql/query/credentials.ts @@ -5,6 +5,13 @@ import { GraphQLNonNull, GraphQLFieldConfig } from "graphql"; + +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { GraphQLCredential } from "../GraphQLCredential"; import { Context } from "../../Context"; import { Credential } from "../../model"; @@ -12,31 +19,22 @@ import { filter } from "../../util/filter"; export const credentials: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { type: new GraphQLList(new GraphQLNonNull(GraphQLCredential)), description: "List all credentials.", + ...connectionArgs, args: { includeDisabled: { type: GraphQLBoolean, defaultValue: false, description: "Include disabled credentials in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." } }, - async resolve(source, args, context): Promise[]> { + async resolve(source, args, context) { const { tx, authorization: a, @@ -64,8 +62,12 @@ export const credentials: GraphQLFieldConfig< ids.rows.map(({ id }) => id), credentialMap ); - return filter(credentials, credential => - credential.isAccessibleBy(realm, a, tx) + + return connectionFromArray( + await filter(credentials, credential => + credential.isAccessibleBy(realm, a, tx) + ), + args ); } }; diff --git a/packages/authx/src/graphql/query/grants.ts b/packages/authx/src/graphql/query/grants.ts index fbb7006c..ed547e04 100644 --- a/packages/authx/src/graphql/query/grants.ts +++ b/packages/authx/src/graphql/query/grants.ts @@ -5,6 +5,13 @@ import { GraphQLNonNull, GraphQLFieldConfig } from "graphql"; + +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { GraphQLGrant } from "../GraphQLGrant"; import { Context } from "../../Context"; import { Grant } from "../../model"; @@ -12,31 +19,22 @@ import { filter } from "../../util/filter"; export const grants: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { type: new GraphQLList(new GraphQLNonNull(GraphQLGrant)), description: "List all grants.", args: { + ...connectionArgs, includeDisabled: { type: GraphQLBoolean, defaultValue: false, description: "Include disabled grants in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." } }, - async resolve(source, args, context): Promise { + async resolve(source, args, context) { const { tx, authorization: a, realm } = context; if (!a) return []; @@ -55,6 +53,10 @@ export const grants: GraphQLFieldConfig< } const grants = await Grant.read(tx, ids.rows.map(({ id }) => id)); - return filter(grants, grant => grant.isAccessibleBy(realm, a, tx)); + + return connectionFromArray( + await filter(grants, grant => grant.isAccessibleBy(realm, a, tx)), + args + ); } }; diff --git a/packages/authx/src/graphql/query/roles.ts b/packages/authx/src/graphql/query/roles.ts index 781d564a..27868acd 100644 --- a/packages/authx/src/graphql/query/roles.ts +++ b/packages/authx/src/graphql/query/roles.ts @@ -5,6 +5,13 @@ import { GraphQLNonNull, GraphQLFieldConfig } from "graphql"; + +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { GraphQLRole } from "../GraphQLRole"; import { Context } from "../../Context"; import { Role } from "../../model"; @@ -12,31 +19,22 @@ import { filter } from "../../util/filter"; export const roles: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { type: new GraphQLList(new GraphQLNonNull(GraphQLRole)), description: "List all roles.", args: { + ...connectionArgs, includeDisabled: { type: GraphQLBoolean, defaultValue: false, description: "Include disabled roles in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." } }, - async resolve(source, args, context): Promise { + async resolve(source, args, context) { const { tx, authorization: a, realm } = context; if (!a) return []; @@ -55,6 +53,10 @@ export const roles: GraphQLFieldConfig< } const roles = await Role.read(tx, ids.rows.map(({ id }) => id)); - return filter(roles, role => role.isAccessibleBy(realm, a, tx)); + + return connectionFromArray( + await filter(roles, role => role.isAccessibleBy(realm, a, tx)), + args + ); } }; diff --git a/packages/authx/src/graphql/query/users.ts b/packages/authx/src/graphql/query/users.ts index 931a9011..6e8e5fac 100644 --- a/packages/authx/src/graphql/query/users.ts +++ b/packages/authx/src/graphql/query/users.ts @@ -1,10 +1,16 @@ import { GraphQLBoolean, - GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLFieldConfig } from "graphql"; + +import { + connectionFromArray, + connectionArgs, + ConnectionArguments +} from "graphql-relay"; + import { GraphQLUser } from "../GraphQLUser"; import { Context } from "../../Context"; import { User } from "../../model"; @@ -12,31 +18,22 @@ import { filter } from "../../util/filter"; export const users: GraphQLFieldConfig< any, - { + ConnectionArguments & { includeDisabled: boolean; - offset: null | number; - limit: null | number; }, Context > = { type: new GraphQLList(new GraphQLNonNull(GraphQLUser)), description: "List all users.", args: { + ...connectionArgs, includeDisabled: { type: GraphQLBoolean, defaultValue: false, description: "Include disabled users in results." - }, - offset: { - type: GraphQLInt, - description: "The number of results to skip." - }, - limit: { - type: GraphQLInt, - description: "The maximum number of results to return." } }, - async resolve(source, args, context): Promise { + async resolve(source, args, context) { const { tx, authorization: a, realm } = context; if (!a) return []; @@ -55,6 +52,10 @@ export const users: GraphQLFieldConfig< } const users = await User.read(tx, ids.rows.map(({ id }) => id)); - return filter(users, user => user.isAccessibleBy(realm, a, tx)); + + return connectionFromArray( + await filter(users, user => user.isAccessibleBy(realm, a, tx)), + args + ); } }; diff --git a/src/viewer.test.ts b/src/viewer.test.ts index 7f6c9042..ae9ac152 100644 --- a/src/viewer.test.ts +++ b/src/viewer.test.ts @@ -107,155 +107,281 @@ test("Deep query on viewer.", async t => { "https://www.dundermifflin.com", "https://admin.dundermifflin.com" ], - users: [ - { - id: "51192909-3664-44d5-be62-c6b45f0b0ee6" + users: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "51192909-3664-44d5-be62-c6b45f0b0ee6" + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjA=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" } - ] + } }, secrets: [ "ZTQ2NzA3NjItYmViNy00MzVjLTk0YWYtMDU1Yjk1MWY5N2U2OjE1NTM5MjUzNDA6ZDQ5NDJjZGFhYTY1ZTg4YmQ2MWQ1MDIyZjlmN2E0ZGU=" ], codes: [], scopes: ["**:**:**"], - authorizations: [] + authorizations: { + edges: [], + pageInfo: { + endCursor: null, + hasNextPage: false, + hasPreviousPage: false, + startCursor: null + } + } }, user: { id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb", enabled: true, type: "HUMAN", name: "Michael Scott", - authorizations: [ - { - id: "c70da498-27ed-4c3b-a318-38bb220cef48" + authorizations: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "c70da498-27ed-4c3b-a318-38bb220cef48" + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjA=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" } - ], - credentials: [ - { - id: "540128ad-7a55-423e-a85c-103677df333c", - enabled: true, - user: { - id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb" - }, - authority: { - id: "0d765613-e813-40e5-9aa7-89f96531364e", - enabled: true, - name: "Email", - description: "The email authority", - privateKey: - "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCfb+nyTPFCntEXbrFPU5DeE0gC4jXRcSFWDfCRgeqeQWqIW9De\nMmCj13k0z6fQCiG3FATYosS64wAs+OiyGtu9q/JyUEVIBMF0upDJMA53AFFx+0Fb\n/i76JFPTY7SxzvioIFeKRwY8evIRWQWYO95Os6gKBac/x5qiUn5fh2xM+wIDAQAB\nAoGAeOPGo24r0LPTHs1TrC5Uvc4o3+bdn70D5dgT/IBhgTVgrZvQt2nDVPfgc2aw\ne1HzVnnbYteoC3xrea4R4lnzGpgcvLYyJ+LEAeRNT66u12EHnCjl8OM5Ods79RO2\npSaGBiAlntq9E86DBJ9ma9lL9NXiokCx4h1ph9rqr6T+DMECQQD7zM56evJj8JyV\nkyu7m3PGpepqgMtO4LjHlkU9ZP2HRfrq+bl4yWps1TyCTPzaRujXW+hHJBPsTYar\nTmsLcDepAkEAohi3FmYiAMhppgPMFqIr15tY04dKDw4kPgbaQLXT59v9e16alj+2\nhsBvMWA/juLuk/2JRuNutY0WBmtkkS42AwJBAKEjS++txniWfl5qNE53CPxTKVTG\n31S3EwkG7YCApI5xBkZhUYQuwWCshXCNfDLjthY7xsXgHK/YXRo7sN09DyECQD2W\n0HIFSmQrweCfTrtG0Qux7dUpcV05DVI3/lNaAvL05mIqtufhu3OFyHnlTSD4XpgC\nXFd/8L+wpK65vVNgUIsCQFO6/fma+fjXx9kG+/zy4C/VwJWFUcpo5Z3R2TF7FheW\n5N6OERXoA+Qu+ew7xS6WrAp33dHncIyr9ekkvGc01FU=\n-----END RSA PRIVATE KEY-----", - publicKeys: [ - "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCfb+nyTPFCntEXbrFPU5DeE0gC\n4jXRcSFWDfCRgeqeQWqIW9DeMmCj13k0z6fQCiG3FATYosS64wAs+OiyGtu9q/Jy\nUEVIBMF0upDJMA53AFFx+0Fb/i76JFPTY7SxzvioIFeKRwY8evIRWQWYO95Os6gK\nBac/x5qiUn5fh2xM+wIDAQAB\n-----END PUBLIC KEY-----" - ], - proofValidityDuration: 3600, - authenticationEmailSubject: "Reset your password", - authenticationEmailText: - "Please visit the following link to authenticate with this email address:\n\n{{{url}}}", - authenticationEmailHtml: - 'Please follow this link to authenticate with this email address.', - verificationEmailSubject: "Verify your email", - verificationEmailText: - "Please visit the following link to prove your control of this email address:\n\n{{{url}}}", - verificationEmailHtml: - 'Please follow this link to prove your control of this email address.' - }, - email: "michael.scott@dundermifflin.com" - }, - { - id: "c1a8cc41-66d5-4aef-8b97-e5f97d2bc699", - enabled: true, - user: { - id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb" - }, - authority: { - id: "725f9c3b-4a72-4021-9066-c89e534df5be", - enabled: true, - name: "Password", - description: "The password authority.", - rounds: 4 + }, + credentials: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "540128ad-7a55-423e-a85c-103677df333c", + enabled: true, + user: { + id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb" + }, + authority: { + id: "0d765613-e813-40e5-9aa7-89f96531364e", + enabled: true, + name: "Email", + description: "The email authority", + privateKey: + "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCfb+nyTPFCntEXbrFPU5DeE0gC4jXRcSFWDfCRgeqeQWqIW9De\nMmCj13k0z6fQCiG3FATYosS64wAs+OiyGtu9q/JyUEVIBMF0upDJMA53AFFx+0Fb\n/i76JFPTY7SxzvioIFeKRwY8evIRWQWYO95Os6gKBac/x5qiUn5fh2xM+wIDAQAB\nAoGAeOPGo24r0LPTHs1TrC5Uvc4o3+bdn70D5dgT/IBhgTVgrZvQt2nDVPfgc2aw\ne1HzVnnbYteoC3xrea4R4lnzGpgcvLYyJ+LEAeRNT66u12EHnCjl8OM5Ods79RO2\npSaGBiAlntq9E86DBJ9ma9lL9NXiokCx4h1ph9rqr6T+DMECQQD7zM56evJj8JyV\nkyu7m3PGpepqgMtO4LjHlkU9ZP2HRfrq+bl4yWps1TyCTPzaRujXW+hHJBPsTYar\nTmsLcDepAkEAohi3FmYiAMhppgPMFqIr15tY04dKDw4kPgbaQLXT59v9e16alj+2\nhsBvMWA/juLuk/2JRuNutY0WBmtkkS42AwJBAKEjS++txniWfl5qNE53CPxTKVTG\n31S3EwkG7YCApI5xBkZhUYQuwWCshXCNfDLjthY7xsXgHK/YXRo7sN09DyECQD2W\n0HIFSmQrweCfTrtG0Qux7dUpcV05DVI3/lNaAvL05mIqtufhu3OFyHnlTSD4XpgC\nXFd/8L+wpK65vVNgUIsCQFO6/fma+fjXx9kG+/zy4C/VwJWFUcpo5Z3R2TF7FheW\n5N6OERXoA+Qu+ew7xS6WrAp33dHncIyr9ekkvGc01FU=\n-----END RSA PRIVATE KEY-----", + publicKeys: [ + "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCfb+nyTPFCntEXbrFPU5DeE0gC\n4jXRcSFWDfCRgeqeQWqIW9DeMmCj13k0z6fQCiG3FATYosS64wAs+OiyGtu9q/Jy\nUEVIBMF0upDJMA53AFFx+0Fb/i76JFPTY7SxzvioIFeKRwY8evIRWQWYO95Os6gK\nBac/x5qiUn5fh2xM+wIDAQAB\n-----END PUBLIC KEY-----" + ], + proofValidityDuration: 3600, + authenticationEmailSubject: "Reset your password", + authenticationEmailText: + "Please visit the following link to authenticate with this email address:\n\n{{{url}}}", + authenticationEmailHtml: + 'Please follow this link to authenticate with this email address.', + verificationEmailSubject: "Verify your email", + verificationEmailText: + "Please visit the following link to prove your control of this email address:\n\n{{{url}}}", + verificationEmailHtml: + 'Please follow this link to prove your control of this email address.' + }, + email: "michael.scott@dundermifflin.com" + } }, - hash: - "$2a$04$j.W.ev.hBuIZZEKRZRpcPOmHz6SjaYtg/cO8vnBlq3lHHnFh2B1N2" + { + cursor: "YXJyYXljb25uZWN0aW9uOjE=", + node: { + id: "c1a8cc41-66d5-4aef-8b97-e5f97d2bc699", + enabled: true, + user: { + id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb" + }, + authority: { + id: "725f9c3b-4a72-4021-9066-c89e534df5be", + enabled: true, + name: "Password", + description: "The password authority.", + rounds: 4 + }, + hash: + "$2a$04$j.W.ev.hBuIZZEKRZRpcPOmHz6SjaYtg/cO8vnBlq3lHHnFh2B1N2" + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjE=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" } - ], - grants: [ - { - id: "e4670762-beb7-435c-94af-055b951f97e6" + }, + grants: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "e4670762-beb7-435c-94af-055b951f97e6" + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjA=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" } - ], + }, grant: { id: "e4670762-beb7-435c-94af-055b951f97e6" }, - roles: [ - { - id: "e833c8b8-acf1-42a1-9809-2bedab7d58c7", - enabled: true, - name: "Default User", - description: - "This role provides the basic abilities needed for a human user.", - users: [ - { - id: "0cbd3783-0424-4f35-be51-b42f07a2a987", - enabled: true - }, - { - id: "1691f38d-92c8-4d86-9a89-da99528cfcb5", - enabled: true - }, - { - id: "306eabbb-cc2b-4f88-be19-4bb6ec98e5c3", - enabled: true - }, - { - id: "51192909-3664-44d5-be62-c6b45f0b0ee6", - enabled: true - }, - { - id: "9ad4b34b-781d-44fe-ac39-9b7ac43dde21", - enabled: false - }, - { - id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb", - enabled: true - }, - { - id: "d0fc4c64-a3d6-4d97-9341-07de24439bb1", - enabled: true - }, - { - id: "dc396449-2c7d-4a23-a159-e6415ded71d2", - enabled: false - }, - { - id: "eaa9fa5e-088a-4ae2-a6ab-f120006b20a9", - enabled: true + roles: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "e833c8b8-acf1-42a1-9809-2bedab7d58c7", + enabled: true, + name: "Default User", + description: + "This role provides the basic abilities needed for a human user.", + users: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "0cbd3783-0424-4f35-be51-b42f07a2a987", + enabled: true + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjE=", + node: { + id: "1691f38d-92c8-4d86-9a89-da99528cfcb5", + enabled: true + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjI=", + node: { + id: "306eabbb-cc2b-4f88-be19-4bb6ec98e5c3", + enabled: true + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjM=", + node: { + id: "51192909-3664-44d5-be62-c6b45f0b0ee6", + enabled: true + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjQ=", + node: { + id: "9ad4b34b-781d-44fe-ac39-9b7ac43dde21", + enabled: false + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjU=", + node: { + id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb", + enabled: true + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjY=", + node: { + id: "d0fc4c64-a3d6-4d97-9341-07de24439bb1", + enabled: true + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjc=", + node: { + id: "dc396449-2c7d-4a23-a159-e6415ded71d2", + enabled: false + } + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjg=", + node: { + id: "eaa9fa5e-088a-4ae2-a6ab-f120006b20a9", + enabled: true + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjg=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" + } + }, + scopes: [ + "authx:authorization.equal.self.*:**", + "authx:grant.equal.self.*:**", + "authx:user.equal.self:**" + ] } - ], - scopes: [ - "authx:authorization.equal.self.*:**", - "authx:grant.equal.self.*:**", - "authx:user.equal.self:**" - ] - }, - { - id: "ee37605c-5834-40c9-bd80-bac16d9e62a4", - enabled: true, - name: "AuthX Administrator", - description: "This role provides full access to authx.", - users: [ - { - id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb", - enabled: true + }, + { + cursor: "YXJyYXljb25uZWN0aW9uOjE=", + node: { + id: "ee37605c-5834-40c9-bd80-bac16d9e62a4", + enabled: true, + name: "AuthX Administrator", + description: "This role provides full access to authx.", + users: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "a6a0946d-eeb4-45cd-83c6-c7920f2272eb", + enabled: true + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjA=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" + } + }, + scopes: ["authx:**:**"] } - ], - scopes: ["authx:**:**"] + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjE=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" } - ], - clients: [ - { - id: "702d2103-a1b3-4873-b36b-dc8823fe95d1" + }, + clients: { + edges: [ + { + cursor: "YXJyYXljb25uZWN0aW9uOjA=", + node: { + id: "702d2103-a1b3-4873-b36b-dc8823fe95d1" + } + } + ], + pageInfo: { + endCursor: "YXJyYXljb25uZWN0aW9uOjA=", + hasNextPage: false, + hasPreviousPage: false, + startCursor: "YXJyYXljb25uZWN0aW9uOjA=" } - ] + } }, secret: "8f57395ecd9d6fcb884145f8f6feff357fead2fbd83607e87d71a7c372cf37ad", @@ -270,7 +396,7 @@ test("Deep query on viewer.", async t => { tx, authorization }, - document: parse(` + document: parse(/* GraphQL */ ` query { viewer { id @@ -289,14 +415,36 @@ test("Deep query on viewer.", async t => { secrets urls users { - id + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + } + } } } secrets codes scopes authorizations { - id + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + } + } } } user { @@ -305,85 +453,151 @@ test("Deep query on viewer.", async t => { type name authorizations { - id + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + } + } } credentials { - id - enabled - user { - id + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage } - authority { - id - enabled - name - description + edges { + cursor + node { + id + enabled + user { + id + } + authority { + id + enabled + name + description - ... on EmailAuthority { - privateKey - publicKeys - proofValidityDuration - authenticationEmailSubject - authenticationEmailText - authenticationEmailHtml - verificationEmailSubject - verificationEmailText - verificationEmailHtml - } + ... on EmailAuthority { + privateKey + publicKeys + proofValidityDuration + authenticationEmailSubject + authenticationEmailText + authenticationEmailHtml + verificationEmailSubject + verificationEmailText + verificationEmailHtml + } - ... on PasswordAuthority { - rounds - } - } + ... on PasswordAuthority { + rounds + } + } - authority { - id - enabled - name - description - } + authority { + id + enabled + name + description + } - ... on EmailCredential { - email - authority { - privateKey - publicKeys - proofValidityDuration - authenticationEmailSubject - authenticationEmailText - authenticationEmailHtml - verificationEmailSubject - verificationEmailText - verificationEmailHtml - } - } + ... on EmailCredential { + email + authority { + privateKey + publicKeys + proofValidityDuration + authenticationEmailSubject + authenticationEmailText + authenticationEmailHtml + verificationEmailSubject + verificationEmailText + verificationEmailHtml + } + } - ... on PasswordCredential { - authority { - rounds + ... on PasswordCredential { + authority { + rounds + } + hash + } } - hash } } grants { - id + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + } + } } grant(clientId: "17436d83-6022-4101-bf9f-997f1550f57c") { id } roles { - id - enabled - name - description - users { - id - enabled + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + enabled + name + description + users { + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + enabled + } + } + } + scopes + } } - scopes } clients { - id + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + edges { + cursor + node { + id + } + } } } secret diff --git a/yarn.lock b/yarn.lock index d5d1a41d..fd9b2c4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -447,6 +447,13 @@ dependencies: "@types/react" "*" +"@types/graphql-relay@^0.4.9": + version "0.4.9" + resolved "https://registry.yarnpkg.com/@types/graphql-relay/-/graphql-relay-0.4.9.tgz#0f34ee87a1bf1fc4c986eeefe0707318c7433212" + integrity sha512-SL0UXsnNozTBIXwSs0pGB1iTWKaQzxmfqBwx9m0NvDGg463sYaFw4f7rizTzL4OSaVobLkvGJWUPesbg66MfHw== + dependencies: + "@types/graphql" "*" + "@types/graphql@*": version "14.2.0" resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.2.0.tgz#74e1da5f2a4a744ac6eb3ed57b48242ea9367202" @@ -2861,6 +2868,13 @@ graphql-react@^8.1.1: object-assign "^4.1.1" prop-types "^15.7.2" +graphql-relay@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/graphql-relay/-/graphql-relay-0.6.0.tgz#18ec36b772cfcb3dbb9bd369c3f8004cf42c7b93" + integrity sha512-OVDi6C9/qOT542Q3KxZdXja3NrDvqzbihn1B44PH8P/c5s0Q90RyQwT6guhGqXqbYEH6zbeLJWjQqiYvcg2vVw== + dependencies: + prettier "^1.16.0" + graphql@^14.3.1: version "14.3.1" resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.3.1.tgz#b3aa50e61a841ada3c1f9ccda101c483f8e8c807" @@ -4850,6 +4864,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@^1.16.0: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + prettier@^1.16.4: version "1.17.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db"