@subsquid/openreader 2.0.0 → 3.0.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 (100) hide show
  1. package/lib/context.d.ts +5 -2
  2. package/lib/context.d.ts.map +1 -1
  3. package/lib/ir/args.d.ts +1 -1
  4. package/lib/ir/args.d.ts.map +1 -1
  5. package/lib/ir/connection.d.ts +3 -4
  6. package/lib/ir/connection.d.ts.map +1 -1
  7. package/lib/ir/connection.js.map +1 -1
  8. package/lib/ir/fields.d.ts +6 -2
  9. package/lib/ir/fields.d.ts.map +1 -1
  10. package/lib/ir/fields.js +15 -0
  11. package/lib/ir/fields.js.map +1 -1
  12. package/lib/limit.size.d.ts +7 -2
  13. package/lib/limit.size.d.ts.map +1 -1
  14. package/lib/limit.size.js +106 -12
  15. package/lib/limit.size.js.map +1 -1
  16. package/lib/main.js +6 -9
  17. package/lib/main.js.map +1 -1
  18. package/lib/model.d.ts +3 -1
  19. package/lib/model.d.ts.map +1 -1
  20. package/lib/model.schema.d.ts +2 -2
  21. package/lib/model.schema.d.ts.map +1 -1
  22. package/lib/model.schema.js +29 -7
  23. package/lib/model.schema.js.map +1 -1
  24. package/lib/model.tools.d.ts +6 -1
  25. package/lib/model.tools.d.ts.map +1 -1
  26. package/lib/model.tools.js +111 -8
  27. package/lib/model.tools.js.map +1 -1
  28. package/lib/opencrud/orderBy.d.ts +2 -2
  29. package/lib/opencrud/orderBy.d.ts.map +1 -1
  30. package/lib/opencrud/orderBy.js +13 -17
  31. package/lib/opencrud/orderBy.js.map +1 -1
  32. package/lib/opencrud/schema.d.ts +4 -4
  33. package/lib/opencrud/schema.d.ts.map +1 -1
  34. package/lib/opencrud/schema.js +60 -64
  35. package/lib/opencrud/schema.js.map +1 -1
  36. package/lib/opencrud/tree.d.ts +9 -7
  37. package/lib/opencrud/tree.d.ts.map +1 -1
  38. package/lib/opencrud/tree.js +32 -14
  39. package/lib/opencrud/tree.js.map +1 -1
  40. package/lib/server.d.ts +16 -12
  41. package/lib/server.d.ts.map +1 -1
  42. package/lib/server.js +29 -4
  43. package/lib/server.js.map +1 -1
  44. package/lib/sql/cursor.js +2 -2
  45. package/lib/sql/cursor.js.map +1 -1
  46. package/lib/sql/mapping.d.ts +3 -1
  47. package/lib/sql/mapping.d.ts.map +1 -1
  48. package/lib/sql/mapping.js +16 -1
  49. package/lib/sql/mapping.js.map +1 -1
  50. package/lib/sql/printer.d.ts +29 -11
  51. package/lib/sql/printer.d.ts.map +1 -1
  52. package/lib/sql/printer.js +106 -10
  53. package/lib/sql/printer.js.map +1 -1
  54. package/lib/sql/query.d.ts +11 -11
  55. package/lib/sql/query.d.ts.map +1 -1
  56. package/lib/sql/query.js +41 -19
  57. package/lib/sql/query.js.map +1 -1
  58. package/lib/test/limits.test.d.ts +2 -0
  59. package/lib/test/limits.test.d.ts.map +1 -0
  60. package/lib/test/limits.test.js +159 -0
  61. package/lib/test/limits.test.js.map +1 -0
  62. package/lib/test/queryable.test.d.ts +2 -0
  63. package/lib/test/queryable.test.d.ts.map +1 -0
  64. package/lib/test/queryable.test.js +255 -0
  65. package/lib/test/queryable.test.js.map +1 -0
  66. package/lib/test/setup.d.ts +2 -1
  67. package/lib/test/setup.d.ts.map +1 -1
  68. package/lib/test/setup.js +5 -2
  69. package/lib/test/setup.js.map +1 -1
  70. package/lib/util/execute.d.ts +5 -0
  71. package/lib/util/execute.d.ts.map +1 -0
  72. package/lib/util/execute.js +28 -0
  73. package/lib/util/execute.js.map +1 -0
  74. package/lib/util/limit.d.ts +11 -0
  75. package/lib/util/limit.d.ts.map +1 -0
  76. package/lib/util/limit.js +39 -0
  77. package/lib/util/limit.js.map +1 -0
  78. package/package.json +3 -3
  79. package/src/context.ts +5 -2
  80. package/src/ir/args.ts +1 -1
  81. package/src/ir/connection.ts +3 -4
  82. package/src/ir/fields.ts +22 -2
  83. package/src/limit.size.ts +122 -13
  84. package/src/main.ts +18 -20
  85. package/src/model.schema.ts +40 -13
  86. package/src/model.tools.ts +121 -8
  87. package/src/model.ts +3 -1
  88. package/src/opencrud/orderBy.ts +13 -17
  89. package/src/opencrud/schema.ts +86 -85
  90. package/src/opencrud/tree.ts +55 -26
  91. package/src/server.ts +66 -26
  92. package/src/sql/cursor.ts +2 -2
  93. package/src/sql/mapping.ts +18 -1
  94. package/src/sql/printer.ts +137 -21
  95. package/src/sql/query.ts +50 -30
  96. package/src/test/limits.test.ts +163 -0
  97. package/src/test/queryable.test.ts +258 -0
  98. package/src/test/setup.ts +6 -3
  99. package/src/util/execute.ts +53 -0
  100. package/src/util/limit.ts +34 -0
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeWithLimit = void 0;
4
+ const graphql_1 = require("graphql");
5
+ const execute_1 = require("graphql/execution/execute");
6
+ function executeWithLimit(maxQueries, args) {
7
+ (0, execute_1.assertValidExecutionArguments)(args.schema, args.document, args.variableValues);
8
+ let xtx = (0, execute_1.buildExecutionContext)(args.schema, args.document, args.rootValue, args.contextValue, args.variableValues, args.operationName, args.fieldResolver, args.typeResolver);
9
+ if (Array.isArray(xtx)) {
10
+ return { errors: xtx };
11
+ }
12
+ let etx = xtx;
13
+ if (etx.operation.operation == 'query') {
14
+ let query = (0, graphql_1.getOperationRootType)(etx.schema, etx.operation);
15
+ let fields = (0, execute_1.collectFields)(etx, query, etx.operation.selectionSet, Object.create(null), Object.create(null));
16
+ let fieldsCount = Object.keys(fields).length;
17
+ if (fieldsCount > maxQueries) {
18
+ return {
19
+ errors: [
20
+ new graphql_1.GraphQLError(`only ${maxQueries} root query fields allowed, but got ${fieldsCount}`)
21
+ ]
22
+ };
23
+ }
24
+ }
25
+ return (0, execute_1.execute)(args);
26
+ }
27
+ exports.executeWithLimit = executeWithLimit;
28
+ //# sourceMappingURL=execute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/util/execute.ts"],"names":[],"mappings":";;;AAAA,qCAA0D;AAE1D,uDAOkC;AAIlC,SAAgB,gBAAgB,CAAC,UAAkB,EAAE,IAAmB;IACpE,IAAA,uCAA6B,EAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,CAAC,CAAA;IAE9E,IAAI,GAAG,GAAG,IAAA,+BAAqB,EAC3B,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,cAAc,EACnB,IAAI,CAAC,aAAa,EAClB,IAAI,CAAC,aAAa,EAClB,IAAI,CAAC,YAAY,CACpB,CAAA;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QACpB,OAAO,EAAC,MAAM,EAAE,GAAG,EAAC,CAAA;KACvB;IAED,IAAI,GAAG,GAAG,GAAuB,CAAA;IACjC,IAAI,GAAG,CAAC,SAAS,CAAC,SAAS,IAAI,OAAO,EAAE;QACpC,IAAI,KAAK,GAAG,IAAA,8BAAoB,EAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;QAC3D,IAAI,MAAM,GAAG,IAAA,uBAAa,EACtB,GAAG,EACH,KAAK,EACL,GAAG,CAAC,SAAS,CAAC,YAAY,EAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EACnB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CACtB,CAAA;QACD,IAAI,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAA;QAC5C,IAAI,WAAW,GAAG,UAAU,EAAE;YAC1B,OAAO;gBACH,MAAM,EAAE;oBACJ,IAAI,sBAAY,CAAC,QAAQ,UAAU,uCAAuC,WAAW,EAAE,CAAC;iBAC3F;aACJ,CAAA;SACJ;KACJ;IAED,OAAO,IAAA,iBAAc,EAAC,IAAI,CAAC,CAAA;AAC/B,CAAC;AAvCD,4CAuCC"}
@@ -0,0 +1,11 @@
1
+ export declare class Limit {
2
+ private error;
3
+ private value;
4
+ constructor(error: Error, value: number);
5
+ get left(): number;
6
+ check(cb: (left: number) => number): void;
7
+ }
8
+ export declare class ResponseSizeLimit extends Limit {
9
+ constructor(maxNodes: number);
10
+ }
11
+ //# sourceMappingURL=limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limit.d.ts","sourceRoot":"","sources":["../../src/util/limit.ts"],"names":[],"mappings":"AAIA,qBAAa,KAAK;IACF,OAAO,CAAC,KAAK;IAAS,OAAO,CAAC,KAAK;gBAA3B,KAAK,EAAE,KAAK,EAAU,KAAK,EAAE,MAAM;IAIvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI;CAS5C;AAOD,qBAAa,iBAAkB,SAAQ,KAAK;gBAC5B,QAAQ,EAAE,MAAM;CAG/B"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ResponseSizeLimit = exports.Limit = void 0;
7
+ const assert_1 = __importDefault(require("assert"));
8
+ const graphql_1 = require("graphql");
9
+ class Limit {
10
+ constructor(error, value) {
11
+ this.error = error;
12
+ this.value = value;
13
+ (0, assert_1.default)(this.value > 0);
14
+ }
15
+ get left() {
16
+ return Math.max(this.value, 0);
17
+ }
18
+ check(cb) {
19
+ if (this.value < 0)
20
+ throw this.error;
21
+ let left = this.value - cb(this.value);
22
+ if (left < 0) {
23
+ throw this.error;
24
+ }
25
+ else {
26
+ this.value = left;
27
+ }
28
+ }
29
+ }
30
+ exports.Limit = Limit;
31
+ const SIZE_LIMIT = new graphql_1.GraphQLError('response might exceed the size limit');
32
+ SIZE_LIMIT.stack = undefined;
33
+ class ResponseSizeLimit extends Limit {
34
+ constructor(maxNodes) {
35
+ super(SIZE_LIMIT, maxNodes);
36
+ }
37
+ }
38
+ exports.ResponseSizeLimit = ResponseSizeLimit;
39
+ //# sourceMappingURL=limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limit.js","sourceRoot":"","sources":["../../src/util/limit.ts"],"names":[],"mappings":";;;;;;AAAA,oDAA2B;AAC3B,qCAAoC;AAGpC,MAAa,KAAK;IACd,YAAoB,KAAY,EAAU,KAAa;QAAnC,UAAK,GAAL,KAAK,CAAO;QAAU,UAAK,GAAL,KAAK,CAAQ;QACnD,IAAA,gBAAM,EAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IAC1B,CAAC;IAED,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,EAA4B;QAC9B,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC;YAAE,MAAM,IAAI,CAAC,KAAK,CAAA;QACpC,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACtC,IAAI,IAAI,GAAG,CAAC,EAAE;YACV,MAAM,IAAI,CAAC,KAAK,CAAA;SACnB;aAAM;YACH,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;SACpB;IACL,CAAC;CACJ;AAlBD,sBAkBC;AAGD,MAAM,UAAU,GAAG,IAAI,sBAAY,CAAC,sCAAsC,CAAC,CAAA;AAC3E,UAAU,CAAC,KAAK,GAAG,SAAS,CAAA;AAG5B,MAAa,iBAAkB,SAAQ,KAAK;IACxC,YAAY,QAAgB;QACxB,KAAK,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;IAC/B,CAAC;CACJ;AAJD,8CAIC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@subsquid/openreader",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "GraphQL server for postgres-compatible databases",
5
5
  "keywords": [
6
6
  "graphql",
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@graphql-tools/merge": "^8",
25
- "@subsquid/graphiql-console": "^0.2.0",
25
+ "@subsquid/graphiql-console": "^0.3.0",
26
26
  "@subsquid/logger": "^0.1.0",
27
27
  "@subsquid/util-internal": "^0.0.1",
28
28
  "@subsquid/util-internal-commander": "^0.0.0",
@@ -54,7 +54,7 @@
54
54
  "typescript": "~4.7.4"
55
55
  },
