@subsquid/openreader 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/lib/context.d.ts +5 -2
  2. package/lib/context.d.ts.map +1 -1
  3. package/lib/limit.size.d.ts +7 -2
  4. package/lib/limit.size.d.ts.map +1 -1
  5. package/lib/limit.size.js +74 -11
  6. package/lib/limit.size.js.map +1 -1
  7. package/lib/main.js +7 -1
  8. package/lib/main.js.map +1 -1
  9. package/lib/model.d.ts +1 -0
  10. package/lib/model.d.ts.map +1 -1
  11. package/lib/model.schema.d.ts.map +1 -1
  12. package/lib/model.schema.js +21 -4
  13. package/lib/model.schema.js.map +1 -1
  14. package/lib/opencrud/schema.d.ts.map +1 -1
  15. package/lib/opencrud/schema.js +12 -7
  16. package/lib/opencrud/schema.js.map +1 -1
  17. package/lib/server.d.ts +16 -12
  18. package/lib/server.d.ts.map +1 -1
  19. package/lib/server.js +29 -4
  20. package/lib/server.js.map +1 -1
  21. package/lib/test/limits.test.d.ts +2 -0
  22. package/lib/test/limits.test.d.ts.map +1 -0
  23. package/lib/test/limits.test.js +159 -0
  24. package/lib/test/limits.test.js.map +1 -0
  25. package/lib/test/setup.d.ts +2 -1
  26. package/lib/test/setup.d.ts.map +1 -1
  27. package/lib/test/setup.js +5 -2
  28. package/lib/test/setup.js.map +1 -1
  29. package/lib/util/execute.d.ts +5 -0
  30. package/lib/util/execute.d.ts.map +1 -0
  31. package/lib/util/execute.js +28 -0
  32. package/lib/util/execute.js.map +1 -0
  33. package/lib/util/limit.d.ts +11 -0
  34. package/lib/util/limit.d.ts.map +1 -0
  35. package/lib/util/limit.js +39 -0
  36. package/lib/util/limit.js.map +1 -0
  37. package/package.json +2 -2
  38. package/src/context.ts +5 -2
  39. package/src/limit.size.ts +96 -12
  40. package/src/main.ts +19 -10
  41. package/src/model.schema.ts +24 -4
  42. package/src/model.ts +1 -0
  43. package/src/opencrud/schema.ts +14 -7
  44. package/src/server.ts +66 -26
  45. package/src/test/limits.test.ts +163 -0
  46. package/src/test/setup.ts +6 -3
  47. package/src/util/execute.ts +53 -0
  48. package/src/util/limit.ts +34 -0
package/src/main.ts CHANGED
@@ -1,12 +1,12 @@
1
- import {createLogger} from "@subsquid/logger"
2
- import {runProgram} from "@subsquid/util-internal"
3
- import {nat, Url} from "@subsquid/util-internal-commander"
4
- import {waitForInterruption} from "@subsquid/util-internal-http-server"
5
- import {Command, Option} from "commander"
6
- import {Pool} from "pg"
7
- import {Dialect} from "./dialect"
8
- import {serve} from "./server"
9
- import {loadModel} from "./tools"
1
+ import {createLogger} from '@subsquid/logger'
2
+ import {runProgram} from '@subsquid/util-internal'
3
+ import {nat, Url} from '@subsquid/util-internal-commander'
4
+ import {waitForInterruption} from '@subsquid/util-internal-http-server'
5
+ import {Command, Option} from 'commander'
6
+ import {Pool} from 'pg'
7
+ import {Dialect} from './dialect'
8
+ import {serve} from './server'
9
+ import {loadModel} from './tools'
10
10
 
11
11
 
12
12
  const LOG = createLogger('sqd:openreader')
@@ -26,10 +26,13 @@ GraphQL server for postgres-compatible databases
26
26
  )
27
27
  program.option('-p, --port <number>', 'port to listen on', nat, 3000)
