@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.
- package/lib/context.d.ts +5 -2
- package/lib/context.d.ts.map +1 -1
- package/lib/ir/args.d.ts +1 -1
- package/lib/ir/args.d.ts.map +1 -1
- package/lib/ir/connection.d.ts +3 -4
- package/lib/ir/connection.d.ts.map +1 -1
- package/lib/ir/connection.js.map +1 -1
- package/lib/ir/fields.d.ts +6 -2
- package/lib/ir/fields.d.ts.map +1 -1
- package/lib/ir/fields.js +15 -0
- package/lib/ir/fields.js.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 +106 -12
- package/lib/limit.size.js.map +1 -1
- package/lib/main.js +6 -9
- package/lib/main.js.map +1 -1
- package/lib/model.d.ts +3 -1
- package/lib/model.d.ts.map +1 -1
- package/lib/model.schema.d.ts +2 -2
- package/lib/model.schema.d.ts.map +1 -1
- package/lib/model.schema.js +29 -7
- package/lib/model.schema.js.map +1 -1
- package/lib/model.tools.d.ts +6 -1
- package/lib/model.tools.d.ts.map +1 -1
- package/lib/model.tools.js +111 -8
- package/lib/model.tools.js.map +1 -1
- package/lib/opencrud/orderBy.d.ts +2 -2
- package/lib/opencrud/orderBy.d.ts.map +1 -1
- package/lib/opencrud/orderBy.js +13 -17
- package/lib/opencrud/orderBy.js.map +1 -1
- package/lib/opencrud/schema.d.ts +4 -4
- package/lib/opencrud/schema.d.ts.map +1 -1
- package/lib/opencrud/schema.js +60 -64
- package/lib/opencrud/schema.js.map +1 -1
- package/lib/opencrud/tree.d.ts +9 -7
- package/lib/opencrud/tree.d.ts.map +1 -1
- package/lib/opencrud/tree.js +32 -14
- package/lib/opencrud/tree.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/sql/cursor.js +2 -2
- package/lib/sql/cursor.js.map +1 -1
- package/lib/sql/mapping.d.ts +3 -1
- package/lib/sql/mapping.d.ts.map +1 -1
- package/lib/sql/mapping.js +16 -1
- package/lib/sql/mapping.js.map +1 -1
- package/lib/sql/printer.d.ts +29 -11
- package/lib/sql/printer.d.ts.map +1 -1
- package/lib/sql/printer.js +106 -10
- package/lib/sql/printer.js.map +1 -1
- package/lib/sql/query.d.ts +11 -11
- package/lib/sql/query.d.ts.map +1 -1
- package/lib/sql/query.js +41 -19
- package/lib/sql/query.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/queryable.test.d.ts +2 -0
- package/lib/test/queryable.test.d.ts.map +1 -0
- package/lib/test/queryable.test.js +255 -0
- package/lib/test/queryable.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 +3 -3
- package/src/context.ts +5 -2
- package/src/ir/args.ts +1 -1
- package/src/ir/connection.ts +3 -4
- package/src/ir/fields.ts +22 -2
- package/src/limit.size.ts +122 -13
- package/src/main.ts +18 -20
- package/src/model.schema.ts +40 -13
- package/src/model.tools.ts +121 -8
- package/src/model.ts +3 -1
- package/src/opencrud/orderBy.ts +13 -17
- package/src/opencrud/schema.ts +86 -85
- package/src/opencrud/tree.ts +55 -26
- package/src/server.ts +66 -26
- package/src/sql/cursor.ts +2 -2
- package/src/sql/mapping.ts +18 -1
- package/src/sql/printer.ts +137 -21
- package/src/sql/query.ts +50 -30
- package/src/test/limits.test.ts +163 -0
- package/src/test/queryable.test.ts +258 -0
- package/src/test/setup.ts +6 -3
- package/src/util/execute.ts +53 -0
- 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": "
|
|
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.
|
|
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
|
|
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
|
|
2
|
-
import {Query} from
|
|
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
package/src/ir/connection.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {OrderBy, Where} from
|
|
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?:
|
|
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 {
|
|
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?:
|
|
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
|
|
2
|
-
import
|
|
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
|
|
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
|
|
31
|
-
case "list-lookup":
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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,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-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
subscriptionMaxResponseNodes: opts.subscriptionMaxResponseSize
|
|
71
69
|
})
|
|
72
70
|
|
|
73
71
|
LOG.info(`listening on port ${server.port}`)
|
package/src/model.schema.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {assertNotNull, unexpectedCase} from
|
|
2
|
-
import assert from
|
|
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
|
|
21
|
-
import {Index, Model, Prop, PropType, Scalar} from
|
|
22
|
-
import {validateModel} from
|
|
23
|
-
import {customScalars} from
|
|
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
|
-
|
|
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
|
}
|