56
56
  "scripts": {
57
- "build": "rm -rf dist && tsc",
57
+ "build": "rm -rf lib && tsc",
58
58
  "test": "make up && sleep 2 && make test test-cockroach && make down || (make down && exit 1)"
59
59
  },
60
60
  "readme": "# OpenReader\n\nGraphQL server for squid framework. Given [data schema](https://docs.subsquid.io/reference/openreader-schema) \nand compatible Postgres database it serves \"read part\" of [OpenCRUD spec](https://www.opencrud.org).\n\n## Usage\n\n```bash\nopenreader schema.graphql\n```\n\nDatabase connection and server port are configured via environment variables:\n\n```\nDB_NAME\nDB_USER\nDB_PASS\nDB_HOST\nDB_PORT\nGRAPHQL_SERVER_PORT\n```\n"
package/src/context.ts CHANGED
@@ -1,5 +1,6 @@
1
- import {Dialect} from "./dialect"
2
- import {Query} from "./sql/query"
1
+ import {Dialect} from './dialect'
2
+ import {Query} from './sql/query'
3
+ import {Limit} from './util/limit'
3
4
 
4
5
 
5
6
  export interface Context {
@@ -11,4 +12,6 @@ export interface OpenreaderContext {
11
12
  dialect: Dialect
12
13
  executeQuery<T>(query: Query<T>): Promise<T>
13
14
  subscription<T>(query: Query<T>): AsyncIterable<T>
15
+ responseSizeLimit?: Limit
16
+ subscriptionResponseSizeLimit?: Limit
14
17
  }
package/src/ir/args.ts CHANGED
@@ -1,4 +1,4 @@
1
- export interface EntityListArguments {
1
+ export interface SqlArguments {
2
2
  offset?: number
3
3
  limit?: number
4
4
  orderBy?: OrderBy
@@ -1,13 +1,12 @@
1
- import {OrderBy, Where} from "./args"
2
- import {FieldRequest} from "./fields"
1
+ import {OrderBy, Where} from './args'
3
2
 
4
3
 
5
- export interface RelayConnectionRequest {
4
+ export interface RelayConnectionRequest<R> {
6
5
  orderBy: OrderBy
7
6
  where?: Where
8
7
  first?: number
9
8
  after?: string
10
- edgeNode?: FieldRequest[]
9
+ edgeNode?: R
11
10
  edgeCursor?: boolean
12
11
  pageInfo?: boolean
13
12
  totalCount?: boolean
package/src/ir/fields.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import assert from 'assert'
1
2
  import {
2
3
  EnumPropType,
3
4
  FkPropType,
@@ -9,7 +10,7 @@ import {
9
10
  ScalarPropType,
10
11
  UnionPropType
11
12
  } from "../model"
12
- import {EntityListArguments} from "./args"
13
+ import {SqlArguments} from "./args"
13
14
 
14
15
 
15
16
  export type FieldRequest = EntityListRequest | ObjectRequest | OpaqueRequest
@@ -28,7 +29,7 @@ type Base<T> = T extends PropType ? {
28
29
 
29
30
  export type EntityListRequest = Base<ListLookupPropType> & {
30
31
  children: FieldRequest[]
31
- args?: EntityListArguments
32
+ args?: SqlArguments
32
33
  }
33
34
 
34
35
 
@@ -38,3 +39,22 @@ export type ObjectRequest = Base<FkPropType | LookupPropType | ObjectPropType |
38
39
 
39
40
 
40
41
  export type OpaqueRequest = Base<ScalarPropType | EnumPropType | ListPropType>
42
+
43
+
44
+
45
+ export type FieldsByEntity = Record<string, FieldRequest[]>
46
+
47
+
48
+ export type AnyFields = FieldRequest[] | FieldsByEntity
49
+
50
+
51
+ export function asEntityFields(fields: AnyFields): FieldRequest[] {
52
+ assert(Array.isArray(fields))
53
+ return fields
54
+ }
55
+
56
+
57
+ export function asQueryableFields(fields: AnyFields): FieldsByEntity {
58
+ assert(!Array.isArray(fields))
59
+ return fields
60
+ }
package/src/limit.size.ts CHANGED
@@ -1,11 +1,16 @@
1
- import {unexpectedCase} from "@subsquid/util-internal"
2
- import {FieldRequest} from "./ir/fields"
1
+ import {unexpectedCase} from '@subsquid/util-internal'
2
+ import assert from 'assert'
3
+ import {Where} from './ir/args'
4
+ import {RelayConnectionRequest} from './ir/connection'
5
+ import {AnyFields, FieldRequest} from './ir/fields'
6
+ import {Model} from './model'
7
+ import {getEntity, getQueryableEntities} from './model.tools'
3
8
 
4
9
 
5
- export function getSize(fields: FieldRequest[]): number {
10
+ export function getObjectSize(model: Model, fields: FieldRequest[]): number {
6
11
  let total = 0
7
12
  for (let req of fields) {
8
- let size = getFieldSize(req)
13
+ let size = getFieldSize(model, req)
9
14
  if (Number.isFinite(size)) {
10
15
  total += size * req.aliases.length
11
16
  } else {
@@ -16,7 +21,7 @@ export function getSize(fields: FieldRequest[]): number {
16
21
  }
17
22
 
18
23
 
19
- function getFieldSize(req: FieldRequest): number {
24
+ function getFieldSize(model: Model, req: FieldRequest): number {
20
25
  switch(req.kind) {
21
26
  case "scalar":
22
27
  case "list":
@@ -27,20 +32,124 @@ function getFieldSize(req: FieldRequest): number {
27
32
  case "fk":
28
33
  case "lookup":
29
34
  case "union":
30
- return getSize(req.children) + 1
31
- case "list-lookup": {
32
- let limit = Math.min(req.args?.limit ?? Infinity, req.prop.cardinality ?? Infinity)
33
- if (Number.isFinite(limit)) {
34
- return limit * Math.max(getSize(req.children), 1)
35
- } else {
36
- return Infinity
35
+ return getObjectSize(model, req.children) + 1
36
+ case "list-lookup":
37
+ return getListSize(
38
+ model,
39
+ req.type.entity,
40
+ req.children,
41
+ Math.min(req.args?.limit ?? Infinity, req.prop.cardinality ?? Infinity),
42
+ req.args?.where
43
+ ) + 1
44
+ default:
45
+ throw unexpectedCase()
46
+ }
47
+ }
48
+
49
+
50
+ export function getListSize(
51
+ model: Model,
52
+ typeName: string,
53
+ fields: AnyFields,
54
+ limit?: number,
55
+ where?: Where
56
+ ): number {
57
+ let cardinality = getCardinality(model, typeName, limit, where)
58
+ if (!Number.isFinite(cardinality)) return Infinity
59
+ let type = model[typeName]
60
+ switch(type.kind) {
61
+ case 'entity': {
62
+ assert(Array.isArray(fields))
63
+ return cardinality * Math.max(getObjectSize(model, fields), 1)
64
+ }
65
+ case 'interface': {
66
+ assert(!Array.isArray(fields))
67
+ let weight = 1
68
+ for (let entity of getQueryableEntities(model, typeName)) {
69
+ weight = Math.max(weight, getObjectSize(model, fields[entity] || []))
37
70
  }
71
+ return cardinality * weight
38
72
  }
39
73
  default:
40
- throw unexpectedCase()
74
+ throw unexpectedCase(type.kind)
41
75
  }
42
76
  }
43
77
 
44
78
 
79
+ function getCardinality(
80
+ model: Model,
81
+ typeName: string,
82
+ limit?: number,
83
+ where?: Where
84
+ ): number {
85
+ let type = model[typeName]
86
+ switch(type.kind) {
87
+ case 'entity':
88
+ return Math.min(type.cardinality ?? Infinity, limit ?? Infinity, getWhereCardinality(where))
89
+ case 'interface': {
90
+ let whereCardinality = getWhereCardinality(where)
91
+ let cardinality = 0
92
+ for (let entity of getQueryableEntities(model, typeName)) {
93
+ cardinality += Math.min(getEntity(model, entity).cardinality ?? Infinity, whereCardinality)
94
+ }
95
+ return Math.min(cardinality, limit ?? Infinity)
96
+ }
97
+ default:
98
+ throw unexpectedCase(type.kind)
99
+ }
100
+ }
101
+
102
+
103
+ function getWhereCardinality(where?: Where): number {
104
+ if (where == null) return Infinity
105
+ switch(where.op) {
106
+ case 'AND': {
107
+ let min = Infinity
108
+ for (let co of where.args) {
109
+ min = Math.min(min, getWhereCardinality(co))
110
+ }
111
+ return min
112
+ }
113
+ case 'OR': {
114
+ if (where.args.length == 0) return Infinity
115
+ let max = 0
116
+ for (let co of where.args) {
117
+ max = Math.max(max, getWhereCardinality(co))
118
+ }
119
+ return max
120
+ }
121
+ case 'eq':
122
+ if (where.field == 'id') {
123
+ return 1
124
+ } else {
125
+ return Infinity
126
+ }
127
+ case 'in':
128
+ if (where.field == 'id') {
129
+ return where.values.length
130
+ } else {
131
+ return Infinity
132
+ }
133
+ default:
134
+ return Infinity
135
+ }
136
+ }
45
137
 
46
138
 
139
+ export function getConnectionSize(model: Model, typeName: string, req: RelayConnectionRequest<AnyFields>): number {
140
+ let first = req.first ?? 100
141
+ let total = 0
142
+ if (req.edgeNode) {
143
+ total += getListSize(model, typeName, req.edgeNode, first, req.where)
144
+ }
145
+ if (req.edgeCursor) {
146
+ total += getCardinality(model, typeName, first, req.where)
147
+ }
148
+ if (req.pageInfo) {
149
+ total += 4
150
+ }
151
+ if (req.totalCount) {
152
+ total += 1
153
+ }
154
+ return total
155
+ }
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,12 @@ 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
- program.option('--subscription-sql-statement-timeout <ms>', 'sql statement timeout for polling queries', nat)
34
+ program.option('--subscription-max-response-size <nodes>', 'max response size measured in nodes', nat)
33
35
 
34
36
  let opts = program.parse().opts() as {
35
37
  schema: string
@@ -37,10 +39,12 @@ GraphQL server for postgres-compatible databases
37
39
  dbType: Dialect
38
40
  port: number
39
41
  maxRequestSize: number
42
+ maxRootFields?: number
43
+ maxResponseSize?: number
40
44
  sqlStatementTimeout?: number
41
45
  subscriptions?: boolean
42
46
  subscriptionPollInterval: number
43
- subscriptionSqlStatementTimeout?: number
47
+ subscriptionMaxResponseSize?: number
44
48
  }
45
49
 
46
50
  let model = loadModel(opts.schema)
@@ -50,14 +54,6 @@ GraphQL server for postgres-compatible databases
50
54
  statement_timeout: opts.sqlStatementTimeout || undefined
51
55
  })
52
56
 
53
- let subscriptionConnection: Pool | undefined
54
- if (opts.subscriptions) {
55
- subscriptionConnection = new Pool({
56
- connectionString: opts.dbUrl,
57
- statement_timeout: opts.subscriptionSqlStatementTimeout || opts.sqlStatementTimeout || undefined
58
- })
59
- }
60
-
61
57
  let server = await serve({
62
58
  model,
63
59
  dialect: opts.dbType,
@@ -65,9 +61,11 @@ GraphQL server for postgres-compatible databases
65
61
  port: opts.port,
66
62
  log: LOG,
67
63
  maxRequestSizeBytes: opts.maxRequestSize * 1024,
64
+ maxRootFields: opts.maxRootFields,
65
+ maxResponseNodes: opts.maxResponseSize,
68
66
  subscriptions: opts.subscriptions,
69
67
  subscriptionPollInterval: opts.subscriptionPollInterval,
70
- subscriptionConnection
68
+ subscriptionMaxResponseNodes: opts.subscriptionMaxResponseSize
71
69
  })
72
70
 
73
71
  LOG.info(`listening on port ${server.port}`)
@@ -1,5 +1,5 @@
1
- import {assertNotNull, unexpectedCase} from "@subsquid/util-internal"
2
- import assert from "assert"
1
+ import {assertNotNull, unexpectedCase} from '@subsquid/util-internal'
2
+ import assert from 'assert'
3
3
  import {
4
4
  buildASTSchema,
5
5
  DocumentNode,
@@ -17,19 +17,20 @@ import {
17
17
  GraphQLUnionType,
18
18
  parse,
19
19
  validateSchema
20
- } from "graphql"
21
- import {Index, Model, Prop, PropType, Scalar} from "./model"
22
- import {validateModel} from "./model.tools"
23
- import {customScalars} from "./scalars"
20
+ } from 'graphql'
21
+ import {Index, Model, Prop, PropType, Scalar} from './model'
22
+ import {validateModel} from './model.tools'
23
+ import {customScalars} from './scalars'
24
24
 
25
25
 
26
26
  const baseSchema = buildASTSchema(parse(`
27
27
  directive @entity on OBJECT
28
+ directive @query on INTERFACE
28
29
  directive @derivedFrom(field: String!) on FIELD_DEFINITION
29
30
  directive @unique on FIELD_DEFINITION
30
31
  directive @index(fields: [String!] unique: Boolean) on OBJECT | FIELD_DEFINITION
31
32
  directive @fulltext(query: String!) on FIELD_DEFINITION
32
- directive @cardinality(value: Int!) on FIELD_DEFINITION
33
+ directive @cardinality(value: Int!) on OBJECT | FIELD_DEFINITION
33
34
  directive @byteWeight(value: Float!) on FIELD_DEFINITION
34
35
  directive @variant on OBJECT # legacy
35
36
  directive @jsonField on OBJECT # legacy
@@ -77,17 +78,18 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
77
78
  let properties: Record<string, Prop> = {}
78
79
  let interfaces: string[] = []
79
80
  let indexes: Index[] = type instanceof GraphQLObjectType ? checkEntityIndexes(type) : []
81
+ let cardinality = checkEntityCardinality(type)
80
82
  let description = type.description || undefined
81
83
 
82
84
  switch(kind) {
83
85
  case 'entity':
84
- model[type.name] = {kind, properties, description, interfaces, indexes}
86
+ model[type.name] = {kind, properties, description, interfaces, indexes, ...cardinality}
85
87
  break
86
88
  case 'object':
87
89
  model[type.name] = {kind, properties, description, interfaces}
88
90
  break
89
91
  case 'interface':
90
- model[type.name] = {kind, properties, description}
92
+ model[type.name] = {kind, properties, description, queryable: isQueryableInterface(type)}
91
93
  break
92
94
  default:
93
95
  throw unexpectedCase(kind)
@@ -172,7 +174,7 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
172
174
  ...limits
173
175
  }
174
176
  } else if (fieldType instanceof GraphQLObjectType) {
175
- if (isEntityType(fieldType)) {
177
+ if (isEntityType(fieldType) && kind != 'interface') {
176
178
  switch(list.nulls.length) {
177
179
  case 0:
178
180
  if (derivedFrom) {
@@ -195,7 +197,7 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
195
197
  properties[key] = {
196
198
  type: {
197
199
  kind: 'fk',
198
- foreignEntity: fieldType.name
200
+ entity: fieldType.name
199
201
  },
200
202
  nullable,
201
203
  unique,
@@ -467,10 +469,29 @@ function checkDerivedFrom(type: GraphQLNamedType, f: GraphQLField<any, any>): {f
467
469
  }
468
470
 
469
471
 
472
+ function checkEntityCardinality(type: GraphQLObjectType | GraphQLInterfaceType): {cardinality?: number} {
473
+ let directives = type.astNode?.directives?.filter(d => d.name.value == 'cardinality') || []
474
+ if (directives.length > 0 && !isEntityType(type)) {
475
+ throw new SchemaError(`@cardinality directive can be only applied to entities, but were applied to ${type.name}`)
476
+ }
477
+ if (directives.length > 1) throw new SchemaError(
478
+ `Multiple @cardinality directives where applied to ${type.name}`
479
+ )
480
+ if (directives.length == 0) return {}
481
+ let arg = assertNotNull(directives[0].arguments?.find(arg => arg.name.value == 'value'))
482
+ assert(arg.value.kind == 'IntValue')
483
+ let cardinality = parseInt(arg.value.value, 10)
484
+ if (cardinality < 0) throw new SchemaError(
485
+ `Incorrect @cardinality where applied to ${type.name}. Cardinality value must be positive.`
486
+ )
487
+ return {cardinality}
488
+ }
489
+
490
+
470
491
  function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {cardinality?: number} {
471
492
  let directives = f.astNode?.directives?.filter(d => d.name.value == 'cardinality') || []
472
493
  if (directives.length > 1) throw new SchemaError(
473
- `Multiple @cardinality where applied to ${type.name}.${f.name}`
494
+ `Multiple @cardinality directives where applied to ${type.name}.${f.name}`
474
495
  )
475
496
  if (directives.length == 0) return {}
476
497
  let arg = assertNotNull(directives[0].arguments?.find(arg => arg.name.value == 'value'))
@@ -486,7 +507,7 @@ function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<
486
507
  function checkByteWeightDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {byteWeight?: number} {
487
508
  let directives = f.astNode?.directives?.filter(d => d.name.value == 'byteWeight') || []
488
509
  if (directives.length > 1) throw new SchemaError(
489
- `Multiple @byteWeight where applied to ${type.name}.${f.name}`
510
+ `Multiple @byteWeight directives where applied to ${type.name}.${f.name}`
490
511
  )
491
512
  if (directives.length == 0) return {}
492
513
  let arg = assertNotNull(directives[0].arguments?.find(arg => arg.name.value == 'value'))
@@ -499,6 +520,12 @@ function checkByteWeightDirective(type: GraphQLNamedType, f: GraphQLField<any, a
499
520
  }
500
521
 
501
522
 
523
+ function isQueryableInterface(type: GraphQLOutputType): boolean {
524
+ return type instanceof GraphQLInterfaceType
525
+ && !!type.astNode?.directives?.find(d => d.name.value == 'query')
526
+ }
527
+
528
+
502
529
  function unsupportedFieldTypeError(propName: string): Error {
503
530
  return new SchemaError(`Property ${propName} has unsupported type`)
504
531
  }