28
28
  program.option('--max-request-size <kb>', 'max request size in kilobytes', nat, 256)
29
+ program.option('--max-root-fields <count>', 'max number of root fields in a query', nat)
30
+ program.option('--max-response-size <nodes>', 'max response size measured in nodes', nat)
29
31
  program.option('--sql-statement-timeout <ms>', 'sql statement timeout in ms', nat)
30
32
  program.option('--subscriptions', 'enable gql subscriptions')
31
33
  program.option('--subscription-poll-interval <ms>', 'subscription poll interval in ms', nat, 1000)
32
34
  program.option('--subscription-sql-statement-timeout <ms>', 'sql statement timeout for polling queries', nat)
35
+ program.option('--subscription-max-response-size <nodes>', 'max response size measured in nodes', nat)
33
36
 
34
37
  let opts = program.parse().opts() as {
35
38
  schema: string
@@ -37,10 +40,13 @@ GraphQL server for postgres-compatible databases
37
40
  dbType: Dialect
38
41
  port: number
39
42
  maxRequestSize: number
43
+ maxRootFields?: number
44
+ maxResponseSize?: number
40
45
  sqlStatementTimeout?: number
41
46
  subscriptions?: boolean
42
47
  subscriptionPollInterval: number
43
48
  subscriptionSqlStatementTimeout?: number
49
+ subscriptionMaxResponseSize?: number
44
50
  }
45
51
 
46
52
  let model = loadModel(opts.schema)
@@ -65,9 +71,12 @@ GraphQL server for postgres-compatible databases
65
71
  port: opts.port,
66
72
  log: LOG,
67
73
  maxRequestSizeBytes: opts.maxRequestSize * 1024,
74
+ maxRootFields: opts.maxRootFields,
75
+ maxResponseNodes: opts.maxResponseSize,
68
76
  subscriptions: opts.subscriptions,
69
77
  subscriptionPollInterval: opts.subscriptionPollInterval,
70
- subscriptionConnection
78
+ subscriptionConnection,
79
+ subscriptionMaxResponseNodes: opts.subscriptionMaxResponseSize
71
80
  })
72
81
 
73
82
  LOG.info(`listening on port ${server.port}`)
