@subsquid/openreader 5.4.0-beta.aa7384 → 5.5.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/db.d.ts +3 -1
- package/lib/db.d.ts.map +1 -1
- package/lib/db.js +3 -2
- package/lib/db.js.map +1 -1
- package/lib/dialect/opencrud/schema.d.ts.map +1 -1
- package/lib/dialect/opencrud/schema.js +15 -1
- package/lib/dialect/opencrud/schema.js.map +1 -1
- package/lib/dialect/opencrud/tree.d.ts.map +1 -1
- package/lib/dialect/opencrud/tree.js +17 -0
- package/lib/dialect/opencrud/tree.js.map +1 -1
- package/lib/main.js +5 -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 +25 -1
- package/lib/model.schema.js.map +1 -1
- package/lib/server.d.ts +4 -2
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +6 -3
- package/lib/server.js.map +1 -1
- package/lib/sql/cursor.d.ts.map +1 -1
- package/lib/sql/cursor.js +8 -1
- package/lib/sql/cursor.js.map +1 -1
- package/lib/test/basic.test.js +1 -0
- package/lib/test/basic.test.js.map +1 -1
- package/lib/test/disableForeignKeyConstraint.test.d.ts +2 -0
- package/lib/test/disableForeignKeyConstraint.test.d.ts.map +1 -0
- package/lib/test/disableForeignKeyConstraint.test.js +332 -0
- package/lib/test/disableForeignKeyConstraint.test.js.map +1 -0
- package/lib/util/util.d.ts +3 -0
- package/lib/util/util.d.ts.map +1 -1
- package/lib/util/util.js +11 -0
- package/lib/util/util.js.map +1 -1
- package/package.json +5 -5
- package/src/db.ts +4 -1
- package/src/dialect/opencrud/schema.ts +20 -2
- package/src/dialect/opencrud/tree.ts +18 -1
- package/src/main.ts +7 -1
- package/src/model.schema.ts +31 -1
- package/src/model.ts +1 -0
- package/src/server.ts +12 -4
- package/src/sql/cursor.ts +8 -2
- package/src/test/basic.test.ts +1 -0
- package/src/test/disableForeignKeyConstraint.test.ts +363 -0
- package/src/util/util.ts +16 -0
package/src/main.ts
CHANGED
|
@@ -29,9 +29,11 @@ GraphQL server for postgres-compatible databases
|
|
|
29
29
|
program.option('--max-root-fields <count>', 'max number of root fields in a query', nat)
|
|
30
30
|
program.option('--max-response-size <nodes>', 'max response size measured in nodes', nat)
|
|
31
31
|
program.option('--sql-statement-timeout <ms>', 'sql statement timeout in ms', nat)
|
|
32
|
+
program.option('--validation-max-errors <count>', 'max validation errors', nat)
|
|
32
33
|
program.option('--subscriptions', 'enable gql subscriptions')
|
|
33
34
|
program.option('--subscription-poll-interval <ms>', 'subscription poll interval in ms', nat, 1000)
|
|
34
35
|
program.option('--subscription-max-response-size <nodes>', 'max response size measured in nodes', nat)
|
|
36
|
+
program.addOption(new Option('--isolation-level <level>', 'transaction isolation level').choices(['SERIALIZABLE', 'REPEATABLE READ', 'READ COMMITTED']))
|
|
35
37
|
|
|
36
38
|
let opts = program.parse().opts() as {
|
|
37
39
|
schema: string
|
|
@@ -42,9 +44,11 @@ GraphQL server for postgres-compatible databases
|
|
|
42
44
|
maxRootFields?: number
|
|
43
45
|
maxResponseSize?: number
|
|
44
46
|
sqlStatementTimeout?: number
|
|
47
|
+
validationMaxErrors?: number
|
|
45
48
|
subscriptions?: boolean
|
|
46
49
|
subscriptionPollInterval: number
|
|
47
50
|
subscriptionMaxResponseSize?: number
|
|
51
|
+
isolationLevel?: 'SERIALIZABLE' | 'REPEATABLE READ' | 'READ COMMITTED'
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
let model = loadModel(opts.schema)
|
|
@@ -65,7 +69,9 @@ GraphQL server for postgres-compatible databases
|
|
|
65
69
|
maxResponseNodes: opts.maxResponseSize,
|
|
66
70
|
subscriptions: opts.subscriptions,
|
|
67
71
|
subscriptionPollInterval: opts.subscriptionPollInterval,
|
|
68
|
-
subscriptionMaxResponseNodes: opts.subscriptionMaxResponseSize
|
|
72
|
+
subscriptionMaxResponseNodes: opts.subscriptionMaxResponseSize,
|
|
73
|
+
validationMaxErrors: opts.validationMaxErrors,
|
|
74
|
+
isolationLevel: opts.isolationLevel
|
|
69
75
|
})
|
|
70
76
|
|
|
71
77
|
LOG.info(`listening on port ${server.port}`)
|
package/src/model.schema.ts
CHANGED
|
@@ -32,6 +32,7 @@ const baseSchema = buildASTSchema(parse(`
|
|
|
32
32
|
directive @fulltext(query: String!) on FIELD_DEFINITION
|
|
33
33
|
directive @cardinality(value: Int!) on OBJECT | FIELD_DEFINITION
|
|
34
34
|
directive @byteWeight(value: Float!) on FIELD_DEFINITION
|
|
35
|
+
directive @disableForeignKeyConstraint on FIELD_DEFINITION
|
|
35
36
|
directive @variant on OBJECT # legacy
|
|
36
37
|
directive @jsonField on OBJECT # legacy
|
|
37
38
|
scalar ID
|
|
@@ -132,6 +133,7 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
|
|
|
132
133
|
let derivedFrom = checkDerivedFrom(type, f)
|
|
133
134
|
let index = checkFieldIndex(type, f)
|
|
134
135
|
let unique = index?.unique || false
|
|
136
|
+
let fkConstraint = checkDisableForeignKeyConstraint(type, f)
|
|
135
137
|
let limits = {
|
|
136
138
|
...checkByteWeightDirective(type, f),
|
|
137
139
|
...checkCardinalityLimitDirective(type, f)
|
|
@@ -199,10 +201,14 @@ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType
|
|
|
199
201
|
description
|
|
200
202
|
}
|
|
201
203
|
} else {
|
|
204
|
+
if (fkConstraint.disableConstraint && !nullable) {
|
|
205
|
+
throw new SchemaError(`Property ${propName} must be nullable when @disableForeignKeyConstraint is applied`)
|
|
206
|
+
}
|
|
202
207
|
properties[key] = {
|
|
203
208
|
type: {
|
|
204
209
|
kind: 'fk',
|
|
205
|
-
entity: fieldType.name
|
|
210
|
+
entity: fieldType.name,
|
|
211
|
+
...fkConstraint
|
|
206
212
|
},
|
|
207
213
|
nullable,
|
|
208
214
|
unique,
|
|
@@ -509,6 +515,30 @@ function checkCardinalityLimitDirective(type: GraphQLNamedType, f: GraphQLField<
|
|
|
509
515
|
}
|
|
510
516
|
|
|
511
517
|
|
|
518
|
+
function checkDisableForeignKeyConstraint(type: GraphQLNamedType, f: GraphQLField<any, any>): {disableConstraint?: boolean} {
|
|
519
|
+
let directives = f.astNode?.directives?.filter(d => d.name.value == 'disableForeignKeyConstraint') || []
|
|
520
|
+
if (directives.length == 0) return {}
|
|
521
|
+
if (!isEntityType(type)) throw new SchemaError(
|
|
522
|
+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but only entity fields can have this directive`
|
|
523
|
+
)
|
|
524
|
+
if (directives.length > 1) throw new SchemaError(
|
|
525
|
+
`Multiple @disableForeignKeyConstraint directives were applied to ${type.name}.${f.name}`
|
|
526
|
+
)
|
|
527
|
+
let fieldType = asNonNull(f)
|
|
528
|
+
let list = unwrapList(fieldType)
|
|
529
|
+
if (list.nulls.length > 0) throw new SchemaError(
|
|
530
|
+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but list fields cannot have this directive`
|
|
531
|
+
)
|
|
532
|
+
if (!isEntityType(list.item)) throw new SchemaError(
|
|
533
|
+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but only foreign key fields can have this directive`
|
|
534
|
+
)
|
|
535
|
+
if (f.astNode?.directives?.some(d => d.name.value == 'derivedFrom')) throw new SchemaError(
|
|
536
|
+
`@disableForeignKeyConstraint was applied to ${type.name}.${f.name}, but @derivedFrom fields cannot have this directive`
|
|
537
|
+
)
|
|
538
|
+
return {disableConstraint: true}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
512
542
|
function checkByteWeightDirective(type: GraphQLNamedType, f: GraphQLField<any, any>): {byteWeight?: number} {
|
|
513
543
|
let directives = f.astNode?.directives?.filter(d => d.name.value == 'byteWeight') || []
|
|
514
544
|
if (directives.length > 1) throw new SchemaError(
|
package/src/model.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Logger} from '@subsquid/logger'
|
|
2
2
|
import {listen, ListeningServer} from '@subsquid/util-internal-http-server'
|
|
3
3
|
import {KeyValueCache, PluginDefinition} from '@subsquid/apollo-server-core'
|
|
4
|
-
import {
|
|
4
|
+
import {ApolloServer} from '@subsquid/apollo-server-express'
|
|
5
5
|
import express from 'express'
|
|
6
6
|
import fs from 'fs'
|
|
7
7
|
import {ExecutionArgs, GraphQLSchema} from 'graphql'
|
|
@@ -12,7 +12,7 @@ import type {Pool} from 'pg'
|
|
|
12
12
|
import {WebSocketServer} from 'ws'
|
|
13
13
|
import {Context, OpenreaderContext} from './context'
|
|
14
14
|
import {PoolOpenreaderContext} from './db'
|
|
15
|
-
import type {DbType} from './db'
|
|
15
|
+
import type {DbType, TransactionIsolationLevel} from './db'
|
|
16
16
|
import type {Model} from './model'
|
|
17
17
|
import {openreaderExecute, openreaderSubscribe} from './util/execute'
|
|
18
18
|
import {ResponseSizeLimit} from './util/limit'
|
|
@@ -34,7 +34,9 @@ export interface ServerOptions {
|
|
|
34
34
|
subscriptionPollInterval?: number
|
|
35
35
|
subscriptionConnection?: Pool
|
|
36
36
|
subscriptionMaxResponseNodes?: number
|
|
37
|
+
validationMaxErrors?: number
|
|
37
38
|
cache?: KeyValueCache
|
|
39
|
+
isolationLevel?: TransactionIsolationLevel
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
export async function serve(options: ServerOptions): Promise<ListeningServer> {
|
|
@@ -44,6 +46,7 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
|
|
|
44
46
|
subscriptionPollInterval,
|
|
45
47
|
maxResponseNodes,
|
|
46
48
|
subscriptionMaxResponseNodes,
|
|
49
|
+
validationMaxErrors,
|
|
47
50
|
log,
|
|
48
51
|
} = options
|
|
49
52
|
|
|
@@ -52,12 +55,15 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
|
|
|
52
55
|
let schemaBuilder = await getSchemaBuilder(options)
|
|
53
56
|
let schema = schemaBuilder.build()
|
|
54
57
|
|
|
58
|
+
let isolationLevel = options.isolationLevel ?? 'SERIALIZABLE'
|
|
59
|
+
|
|
55
60
|
let context = () => {
|
|
56
61
|
let openreader: OpenreaderContext = new PoolOpenreaderContext(
|
|
57
62
|
dbType,
|
|
58
63
|
connection,
|
|
59
64
|
subscriptionConnection,
|
|
60
65
|
subscriptionPollInterval,
|
|
66
|
+
isolationLevel,
|
|
61
67
|
log
|
|
62
68
|
)
|
|
63
69
|
|
|
@@ -85,6 +91,7 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
|
|
|
85
91
|
subscriptions: options.subscriptions,
|
|
86
92
|
log: options.log,
|
|
87
93
|
graphiqlConsole: options.graphiqlConsole,
|
|
94
|
+
validationMaxErrors,
|
|
88
95
|
maxRequestSizeBytes: options.maxRequestSizeBytes,
|
|
89
96
|
maxRootFields: options.maxRootFields,
|
|
90
97
|
cache: options.cache,
|
|
@@ -105,8 +112,8 @@ export interface ApolloOptions {
|
|
|
105
112
|
graphiqlConsole?: boolean
|
|
106
113
|
log?: Logger
|
|
107
114
|
maxRequestSizeBytes?: number
|
|
108
|
-
maxRootFields?: number
|
|
109
115
|
validationMaxErrors?: number
|
|
116
|
+
maxRootFields?: number
|
|
110
117
|
cache?: KeyValueCache
|
|
111
118
|
}
|
|
112
119
|
|
|
@@ -151,6 +158,7 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
|
|
|
151
158
|
cache: options.cache,
|
|
152
159
|
stopOnTerminationSignals: false,
|
|
153
160
|
allowBatchedHttpRequests: false,
|
|
161
|
+
dangerouslyDisableValidation: false,
|
|
154
162
|
validateOptions: {
|
|
155
163
|
maxErrors: validationMaxErrors
|
|
156
164
|
},
|
|
@@ -161,7 +169,7 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
|
|
|
161
169
|
rootValue: {},
|
|
162
170
|
contextValue: req.context,
|
|
163
171
|
variableValues: req.request.variables,
|
|
164
|
-
operationName: req.operationName
|
|
172
|
+
operationName: req.operationName,
|
|
165
173
|
})
|
|
166
174
|
},
|
|
167
175
|
plugins: [
|
package/src/sql/cursor.ts
CHANGED
|
@@ -4,7 +4,7 @@ import assert from "assert"
|
|
|
4
4
|
import {DbType} from "../db"
|
|
5
5
|
import {Entity, JsonObject, Model, ObjectPropType, Prop, UnionPropType} from "../model"
|
|
6
6
|
import {getEntity, getFtsQuery, getObject, getUnionProps} from "../model.tools"
|
|
7
|
-
import {toColumn, toFkColumn, toTable} from "../util/util"
|
|
7
|
+
import {getFkPropByIdField, toColumn, toFkColumn, toTable} from "../util/util"
|
|
8
8
|
import {AliasSet, escapeIdentifier, JoinSet} from "./util"
|
|
9
9
|
|
|
10
10
|
|
|
@@ -62,7 +62,13 @@ export class EntityCursor implements Cursor {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
prop(field: string): Prop {
|
|
65
|
-
|
|
65
|
+
let p = this.entity.properties[field]
|
|
66
|
+
if (p) return p
|
|
67
|
+
let fkProp = getFkPropByIdField(field, this.entity.properties)
|
|
68
|
+
if (fkProp) {
|
|
69
|
+
return {type: {kind: 'scalar', name: 'String'}, nullable: fkProp.nullable}
|
|
70
|
+
}
|
|
71
|
+
return assertNotNull(p, `property ${field} is missing`)
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
output(field: string): string {
|
package/src/test/basic.test.ts
CHANGED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import expect from 'expect'
|
|
2
|
+
import {parse} from 'graphql'
|
|
3
|
+
import {buildModel, buildSchema, SchemaError} from '../model.schema'
|
|
4
|
+
import {useDatabase, useServer} from './setup'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function model(schema: string) {
|
|
8
|
+
return buildModel(buildSchema(parse(schema)))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
describe('@disableForeignKeyConstraint', function () {
|
|
13
|
+
describe('schema validation', function () {
|
|
14
|
+
it('parses valid usage and sets disableConstraint flag', function () {
|
|
15
|
+
let m = model(`
|
|
16
|
+
type Account @entity {
|
|
17
|
+
id: ID!
|
|
18
|
+
}
|
|
19
|
+
type Transfer @entity {
|
|
20
|
+
id: ID!
|
|
21
|
+
from: Account @disableForeignKeyConstraint
|
|
22
|
+
}
|
|
23
|
+
`)
|
|
24
|
+
expect(m.Transfer).toBeDefined()
|
|
25
|
+
expect(m.Transfer.kind).toBe('entity')
|
|
26
|
+
if (m.Transfer.kind === 'entity') {
|
|
27
|
+
let fromProp = m.Transfer.properties.from
|
|
28
|
+
expect(fromProp.type).toEqual({
|
|
29
|
+
kind: 'fk',
|
|
30
|
+
entity: 'Account',
|
|
31
|
+
disableConstraint: true,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('rejects non-nullable FK field', function () {
|
|
37
|
+
expect(() =>
|
|
38
|
+
model(`
|
|
39
|
+
type Account @entity {
|
|
40
|
+
id: ID!
|
|
41
|
+
}
|
|
42
|
+
type Transfer @entity {
|
|
43
|
+
id: ID!
|
|
44
|
+
from: Account! @disableForeignKeyConstraint
|
|
45
|
+
}
|
|
46
|
+
`)
|
|
47
|
+
).toThrow(SchemaError)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('rejects directive on scalar field', function () {
|
|
51
|
+
expect(() =>
|
|
52
|
+
model(`
|
|
53
|
+
type Account @entity {
|
|
54
|
+
id: ID!
|
|
55
|
+
name: String @disableForeignKeyConstraint
|
|
56
|
+
}
|
|
57
|
+
`)
|
|
58
|
+
).toThrow(SchemaError)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('rejects directive on enum field', function () {
|
|
62
|
+
expect(() =>
|
|
63
|
+
model(`
|
|
64
|
+
enum Status { ACTIVE INACTIVE }
|
|
65
|
+
type Account @entity {
|
|
66
|
+
id: ID!
|
|
67
|
+
status: Status @disableForeignKeyConstraint
|
|
68
|
+
}
|
|
69
|
+
`)
|
|
70
|
+
).toThrow(SchemaError)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('rejects directive on list (derivedFrom) field', function () {
|
|
74
|
+
expect(() =>
|
|
75
|
+
model(`
|
|
76
|
+
type Account @entity {
|
|
77
|
+
id: ID!
|
|
78
|
+
transfers: [Transfer!] @derivedFrom(field: "from") @disableForeignKeyConstraint
|
|
79
|
+
}
|
|
80
|
+
type Transfer @entity {
|
|
81
|
+
id: ID!
|
|
82
|
+
from: Account
|
|
83
|
+
}
|
|
84
|
+
`)
|
|
85
|
+
).toThrow(SchemaError)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('rejects directive on @derivedFrom lookup field', function () {
|
|
89
|
+
expect(() =>
|
|
90
|
+
model(`
|
|
91
|
+
type Account @entity {
|
|
92
|
+
id: ID!
|
|
93
|
+
profile: Profile @derivedFrom(field: "account") @disableForeignKeyConstraint
|
|
94
|
+
}
|
|
95
|
+
type Profile @entity {
|
|
96
|
+
id: ID!
|
|
97
|
+
account: Account! @unique
|
|
98
|
+
}
|
|
99
|
+
`)
|
|
100
|
+
).toThrow(SchemaError)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rejects directive on non-entity type', function () {
|
|
104
|
+
expect(() =>
|
|
105
|
+
model(`
|
|
106
|
+
type Account @entity {
|
|
107
|
+
id: ID!
|
|
108
|
+
}
|
|
109
|
+
type Metadata {
|
|
110
|
+
ref: Account @disableForeignKeyConstraint
|
|
111
|
+
}
|
|
112
|
+
type Transfer @entity {
|
|
113
|
+
id: ID!
|
|
114
|
+
meta: Metadata
|
|
115
|
+
}
|
|
116
|
+
`)
|
|
117
|
+
).toThrow(SchemaError)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('runtime (opencrud)', function () {
|
|
122
|
+
// Database setup: transfer table has from_id and to_id columns
|
|
123
|
+
// with NO foreign key constraints. Some IDs reference accounts
|
|
124
|
+
// that don't exist, simulating @disableForeignKeyConstraint behavior.
|
|
125
|
+
useDatabase([
|
|
126
|
+
`create table account (id text primary key, name text)`,
|
|
127
|
+
`create table transfer (id text primary key, from_id text, to_id text, amount numeric)`,
|
|
128
|
+
// account '1' exists, account '2' exists, but account 'missing' does NOT exist
|
|
129
|
+
`insert into account (id, name) values ('1', 'Alice')`,
|
|
130
|
+
`insert into account (id, name) values ('2', 'Bob')`,
|
|
131
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t1', '1', '2', 100)`,
|
|
132
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t2', '2', 'missing', 50)`,
|
|
133
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t3', 'missing', '1', 75)`,
|
|
134
|
+
`insert into transfer (id, from_id, to_id, amount) values ('t4', null, '1', 25)`,
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
const client = useServer(`
|
|
138
|
+
type Account @entity {
|
|
139
|
+
id: ID!
|
|
140
|
+
name: String
|
|
141
|
+
transfersFrom: [Transfer!] @derivedFrom(field: "from")
|
|
142
|
+
transfersTo: [Transfer!] @derivedFrom(field: "to")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type Transfer @entity {
|
|
146
|
+
id: ID!
|
|
147
|
+
from: Account @disableForeignKeyConstraint
|
|
148
|
+
to: Account @disableForeignKeyConstraint
|
|
149
|
+
amount: Int!
|
|
150
|
+
}
|
|
151
|
+
`)
|
|
152
|
+
|
|
153
|
+
it('returns null for FK relation when referenced row is missing, but exposes raw ID via {field}Id', function () {
|
|
154
|
+
return client.test(`
|
|
155
|
+
query {
|
|
156
|
+
transfers(orderBy: [id_ASC]) {
|
|
157
|
+
id
|
|
158
|
+
from { id name }
|
|
159
|
+
fromId
|
|
160
|
+
to { id name }
|
|
161
|
+
toId
|
|
162
|
+
amount
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
`, {
|
|
166
|
+
transfers: [
|
|
167
|
+
{id: 't1', from: {id: '1', name: 'Alice'}, fromId: '1', to: {id: '2', name: 'Bob'}, toId: '2', amount: 100},
|
|
168
|
+
{id: 't2', from: {id: '2', name: 'Bob'}, fromId: '2', to: null, toId: 'missing', amount: 50},
|
|
169
|
+
{id: 't3', from: null, fromId: 'missing', to: {id: '1', name: 'Alice'}, toId: '1', amount: 75},
|
|
170
|
+
{id: 't4', from: null, fromId: null, to: {id: '1', name: 'Alice'}, toId: '1', amount: 25},
|
|
171
|
+
]
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('can query only {field}Id without the relation', function () {
|
|
176
|
+
return client.test(`
|
|
177
|
+
query {
|
|
178
|
+
transfers(orderBy: [id_ASC]) {
|
|
179
|
+
id
|
|
180
|
+
fromId
|
|
181
|
+
toId
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`, {
|
|
185
|
+
transfers: [
|
|
186
|
+
{id: 't1', fromId: '1', toId: '2'},
|
|
187
|
+
{id: 't2', fromId: '2', toId: 'missing'},
|
|
188
|
+
{id: 't3', fromId: 'missing', toId: '1'},
|
|
189
|
+
{id: 't4', fromId: null, toId: '1'},
|
|
190
|
+
]
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('supports {field}Id_eq filter', function () {
|
|
195
|
+
return client.test(`
|
|
196
|
+
query {
|
|
197
|
+
transfers(where: {fromId_eq: "missing"}, orderBy: [id_ASC]) {
|
|
198
|
+
id
|
|
199
|
+
fromId
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
`, {
|
|
203
|
+
transfers: [
|
|
204
|
+
{id: 't3', fromId: 'missing'},
|
|
205
|
+
]
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('supports {field}Id_not_eq filter', function () {
|
|
210
|
+
return client.test(`
|
|
211
|
+
query {
|
|
212
|
+
transfers(where: {fromId_not_eq: "missing"}, orderBy: [id_ASC]) {
|
|
213
|
+
id
|
|
214
|
+
fromId
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
`, {
|
|
218
|
+
transfers: [
|
|
219
|
+
{id: 't1', fromId: '1'},
|
|
220
|
+
{id: 't2', fromId: '2'},
|
|
221
|
+
]
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('supports {field}Id_in filter', function () {
|
|
226
|
+
return client.test(`
|
|
227
|
+
query {
|
|
228
|
+
transfers(where: {fromId_in: ["1", "missing"]}, orderBy: [id_ASC]) {
|
|
229
|
+
id
|
|
230
|
+
fromId
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
`, {
|
|
234
|
+
transfers: [
|
|
235
|
+
{id: 't1', fromId: '1'},
|
|
236
|
+
{id: 't3', fromId: 'missing'},
|
|
237
|
+
]
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('supports {field}Id_not_in filter', function () {
|
|
242
|
+
return client.test(`
|
|
243
|
+
query {
|
|
244
|
+
transfers(where: {fromId_not_in: ["1", "missing"]}, orderBy: [id_ASC]) {
|
|
245
|
+
id
|
|
246
|
+
fromId
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
`, {
|
|
250
|
+
transfers: [
|
|
251
|
+
{id: 't2', fromId: '2'},
|
|
252
|
+
]
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('supports {field}Id_isNull filter', function () {
|
|
257
|
+
return client.test(`
|
|
258
|
+
query {
|
|
259
|
+
transfers(where: {fromId_isNull: true}, orderBy: [id_ASC]) {
|
|
260
|
+
id
|
|
261
|
+
fromId
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
`, {
|
|
265
|
+
transfers: [
|
|
266
|
+
{id: 't4', fromId: null},
|
|
267
|
+
]
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('supports {field}Id_contains filter', function () {
|
|
272
|
+
return client.test(`
|
|
273
|
+
query {
|
|
274
|
+
transfers(where: {fromId_contains: "iss"}, orderBy: [id_ASC]) {
|
|
275
|
+
id
|
|
276
|
+
fromId
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
`, {
|
|
280
|
+
transfers: [
|
|
281
|
+
{id: 't3', fromId: 'missing'},
|
|
282
|
+
]
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('supports {field}Id_startsWith filter', function () {
|
|
287
|
+
return client.test(`
|
|
288
|
+
query {
|
|
289
|
+
transfers(where: {fromId_startsWith: "mis"}, orderBy: [id_ASC]) {
|
|
290
|
+
id
|
|
291
|
+
fromId
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
`, {
|
|
295
|
+
transfers: [
|
|
296
|
+
{id: 't3', fromId: 'missing'},
|
|
297
|
+
]
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('supports {field}Id_endsWith filter', function () {
|
|
302
|
+
return client.test(`
|
|
303
|
+
query {
|
|
304
|
+
transfers(where: {toId_endsWith: "2"}, orderBy: [id_ASC]) {
|
|
305
|
+
id
|
|
306
|
+
toId
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
`, {
|
|
310
|
+
transfers: [
|
|
311
|
+
{id: 't1', toId: '2'},
|
|
312
|
+
]
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('supports {field}Id_gt and {field}Id_lt filters', function () {
|
|
317
|
+
return client.test(`
|
|
318
|
+
query {
|
|
319
|
+
transfers(where: {fromId_gt: "1", fromId_lt: "missing"}, orderBy: [id_ASC]) {
|
|
320
|
+
id
|
|
321
|
+
fromId
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
`, {
|
|
325
|
+
transfers: [
|
|
326
|
+
{id: 't2', fromId: '2'},
|
|
327
|
+
]
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('can combine {field}Id filter with other filters', function () {
|
|
332
|
+
return client.test(`
|
|
333
|
+
query {
|
|
334
|
+
transfers(where: {fromId_eq: "2", amount_gt: 40}, orderBy: [id_ASC]) {
|
|
335
|
+
id
|
|
336
|
+
fromId
|
|
337
|
+
amount
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
`, {
|
|
341
|
+
transfers: [
|
|
342
|
+
{id: 't2', fromId: '2', amount: 50},
|
|
343
|
+
]
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('can combine {field}Id filter with relation filter in OR', function () {
|
|
348
|
+
return client.test(`
|
|
349
|
+
query {
|
|
350
|
+
transfers(where: {OR: [{fromId_eq: "missing"}, {from: {name_eq: "Alice"}}]}, orderBy: [id_ASC]) {
|
|
351
|
+
id
|
|
352
|
+
fromId
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
`, {
|
|
356
|
+
transfers: [
|
|
357
|
+
{id: 't1', fromId: '1'},
|
|
358
|
+
{id: 't3', fromId: 'missing'},
|
|
359
|
+
]
|
|
360
|
+
})
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
})
|
package/src/util/util.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {toSnakeCase} from "@subsquid/util-naming"
|
|
2
2
|
import assert from "assert"
|
|
3
|
+
import {Prop} from "../model"
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export function toColumn(gqlFieldName: string): string {
|
|
@@ -37,3 +38,18 @@ export function invalidFormat(type: string, value: string): Error {
|
|
|
37
38
|
export function identity<T>(x: T): T {
|
|
38
39
|
return x
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
export function toFkIdField(fkFieldName: string): string {
|
|
44
|
+
return fkFieldName + 'Id'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export function getFkPropByIdField(
|
|
49
|
+
idFieldName: string,
|
|
50
|
+
properties: Record<string, Prop>
|
|
51
|
+
): Prop | undefined {
|
|
52
|
+
if (!idFieldName.endsWith('Id')) return undefined
|
|
53
|
+
let fkProp = properties[idFieldName.slice(0, -2)]
|
|
54
|
+
return fkProp?.type.kind == 'fk' ? fkProp : undefined
|
|
55
|
+
}
|