@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.
- package/lib/context.d.ts +5 -2
- package/lib/context.d.ts.map +1 -1
- package/lib/limit.size.d.ts +7 -2
- package/lib/limit.size.d.ts.map +1 -1
- package/lib/limit.size.js +74 -11
- package/lib/limit.size.js.map +1 -1
- package/lib/main.js +7 -1
- package/lib/main.js.map +1 -1
- package/lib/model.d.ts +1 -0
- package/lib/model.d.ts.map +1 -1
- package/lib/model.schema.d.ts.map +1 -1
- package/lib/model.schema.js +21 -4
- package/lib/model.schema.js.map +1 -1
- package/lib/opencrud/schema.d.ts.map +1 -1
- package/lib/opencrud/schema.js +12 -7
- package/lib/opencrud/schema.js.map +1 -1
- package/lib/server.d.ts +16 -12
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +29 -4
- package/lib/server.js.map +1 -1
- package/lib/test/limits.test.d.ts +2 -0
- package/lib/test/limits.test.d.ts.map +1 -0
- package/lib/test/limits.test.js +159 -0
- package/lib/test/limits.test.js.map +1 -0
- package/lib/test/setup.d.ts +2 -1
- package/lib/test/setup.d.ts.map +1 -1
- package/lib/test/setup.js +5 -2
- package/lib/test/setup.js.map +1 -1
- package/lib/util/execute.d.ts +5 -0
- package/lib/util/execute.d.ts.map +1 -0
- package/lib/util/execute.js +28 -0
- package/lib/util/execute.js.map +1 -0
- package/lib/util/limit.d.ts +11 -0
- package/lib/util/limit.d.ts.map +1 -0
- package/lib/util/limit.js +39 -0
- package/lib/util/limit.js.map +1 -0
- package/package.json +2 -2
- package/src/context.ts +5 -2
- package/src/limit.size.ts +96 -12
- package/src/main.ts +19 -10
- package/src/model.schema.ts +24 -4
- package/src/model.ts +1 -0
- package/src/opencrud/schema.ts +14 -7
- package/src/server.ts +66 -26
- package/src/test/limits.test.ts +163 -0
- package/src/test/setup.ts +6 -3
- package/src/util/execute.ts +53 -0
- package/src/util/limit.ts +34 -0
package/src/main.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {createLogger} from
|
|
2
|
-
import {runProgram} from
|
|
3
|
-
import {nat, Url} from
|
|
4
|
-
import {waitForInterruption} from
|
|
5
|
-
import {Command, Option} from
|
|
6
|
-
import {Pool} from
|
|
7
|
-
import {Dialect} from
|
|
8
|
-
import {serve} from
|
|
9
|
-
import {loadModel} from
|
|
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}`)
|
package/src/model.schema.ts
CHANGED
|
@@ -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
package/src/opencrud/schema.ts
CHANGED
|
@@ -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
|
|
2
|
-
import {listen, ListeningServer} from
|
|
3
|
-
import {PluginDefinition} from
|
|
4
|
-
import {ApolloServer} from
|
|
5
|
-
import express from
|
|
6
|
-
import fs from
|
|
7
|
-
import {GraphQLSchema} from
|
|
8
|
-
import {useServer as useWsServer} from
|
|
9
|
-
import http from
|
|
10
|
-
import path from
|
|
11
|
-
import type {Pool} from
|
|
12
|
-
import {WebSocketServer} from
|
|
13
|
-
import {Context} from
|
|
14
|
-
import {PoolOpenreaderContext} from
|
|
15
|
-
import type {Dialect} from
|
|
16
|
-
import type {Model} from
|
|
17
|
-
import {SchemaBuilder} from
|
|
18
|
-
import {logGraphQLError} from
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|