@@ -29,7 +29,7 @@ const baseSchema = buildASTSchema(parse(`
29
29
  directive @unique on FIELD_DEFINITION
30
30
  directive @index(fields: [String!] unique: Boolean) on OBJECT | FIELD_DEFINITION
31
31
  directive @fulltext(query: String!) on FIELD_DEFINITION
32
- directive @cardinality(value: Int!) on FIELD_DEFINITION
32
+ directive @cardinality(value: Int!) on OBJECT | FIELD_DEFINITION
33
33
  directive @byteWeight(value: Float!) on FIELD_DEFINITION
34
34
  directive @variant on OBJECT # legacy
35
35
  directive @jsonField on OBJECT # legacy
@@ -77,11 +77,12 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
77
77
  let properties: Record<string, Prop> = {}
78
78
  let interfaces: string[] = []
79
79
  let indexes: Index[] = type instanceof GraphQLObjectType ? checkEntityIndexes(type) : []
80
+ let cardinality = checkEntityCardinality(type)
80
81
  let description = type.description || undefined
81
82
 
82
83
  switch(kind) {
83
84
  case 'entity':
84
- model[type.name] = {kind, properties, description, interfaces, indexes}
85
+ model[type.name] = {kind, properties, description, interfaces, indexes, ...cardinality}
85
86
  break
86
87
  case 'object':
87
88
  model[type.name] = {kind, properties, description, interfaces}
@@ -467,10 +468,29 @@ function checkDerivedFrom(type: GraphQLNamedType, f: GraphQLField<any, any>): {f
467
468
  }
468
469
 
469
470
 
471
+ function checkEntityCardinality(type: GraphQLObjectType | GraphQLInterfaceType): {cardinality?: number} {
472
+ let directives = type.astNode?.directives?.filter(d => d.name.value == 'cardinality') || []
473
+ if (directives.length > 0 && !isEntityType(type)) {
474
+ throw new SchemaError(`@cardinality directive can be only applied to entities, but were applied to ${type.name}`)
475
+ }
476
+ if (directives.length > 1) throw new SchemaError(
477
+ `Multiple @cardinality directives where applied to ${type.name}`
478
+ )
479
+ if (directives.length == 0) return {}
480
+ let arg = assertNotNull(directives[0].arguments?.find(arg => arg.name.value == 'value'))
481
+ assert(arg.value.kind == 'IntValue')
482
+ let cardinality = parseInt(arg.value.value, 10)
483
+ if (cardinality < 0) throw new SchemaError(
484
+ `Incorrect @cardinality where applied to ${type.name}. Cardinality value must be positive.`
485
+ )
486
+ return {cardinality}
487
+ }
488
+
489
+
470
490
  function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {cardinality?: number} {
471
491
  let directives = f.astNode?.directives?.filter(d => d.name.value == 'cardinality') || []
472
492
  if (directives.length > 1) throw new SchemaError(
473
- `Multiple @cardinality where applied to ${type.name}.${f.name}`
493
+ `Multiple @cardinality directives where applied to ${type.name}.${f.name}`
474
494
  )
475
495
  if (directives.length == 0) return {}
476
496
  let arg = assertNotNull(directives[0].arguments?.find(arg => arg.name.value == 'value'))
@@ -486,7 +506,7 @@ function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<
486
506
  function checkByteWeightDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {byteWeight?: number} {
487
507
  let directives = f.astNode?.directives?.filter(d => d.name.value == 'byteWeight') || []
488
508
  if (directives.length > 1) throw new SchemaError(
489
- `Multiple @byteWeight where applied to ${type.name}.${f.name}`
509
+ `Multiple @byteWeight directives where applied to ${type.name}.${f.name}`
490
510
  )
491
511
  if (directives.length == 0) return {}
492
512
  let arg = assertNotNull(directives[0].arguments?.find(arg => arg.name.value == 'value'))
package/src/model.ts CHANGED
@@ -9,6 +9,7 @@ export interface Entity extends TypeMeta {
9
9
  properties: Record<Name, Prop>
10
10
  interfaces?: Name[]
11
11
  indexes?: Index[]
12
+ cardinality?: number
12
13
  }
13
14
 
14
15
 
@@ -29,11 +29,13 @@ import {
29
29
  } from "graphql/type/definition"
30
30
  import {Context} from "../context"
31
31
  import {decodeRelayConnectionCursor, RelayConnectionRequest} from "../ir/connection"
32
+ import {getEntityListSize, getRelaySize, getSize} from '../limit.size'
32
33
  import {Entity, Interface, JsonObject, Model, Prop} from "../model"
33
34
  import {getObject, getUnionProps} from "../model.tools"
34
35
  import {customScalars} from "../scalars"
35
36
  import {EntityByIdQuery, EntityConnectionQuery, EntityCountQuery, EntityListQuery, Query} from "../sql/query"
36
37
  import {Subscription} from "../subscription"
38
+ import {Limit} from '../util/limit'
37
39
  import {getResolveTree, getTreeRequest, hasTreeRequest, simplifyResolveTree} from "../util/resolve-tree"
38
40
  import {ensureArray, identity} from "../util/util"
39
41
  import {getOrderByMapping, parseOrderBy} from "./orderBy"
@@ -406,13 +408,14 @@ export class SchemaBuilder {
406
408
  private installEntityList(entityName: string, query: GqlFieldMap, subscription: GqlFieldMap): void {
407
409
  let model = this.model
408
410
  let queryName = toPlural(toCamelCase(entityName))
409
- let outputType = new GraphQLList(new GraphQLNonNull(this.get(entityName)))
411
+ let outputType = new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(this.get(entityName))))
410
412
  let argsType = this.entityListArguments(entityName)
411
413
 
412
- function createQuery(context: Context, info: GraphQLResolveInfo) {
414
+ function createQuery(context: Context, info: GraphQLResolveInfo, limit?: Limit) {
413
415
  let tree = getResolveTree(info)
414
416
  let fields = parseResolveTree(model, entityName, info.schema, tree)
415
417
  let args = parseEntityListArguments(model, entityName, tree.args)
418
+ limit?.check(() => getEntityListSize(model, entityName, fields, args.limit, args.where) + 1)
416
419
  return new EntityListQuery(
417
420
  model,
418
421
  context.openreader.dialect,
@@ -426,7 +429,7 @@ export class SchemaBuilder {
426
429
  type: outputType,
427
430
  args: argsType,
428
431
  resolve(source, args, context, info) {
429
- let q = createQuery(context, info)
432
+ let q = createQuery(context, info, context.openreader.responseSizeLimit)
430
433
  return context.openreader.executeQuery(q)
431
434
  }
432
435
  }
@@ -436,7 +439,7 @@ export class SchemaBuilder {
436
439
  args: argsType,
437
440
  resolve: identity,
438
441
  subscribe(source, args, context, info) {
439
- let q = createQuery(context, info)
442
+ let q = createQuery(context, info, context.openreader.subscriptionResponseSizeLimit)
440
443
  return context.openreader.subscription(q)
441
444
  }
442
445
  }
@@ -449,9 +452,10 @@ export class SchemaBuilder {
449
452
  id: {type: new GraphQLNonNull(GraphQLString)}
450
453
  }
451
454
 
452
- function createQuery(context: Context, info: GraphQLResolveInfo) {
455
+ function createQuery(context: Context, info: GraphQLResolveInfo, limit?: Limit) {
453
456
  let tree = getResolveTree(info)
454
457
  let fields = parseResolveTree(model, entityName, info.schema, tree)
458
+ limit?.check(() => getSize(model, fields) + 1)
455
459
  return new EntityByIdQuery(
456
460
  model,
457
461
  context.openreader.dialect,
@@ -465,7 +469,7 @@ export class SchemaBuilder {
465
469
  type: this.get(entityName),
466
470
  args: argsType,
467
471
  async resolve(source, args, context, info) {
468
- let q = createQuery(context, info)
472
+ let q = createQuery(context, info, context.openreader.responseSizeLimit)
469
473
  return context.openreader.executeQuery(q)
470
474
  }
471
475
  }
@@ -475,7 +479,7 @@ export class SchemaBuilder {
475
479
  args: argsType,
476
480
  resolve: identity,
477
481
  subscribe(source, args, context, info) {
478
- let q = createQuery(context, info)
482
+ let q = createQuery(context, info, context.openreader.subscriptionResponseSizeLimit)
479
483
  return context.openreader.subscription(q)
480
484
  }
481
485
  }
@@ -493,6 +497,7 @@ export class SchemaBuilder {
493
497
  async resolve(source, args, context, info) {
494
498
  let tree = getResolveTree(info)
495
499
  let fields = parseResolveTree(model, entityName, info.schema, tree)
500
+ context.openreader.responseSizeLimit?.check(() => getSize(model, fields) + 1)
496
501
  let query = new EntityListQuery(
497
502
  model,
498
503
  context.openreader.dialect,
@@ -577,6 +582,8 @@ export class SchemaBuilder {
577
582
  }
578
583
  }
579
584
 
585
+ context.openreader.responseSizeLimit?.check(() => getRelaySize(model, entityName, req) + 1)
586
+
580
587
  let result = await context.openreader.executeQuery(new EntityConnectionQuery(
581
588
  model,
582
589
  context.openreader.dialect,
package/src/server.ts CHANGED
@@ -1,21 +1,23 @@
1
- import type {Logger} from "@subsquid/logger"
2
- import {listen, ListeningServer} from "@subsquid/util-internal-http-server"
3
- import {PluginDefinition} from "apollo-server-core"
4
- import {ApolloServer} from "apollo-server-express"
5
- import express from "express"
6
- import fs from "fs"
7
- import {GraphQLSchema} from "graphql"
8
- import {useServer as useWsServer} from "graphql-ws/lib/use/ws"
9
- import http from "http"
10
- import path from "path"
11
- import type {Pool} from "pg"
12
- import {WebSocketServer} from "ws"
13
- import {Context} from "./context"
14
- import {PoolOpenreaderContext} from "./db"
15
- import type {Dialect} from "./dialect"
16
- import type {Model} from "./model"
17
- import {SchemaBuilder} from "./opencrud/schema"
18
- import {logGraphQLError} from "./util/error-handling"
1
+ import type {Logger} from '@subsquid/logger'
2
+ import {listen, ListeningServer} from '@subsquid/util-internal-http-server'
3
+ import {PluginDefinition} from 'apollo-server-core'
4
+ import {ApolloServer} from 'apollo-server-express'
5
+ import express from 'express'
6
+ import fs from 'fs'
7
+ import {ExecutionArgs, GraphQLSchema} from 'graphql'
8
+ import {useServer as useWsServer} from 'graphql-ws/lib/use/ws'
9
+ import http from 'http'
10
+ import path from 'path'
11
+ import type {Pool} from 'pg'
12
+ import {WebSocketServer} from 'ws'
13
+ import {Context, OpenreaderContext} from './context'
14
+ import {PoolOpenreaderContext} from './db'
15
+ import type {Dialect} from './dialect'
16
+ import type {Model} from './model'
17
+ import {SchemaBuilder} from './opencrud/schema'
18
+ import {logGraphQLError} from './util/error-handling'
19
+ import {executeWithLimit} from './util/execute'
20
+ import {ResponseSizeLimit} from './util/limit'
19
21
 
20
22
 
21
23
  export interface ServerOptions {
@@ -23,25 +25,46 @@ export interface ServerOptions {
23
25
  model: Model
24
26
  connection: Pool
25
27
  dialect?: Dialect
26
- subscriptions?: boolean
27
- subscriptionPollInterval?: number
28
- subscriptionConnection?: Pool
29
28
  graphiqlConsole?: boolean
30
29
  log?: Logger
31
30
  maxRequestSizeBytes?: number
31
+ maxRootFields?: number
32
+ maxResponseNodes?: number
33
+ subscriptions?: boolean
34
+ subscriptionPollInterval?: number
35
+ subscriptionConnection?: Pool
36
+ subscriptionMaxResponseNodes?: number
32
37
  }
33
38
 
34
39
 
35
40
  export async function serve(options: ServerOptions): Promise<ListeningServer> {
36
- let {connection, subscriptionConnection, subscriptionPollInterval} = options
41
+ let {connection, subscriptionConnection, subscriptionPollInterval, maxResponseNodes, subscriptionMaxResponseNodes} = options
37
42
  let dialect = options.dialect ?? 'postgres'
38
43
 
39
44
  let schema = new SchemaBuilder(options).build()
45
+
40
46
  let context = () => {
47
+ let openreader: OpenreaderContext = new PoolOpenreaderContext(
48
+ dialect,
49
+ connection,
50
+ subscriptionConnection,
51
+ subscriptionPollInterval
52
+ )
53
+
54
+ if (maxResponseNodes) {
55
+ openreader.responseSizeLimit = new ResponseSizeLimit(maxResponseNodes)
56
+ openreader.subscriptionResponseSizeLimit = new ResponseSizeLimit(maxResponseNodes)
57
+ }
58
+
59
+ if (subscriptionMaxResponseNodes) {
60
+ openreader.subscriptionResponseSizeLimit = new ResponseSizeLimit(subscriptionMaxResponseNodes)
61
+ }
62
+
41
63
  return {
42
- openreader: new PoolOpenreaderContext(dialect, connection, subscriptionConnection, subscriptionPollInterval)
64
+ openreader
43
65
  }
44
66
  }
67
+
45
68
  let disposals: Dispose[] = []
46
69
 
47
70
  return addServerCleanup(disposals, runApollo({
@@ -52,7 +75,8 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
52
75
  subscriptions: options.subscriptions,
53
76
  log: options.log,
54
77
  graphiqlConsole: options.graphiqlConsole,
55
- maxRequestSizeBytes: options.maxRequestSizeBytes
78
+ maxRequestSizeBytes: options.maxRequestSizeBytes,
79
+ maxRootFields: options.maxRootFields
56
80
  }), options.log)
57
81
  }
58
82
 
@@ -70,15 +94,21 @@ export interface ApolloOptions {
70
94
  graphiqlConsole?: boolean
71
95
  log?: Logger
72
96
  maxRequestSizeBytes?: number
97
+ maxRootFields?: number
73
98
  }
74
99
 
75
100
 
76
101
  export async function runApollo(options: ApolloOptions): Promise<ListeningServer> {
77
- let {disposals, context, schema, log} = options
102
+ const {disposals, context, schema, log, maxRootFields} = options
103
+
78
104
  let maxRequestSizeBytes = options.maxRequestSizeBytes ?? 256 * 1024
79
105
  let app = express()
80
106
  let server = http.createServer(app)
81
107
 
108
+ const execute = maxRootFields
109
+ ? (args: ExecutionArgs) => executeWithLimit(maxRootFields, args)
110
+ : undefined
111
+
82
112
  if (options.subscriptions) {
83
113
  let wsServer = new WebSocketServer({
84
114
  server,
@@ -89,6 +119,7 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
89
119
  {
90
120
  schema,
91
121
  context,
122
+ execute,
92
123
  onError(ctx, message, errors) {
93
124
  if (log) {
94
125
  // FIXME: we don't want to log client errors
@@ -111,6 +142,16 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
111
142
  schema,
112
143
  context,
113
144
  stopOnTerminationSignals: false,
145
+ executor: execute && (async req => {
146
+ return execute({
147
+ schema,
148
+ document: req.document,
149
+ rootValue: {},
150
+ contextValue: req.context,
151
+ variableValues: req.request.variables,
152
+ operationName: req.operationName
153
+ })
154
+ }),
114
155
  plugins: [
115
156
  ...options.plugins || [],
116
157
  {
@@ -136,7 +177,6 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
136
177
  setupGraphiqlConsole(app)
137
178
  }
138
179
 
139
-
140
180
  await apollo.start()
141
181
  disposals.push(() => apollo.stop())
142
182
 
@@ -0,0 +1,163 @@
1
+ import expect from 'expect'
2
+ import {useDatabase, useServer} from './setup'
3
+
4
+
5
+ describe('response size limits', function() {
6
+ useDatabase([
7
+ `create table "order1" (id text primary key)`,
8
+ `create table item1 (id text primary key, order_id text, name text)`,
9
+ `create table "order2" (id text primary key)`,
10
+ `create table item2 (id text primary key, order_id text, name text)`,
11
+ `create table "order3" (id text primary key)`,
12
+ `create table item3 (id text primary key, order_id text, name text)`,
13
+ ])
14
+
15
+ const client = useServer(`
16
+ type Order1 @entity {
17
+ id: ID!
18
+ items: [Item1!]! @derivedFrom(field: "order")
19
+ }
20
+
21
+ type Item1 @entity {
22
+ id: ID!
23
+ order: Order1!
24
+ name: String
25
+ }
26
+
27
+ type Order2 @entity @cardinality(value: 10) {
28
+ id: ID!
29
+ items: [Item2!]! @derivedFrom(field: "order")
30
+ }
31
+
32
+ type Item2 @entity {
33
+ id: ID!
34
+ order: Order2!
35
+ name: String
36
+ }
37
+
38
+ type Order3 @entity {
39
+ id: ID!
40
+ items: [Item3!]! @derivedFrom(field: "order") @cardinality(value: 10)
41
+ }
42
+
43
+ type Item3 @entity {
44
+ id: ID!
45
+ order: Order3!
46
+ name: String @byteWeight(value: 10.0)
47
+ }
48
+ `, {
49
+ maxResponseNodes: 50,
50
+ maxRootFields: 3
51
+ })
52
+
53
+ it('unlimited requests fail', async function() {
54
+ let result = await client.query(`
55
+ query {
56
+ order1s {
57
+ id
58
+ }
59
+ }
60
+ `)
61
+ expect(result).toMatchObject({
62
+ data: null,
63
+ errors: [
64
+ expect.objectContaining({message: 'response might exceed the size limit', path: ['order1s']})
65
+ ]
66
+ })
67
+ })
68
+
69
+ it('limited requests work', function() {
70
+ return client.test(`
71
+ query {
72
+ order1s(limit: 10) {
73
+ items(limit: 2) {
74
+ id
75
+ }
76
+ }
77
+ }
78
+ `, {
79
+ order1s: []
80
+ })
81
+ })
82
+
83
+ it('entity level cardinalities are respected', function() {
84
+ return client.test(`
85
+ query {
86
+ order2s {
87
+ id
88
+ }
89
+ }
90
+ `, {
91
+ order2s: []
92
+ })
93
+ })
94
+
95
+ it('item cardinalities are respected', function() {
96
+ return client.test(`
97
+ query {
98
+ order3s(limit: 1) {
99
+ items { id }
100
+ }
101
+ }
102
+ `, {
103
+ order3s: []
104
+ })
105
+ })
106
+
107
+ it('@byteWeight annotations are respected', async function() {
108
+ let result = await client.query(`
109
+ query {
110
+ order3s(limit: 1) {
111
+ items(limit: 8) { name }
112
+ }
113
+ }
114
+ `)
115
+ expect(result).toEqual({
116
+ data: null,
117
+ errors: [
118
+ expect.objectContaining({
119
+ message: 'response might exceed the size limit',
120
+ path: ['order3s']
121
+ })
122
+ ]
123
+ })
124
+ await client.test(`
125
+ query {
126
+ order3s(limit: 1) {
127
+ items(limit: 4) { name }
128
+ }
129
+ }
130
+ `, {
131
+ order3s: []
132
+ })
133
+ })
134
+
135
+ it('id_in conditions are understood', function() {
136
+ return client.test(`
137
+ query {
138
+ order1s(where: {id_in: ["1", "2", "3"]}) {
139
+ id
140
+ }
141
+ }
142
+ `, {
143
+ order1s: []
144
+ })
145
+ })
146
+
147
+ it('root query fields limit', async function() {
148
+ return client.errorTest(`
149
+ query {
150
+ a: order1ById(id: "1") { id }
151
+ b: order1ById(id: "1") { id }
152
+ c: order1ById(id: "1") { id }
153
+ d: order1ById(id: "1") { id }
154
+ }
155
+ `, {
156
+ errors: [
157
+ expect.objectContaining({
158
+ message: 'only 3 root query fields allowed, but got 4'
159
+ })
160
+ ]
161
+ })
162
+ })
163
+ })
package/src/test/setup.ts CHANGED
@@ -4,7 +4,7 @@ import {Client} from "gql-test-client"
4
4
  import {parse} from "graphql"
5
5
  import {Client as PgClient, ClientBase, Pool} from "pg"
6
6
  import {buildModel, buildSchema} from "../model.schema"
7
- import {serve} from "../server"
7
+ import {serve, ServerOptions} from '../server'
8
8
 
9
9
 
10
10
  export function isCockroach(): boolean {
@@ -59,7 +59,7 @@ export function useDatabase(sql: string[]): void {
59
59
  }
60
60
 
61
61
 
62
- export function useServer(schema: string): Client {
62
+ export function useServer(schema: string, options?: Partial<ServerOptions>): Client {
63
63
  let client = new Client('not defined')
64
64
  let db = new Pool(db_config)
65
65
  let info: ListeningServer | undefined
@@ -69,7 +69,10 @@ export function useServer(schema: string): Client {
69
69
  model: buildModel(buildSchema(parse(schema))),
70
70
  port: 0,
71
71
  dialect: isCockroach() ? 'cockroach' : 'postgres',
72
- subscriptions: true
72
+ subscriptions: true,
73
+ subscriptionPollInterval: 500,
74
+ maxRootFields: 10,
75
+ ...options
73
76
  })
74
77
  client.endpoint = `http://localhost:${info.port}/graphql`
75
78
  })
