@subsquid/openreader 5.4.0-beta.aa7384 → 5.4.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 (41) hide show
  1. package/lib/dialect/opencrud/schema.d.ts.map +1 -1
  2. package/lib/dialect/opencrud/schema.js +15 -1
  3. package/lib/dialect/opencrud/schema.js.map +1 -1
  4. package/lib/dialect/opencrud/tree.d.ts.map +1 -1
  5. package/lib/dialect/opencrud/tree.js +17 -0
  6. package/lib/dialect/opencrud/tree.js.map +1 -1
  7. package/lib/main.js +3 -1
  8. package/lib/main.js.map +1 -1
  9. package/lib/model.d.ts +1 -0
  10. package/lib/model.d.ts.map +1 -1
  11. package/lib/model.schema.d.ts.map +1 -1
  12. package/lib/model.schema.js +25 -1
  13. package/lib/model.schema.js.map +1 -1
  14. package/lib/server.d.ts +2 -1
  15. package/lib/server.d.ts.map +1 -1
  16. package/lib/server.js +4 -2
  17. package/lib/server.js.map +1 -1
  18. package/lib/sql/cursor.d.ts.map +1 -1
  19. package/lib/sql/cursor.js +8 -1
  20. package/lib/sql/cursor.js.map +1 -1
  21. package/lib/test/basic.test.js +1 -0
  22. package/lib/test/basic.test.js.map +1 -1
  23. package/lib/test/disableForeignKeyConstraint.test.d.ts +2 -0
  24. package/lib/test/disableForeignKeyConstraint.test.d.ts.map +1 -0
  25. package/lib/test/disableForeignKeyConstraint.test.js +332 -0
  26. package/lib/test/disableForeignKeyConstraint.test.js.map +1 -0
  27. package/lib/util/util.d.ts +3 -0
  28. package/lib/util/util.d.ts.map +1 -1
  29. package/lib/util/util.js +11 -0
  30. package/lib/util/util.js.map +1 -1
  31. package/package.json +5 -5
  32. package/src/dialect/opencrud/schema.ts +20 -2
  33. package/src/dialect/opencrud/tree.ts +18 -1
  34. package/src/main.ts +4 -1
  35. package/src/model.schema.ts +31 -1
  36. package/src/model.ts +1 -0
  37. package/src/server.ts +7 -3
  38. package/src/sql/cursor.ts +8 -2
  39. package/src/test/basic.test.ts +1 -0
  40. package/src/test/disableForeignKeyConstraint.test.ts +363 -0
  41. package/src/util/util.ts +16 -0
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 { ApolloServer } from '@subsquid/apollo-server-express';
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'
@@ -34,6 +34,7 @@ export interface ServerOptions {
34
34
  subscriptionPollInterval?: number
35
35
  subscriptionConnection?: Pool
36
36
  subscriptionMaxResponseNodes?: number
37
+ validationMaxErrors?: number
37
38
  cache?: KeyValueCache
38
39
  }
39
40
 
@@ -44,6 +45,7 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
44
45
  subscriptionPollInterval,
45
46
  maxResponseNodes,
46
47
  subscriptionMaxResponseNodes,
48
+ validationMaxErrors,
47
49
  log,
48
50
  } = options
49
51
 
@@ -85,6 +87,7 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
85
87
  subscriptions: options.subscriptions,
86
88
  log: options.log,
87
89
  graphiqlConsole: options.graphiqlConsole,
90
+ validationMaxErrors,
88
91
  maxRequestSizeBytes: options.maxRequestSizeBytes,
89
92
  maxRootFields: options.maxRootFields,
90
93
  cache: options.cache,
@@ -105,8 +108,8 @@ export interface ApolloOptions {
105
108
  graphiqlConsole?: boolean
106
109
  log?: Logger
107
110
  maxRequestSizeBytes?: number
108
- maxRootFields?: number
109
111
  validationMaxErrors?: number
112
+ maxRootFields?: number
110
113
  cache?: KeyValueCache
111
114
  }
112
115
 
@@ -151,6 +154,7 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
151
154
  cache: options.cache,
152
155
  stopOnTerminationSignals: false,
153
156
  allowBatchedHttpRequests: false,
157
+ dangerouslyDisableValidation: false,
154
158
  validateOptions: {
155
159
  maxErrors: validationMaxErrors
156
160
  },
@@ -161,7 +165,7 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
161
165
  rootValue: {},
162
166
  contextValue: req.context,
163
167
  variableValues: req.request.variables,
164
- operationName: req.operationName
168
+ operationName: req.operationName,
165
169
  })
166
170
  },
167
171
  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
- return assertNotNull(this.entity.properties[field], `property ${field} is missing`)
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 {
@@ -314,6 +314,7 @@ describe('basic tests', function() {
314
314
  fields: [
315
315
  {description: 'Unique identifier'},
316
316
  {description: 'Related account'},
317
+ {description: 'Related account'},
317
318
  {description: 'Balance'},
318
319
  ]
319
320
  }
@@ -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
+ }