@subsquid/openreader 5.4.0-beta.11e779 → 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 (47) hide show
  1. package/lib/dialect/opencrud/schema.d.ts.map +1 -1
  2. package/lib/dialect/opencrud/schema.js +16 -2
  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/model.d.ts +1 -0
  8. package/lib/model.d.ts.map +1 -1
  9. package/lib/model.schema.d.ts.map +1 -1
  10. package/lib/model.schema.js +25 -1
  11. package/lib/model.schema.js.map +1 -1
  12. package/lib/server.d.ts +1 -1
  13. package/lib/server.d.ts.map +1 -1
  14. package/lib/server.js +4 -2
  15. package/lib/server.js.map +1 -1
  16. package/lib/sql/cursor.d.ts.map +1 -1
  17. package/lib/sql/cursor.js +8 -1
  18. package/lib/sql/cursor.js.map +1 -1
  19. package/lib/test/basic.test.js +1 -0
  20. package/lib/test/basic.test.js.map +1 -1
  21. package/lib/test/disableForeignKeyConstraint.test.d.ts +2 -0
  22. package/lib/test/disableForeignKeyConstraint.test.d.ts.map +1 -0
  23. package/lib/test/disableForeignKeyConstraint.test.js +332 -0
  24. package/lib/test/disableForeignKeyConstraint.test.js.map +1 -0
  25. package/lib/util/execute.js +1 -1
  26. package/lib/util/execute.js.map +1 -1
  27. package/lib/util/limit.js +1 -1
  28. package/lib/util/limit.js.map +1 -1
  29. package/lib/util/resolve-tree.js +1 -1
  30. package/lib/util/resolve-tree.js.map +1 -1
  31. package/lib/util/util.d.ts +3 -0
  32. package/lib/util/util.d.ts.map +1 -1
  33. package/lib/util/util.js +11 -0
  34. package/lib/util/util.js.map +1 -1
  35. package/package.json +6 -6
  36. package/src/dialect/opencrud/schema.ts +21 -3
  37. package/src/dialect/opencrud/tree.ts +18 -1
  38. package/src/model.schema.ts +31 -1
  39. package/src/model.ts +1 -0
  40. package/src/server.ts +5 -4
  41. package/src/sql/cursor.ts +8 -2
  42. package/src/test/basic.test.ts +1 -0
  43. package/src/test/disableForeignKeyConstraint.test.ts +363 -0
  44. package/src/util/execute.ts +1 -1
  45. package/src/util/limit.ts +1 -1
  46. package/src/util/resolve-tree.ts +1 -1
  47. package/src/util/util.ts +16 -0
package/src/server.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import {Logger} from '@subsquid/logger'
2
2
  import {listen, ListeningServer} from '@subsquid/util-internal-http-server'
3
- import {KeyValueCache, PluginDefinition} from 'apollo-server-core'
4
- import {ApolloServer} from 'apollo-server-express'
3
+ import {KeyValueCache, PluginDefinition} from '@subsquid/apollo-server-core'
4
+ import {ApolloServer} from '@subsquid/apollo-server-express'
5
5
  import express from 'express'
6
- import { validate } from 'graphql'
7
6
  import fs from 'fs'
8
7
  import {ExecutionArgs, GraphQLSchema} from 'graphql'
9
8
  import {useServer as useWsServer} from 'graphql-ws/lib/use/ws'
@@ -156,7 +155,9 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
156
155
  stopOnTerminationSignals: false,
157
156
  allowBatchedHttpRequests: false,
158
157
  dangerouslyDisableValidation: false,
159
- validationMaxErrors,
158
+ validateOptions: {
159
+ maxErrors: validationMaxErrors
160
+ },
160
161
  executor: async req => {
161
162
  return execute({
162
163
  schema,
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
+ })
@@ -1,6 +1,6 @@
1
1
  import {Logger, LogLevel} from '@subsquid/logger'
2
2
  import {addErrorContext} from '@subsquid/util-internal'
3
- import {UserInputError} from 'apollo-server-core'
3
+ import {UserInputError} from '@subsquid/apollo-server-core'
4
4
  import {getOperationRootType, GraphQLError, subscribe as graphqlSubscribe} from 'graphql'
5
5
  import {ExecutionResult} from 'graphql-ws'
6
6
  import {
package/src/util/limit.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {UserInputError} from 'apollo-server-core'
1
+ import {UserInputError} from '@subsquid/apollo-server-core'
2
2
  import assert from 'assert'
3
3
 
4
4
 
@@ -1,4 +1,4 @@
1
- import {UserInputError} from "apollo-server-core"
1
+ import {UserInputError} from "@subsquid/apollo-server-core"
2
2
  import assert from "assert"
3
3
  import {GraphQLResolveInfo, GraphQLSchema} from "graphql"
4
4
  import {
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
+ }