@@ -0,0 +1,53 @@
1
+ import {getOperationRootType, GraphQLError} from 'graphql'
2
+ import {ExecutionResult} from 'graphql-ws'
3
+ import {
4
+ assertValidExecutionArguments,
5
+ buildExecutionContext,
6
+ collectFields,
7
+ execute as graphqlExecute,
8
+ ExecutionArgs,
9
+ ExecutionContext
10
+ } from 'graphql/execution/execute'
11
+ import {PromiseOrValue} from 'graphql/jsutils/PromiseOrValue'
12
+
13
+
14
+ export function executeWithLimit(maxQueries: number, args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
15
+ assertValidExecutionArguments(args.schema, args.document, args.variableValues)
16
+
17
+ let xtx = buildExecutionContext(
18
+ args.schema,
19
+ args.document,
20
+ args.rootValue,
21
+ args.contextValue,
22
+ args.variableValues,
23
+ args.operationName,
24
+ args.fieldResolver,
25
+ args.typeResolver
26
+ )
27
+
28
+ if (Array.isArray(xtx)) {
29
+ return {errors: xtx}
30
+ }
31
+
32
+ let etx = xtx as ExecutionContext
33
+ if (etx.operation.operation == 'query') {
34
+ let query = getOperationRootType(etx.schema, etx.operation)
35
+ let fields = collectFields(
36
+ etx,
37
+ query,
38
+ etx.operation.selectionSet,
39
+ Object.create(null),
40
+ Object.create(null)
41
+ )
42
+ let fieldsCount = Object.keys(fields).length
43
+ if (fieldsCount > maxQueries) {
44
+ return {
45
+ errors: [
46
+ new GraphQLError(`only ${maxQueries} root query fields allowed, but got ${fieldsCount}`)
47
+ ]
48
+ }
49
+ }
50
+ }
51
+
52
+ return graphqlExecute(args)
53
+ }
@@ -0,0 +1,34 @@
1
+ import assert from 'assert'
2
+ import {GraphQLError} from 'graphql'
3
+
4
+
5
+ export class Limit {
6
+ constructor(private error: Error, private value: number) {
7
+ assert(this.value > 0)
8
+ }
9
+
10
+ get left(): number {
11
+ return Math.max(this.value, 0)
12
+ }
13
+
14
+ check(cb: (left: number) => number): void {
15
+ if (this.value < 0) throw this.error
16
+ let left = this.value - cb(this.value)
17
+ if (left < 0) {
18
+ throw this.error
19
+ } else {
20
+ this.value = left
21
+ }
22
+ }
23
+ }
24
+
25
+
26
+ const SIZE_LIMIT = new GraphQLError('response might exceed the size limit')
27
+ SIZE_LIMIT.stack = undefined
28
+
29
+
30
+ export class ResponseSizeLimit extends Limit {
31
+ constructor(maxNodes: number) {
32
+ super(SIZE_LIMIT, maxNodes)
33
+ }
34
+ }