@subsquid/openreader 0.2.1 → 0.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 (131) hide show
  1. package/README.md +2 -15
  2. package/bin/main.js +2 -0
  3. package/dist/db.d.ts +28 -0
  4. package/dist/db.d.ts.map +1 -0
  5. package/dist/db.js +69 -0
  6. package/dist/db.js.map +1 -0
  7. package/dist/gql/opencrud.d.ts +1 -0
  8. package/dist/gql/opencrud.d.ts.map +1 -0
  9. package/dist/gql/opencrud.js +10 -9
  10. package/dist/gql/opencrud.js.map +1 -1
  11. package/dist/gql/schema.d.ts +1 -0
  12. package/dist/gql/schema.d.ts.map +1 -0
  13. package/dist/gql/schema.js +116 -17
  14. package/dist/gql/schema.js.map +1 -1
  15. package/dist/main.d.ts +2 -3
  16. package/dist/main.d.ts.map +1 -0
  17. package/dist/main.js +6 -30
  18. package/dist/main.js.map +1 -1
  19. package/dist/model.d.ts +9 -0
  20. package/dist/model.d.ts.map +1 -0
  21. package/dist/model.tools.d.ts +2 -0
  22. package/dist/model.tools.d.ts.map +1 -0
  23. package/dist/model.tools.js +27 -1
  24. package/dist/model.tools.js.map +1 -1
  25. package/dist/orderBy.d.ts +1 -0
  26. package/dist/orderBy.d.ts.map +1 -0
  27. package/dist/queryBuilder.d.ts +3 -2
  28. package/dist/queryBuilder.d.ts.map +1 -0
  29. package/dist/queryBuilder.js +28 -27
  30. package/dist/queryBuilder.js.map +1 -1
  31. package/dist/relayConnection.d.ts +1 -0
  32. package/dist/relayConnection.d.ts.map +1 -0
  33. package/dist/requestedFields.d.ts +1 -0
  34. package/dist/requestedFields.d.ts.map +1 -0
  35. package/dist/requestedFields.js +3 -3
  36. package/dist/requestedFields.js.map +1 -1
  37. package/dist/resolver.d.ts +3 -2
  38. package/dist/resolver.d.ts.map +1 -0
  39. package/dist/resolver.js +12 -11
  40. package/dist/resolver.js.map +1 -1
  41. package/dist/scalars.d.ts +2 -2
  42. package/dist/scalars.d.ts.map +1 -0
  43. package/dist/scalars.js.map +1 -1
  44. package/dist/server.d.ts +5 -11
  45. package/dist/server.d.ts.map +1 -0
  46. package/dist/server.js +13 -31
  47. package/dist/server.js.map +1 -1
  48. package/dist/test/basic.test.d.ts +2 -0
  49. package/dist/test/basic.test.d.ts.map +1 -0
  50. package/dist/test/basic.test.js +286 -0
  51. package/dist/test/basic.test.js.map +1 -0
  52. package/dist/test/connection.test.d.ts +2 -0
  53. package/dist/test/connection.test.d.ts.map +1 -0
  54. package/dist/test/connection.test.js +193 -0
  55. package/dist/test/connection.test.js.map +1 -0
  56. package/dist/test/fts.test.d.ts +2 -0
  57. package/dist/test/fts.test.d.ts.map +1 -0
  58. package/dist/test/fts.test.js +110 -0
  59. package/dist/test/fts.test.js.map +1 -0
  60. package/dist/test/lists.test.d.ts +2 -0
  61. package/dist/test/lists.test.d.ts.map +1 -0
  62. package/dist/test/lists.test.js +266 -0
  63. package/dist/test/lists.test.js.map +1 -0
  64. package/dist/test/lookup.test.d.ts +2 -0
  65. package/dist/test/lookup.test.d.ts.map +1 -0
  66. package/dist/test/lookup.test.js +109 -0
  67. package/dist/test/lookup.test.js.map +1 -0
  68. package/dist/test/scalars.test.d.ts +2 -0
  69. package/dist/test/scalars.test.d.ts.map +1 -0
  70. package/dist/test/scalars.test.js +303 -0
  71. package/dist/test/scalars.test.js.map +1 -0
  72. package/dist/test/tools.test.d.ts +2 -0
  73. package/dist/test/tools.test.d.ts.map +1 -0
  74. package/dist/test/tools.test.js +49 -0
  75. package/dist/test/tools.test.js.map +1 -0
  76. package/dist/test/typed-json.test.d.ts +2 -0
  77. package/dist/test/typed-json.test.d.ts.map +1 -0
  78. package/dist/test/typed-json.test.js +75 -0
  79. package/dist/test/typed-json.test.js.map +1 -0
  80. package/dist/test/unions.test.d.ts +2 -0
  81. package/dist/test/unions.test.d.ts.map +1 -0
  82. package/dist/test/unions.test.js +84 -0
  83. package/dist/test/unions.test.js.map +1 -0
  84. package/dist/test/util/setup.d.ts +7 -0
  85. package/dist/test/util/setup.d.ts.map +1 -0
  86. package/dist/test/util/setup.js +60 -0
  87. package/dist/test/util/setup.js.map +1 -0
  88. package/dist/test/where.test.d.ts +2 -0
  89. package/dist/test/where.test.d.ts.map +1 -0
  90. package/dist/test/where.test.js +127 -0
  91. package/dist/test/where.test.js.map +1 -0
  92. package/dist/tools.d.ts +1 -0
  93. package/dist/tools.d.ts.map +1 -0
  94. package/dist/util.d.ts +1 -13
  95. package/dist/util.d.ts.map +1 -0
  96. package/dist/util.js +6 -79
  97. package/dist/util.js.map +1 -1
  98. package/dist/where.d.ts +1 -0
  99. package/dist/where.d.ts.map +1 -0
  100. package/package.json +26 -20
  101. package/src/db.ts +83 -0
  102. package/src/gql/opencrud.ts +328 -0
  103. package/src/gql/schema.ts +463 -0
  104. package/src/main.ts +51 -0
  105. package/src/model.tools.ts +201 -0
  106. package/src/model.ts +137 -0
  107. package/src/orderBy.ts +105 -0
  108. package/src/queryBuilder.ts +785 -0
  109. package/src/relayConnection.ts +80 -0
  110. package/src/requestedFields.ts +246 -0
  111. package/src/resolver.ts +199 -0
  112. package/src/scalars.ts +247 -0
  113. package/src/server.ts +115 -0
  114. package/src/test/basic.test.ts +339 -0
  115. package/src/test/connection.test.ts +195 -0
  116. package/src/test/fts.test.ts +114 -0
  117. package/src/test/lists.test.ts +278 -0
  118. package/src/test/lookup.test.ts +111 -0
  119. package/src/test/scalars.test.ts +316 -0
  120. package/src/test/tools.test.ts +27 -0
  121. package/src/test/typed-json.test.ts +76 -0
  122. package/src/test/unions.test.ts +85 -0
  123. package/src/test/util/setup.ts +63 -0
  124. package/src/test/where.test.ts +135 -0
  125. package/src/tools.ts +33 -0
  126. package/src/util.ts +39 -0
  127. package/src/where.ts +110 -0
  128. package/CHANGELOG.md +0 -20
  129. package/dist/transaction.d.ts +0 -10
  130. package/dist/transaction.js +0 -47
  131. package/dist/transaction.js.map +0 -1
@@ -0,0 +1,463 @@
1
+ import {assertNotNull, unexpectedCase} from "@subsquid/util"
2
+ import assert from "assert"
3
+ import {
4
+ buildASTSchema,
5
+ DocumentNode,
6
+ extendSchema,
7
+ GraphQLEnumType,
8
+ GraphQLField,
9
+ GraphQLInterfaceType,
10
+ GraphQLList,
11
+ GraphQLNamedType,
12
+ GraphQLNonNull,
13
+ GraphQLObjectType,
14
+ GraphQLOutputType,
15
+ GraphQLScalarType,
16
+ GraphQLSchema,
17
+ GraphQLUnionType,
18
+ parse,
19
+ validateSchema
20
+ } from "graphql"
21
+ import {Index, Model, Prop, PropType} from "../model"
22
+ import {validateModel} from "../model.tools"
23
+ import {scalars_list} from "../scalars"
24
+
25
+
26
+ const baseSchema = buildASTSchema(parse(`
27
+ directive @entity on OBJECT
28
+ directive @derivedFrom(field: String!) on FIELD_DEFINITION
29
+ directive @unique on FIELD_DEFINITION
30
+ directive @index(fields: [String!] unique: Boolean) on OBJECT | FIELD_DEFINITION
31
+ directive @fulltext(query: String!) on FIELD_DEFINITION
32
+ directive @variant on OBJECT # legacy
33
+ directive @jsonField on OBJECT # legacy
34
+ ${scalars_list.map(name => 'scalar ' + name).join('\n')}
35
+ `))
36
+
37
+
38
+ export function buildSchema(doc: DocumentNode): GraphQLSchema {
39
+ let schema = extendSchema(baseSchema, doc)
40
+ let errors = validateSchema(schema).filter(err => !/query root/i.test(err.message))
41
+ if (errors.length > 0) {
42
+ throw errors[0]
43
+ }
44
+ return schema
45
+ }
46
+
47
+
48
+ export function buildModel(schema: GraphQLSchema): Model {
49
+ let types = schema.getTypeMap()
50
+ let model: Model = {}
51
+ for (let key in types) {
52
+ let type = types[key]
53
+ if (isEntityType(type)) {
54
+ addEntityOrJsonObjectOrInterface(model, type as GraphQLObjectType)
55
+ }
56
+ }
57
+ validateModel(model)
58
+ return model
59
+ }
60
+
61
+
62
+ function isEntityType(type: unknown): boolean {
63
+ return type instanceof GraphQLObjectType && !!type.astNode?.directives?.some(d => d.name.value == 'entity')
64
+ }
65
+
66
+
67
+ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType | GraphQLInterfaceType): void {
68
+ if (model[type.name]) return
69
+
70
+ let kind: 'entity' | 'object' | 'interface' = isEntityType(type)
71
+ ? 'entity'
72
+ : type instanceof GraphQLInterfaceType ? 'interface' : 'object'
73
+
74
+ let properties: Record<string, Prop> = {}
75
+ let interfaces: string[] = []
76
+ let indexes: Index[] = type instanceof GraphQLObjectType ? checkEntityIndexes(type) : []
77
+ let description = type.description || undefined
78
+
79
+ switch(kind) {
80
+ case 'entity':
81
+ model[type.name] = {kind, properties, description, interfaces, indexes}
82
+ break
83
+ case 'object':
84
+ model[type.name] = {kind, properties, description, interfaces}
85
+ break
86
+ case 'interface':
87
+ model[type.name] = {kind, properties, description}
88
+ break
89
+ default:
90
+ throw unexpectedCase(kind)
91
+ }
92
+
93
+ let fields = type.getFields()
94
+ if (kind == 'entity') {
95
+ if (fields.id == null) {
96
+ properties.id = {
97
+ type: {kind: 'scalar', name: 'ID'},
98
+ nullable: false
99
+ }
100
+ } else {
101
+ let correctIdType = fields.id.type instanceof GraphQLNonNull
102
+ && fields.id.type.ofType instanceof GraphQLScalarType
103
+ && fields.id.type.ofType.name === 'ID'
104
+ if (!correctIdType) {
105
+ throw unsupportedFieldTypeError(type.name + '.id')
106
+ }
107
+ }
108
+ }
109
+
110
+ for (let key in fields) {
111
+ let f: GraphQLField<any, any> = fields[key]
112
+
113
+ handleFulltextDirective(model, type, f)
114
+
115
+ let propName = `${type.name}.${f.name}`
116
+ let fieldType = f.type
117
+ let nullable = true
118
+ let description = f.description || undefined
119
+ let derivedFrom = checkDerivedFrom(type, f)
120
+ let index = checkFieldIndex(type, f)
121
+ let unique = index?.unique || false
122
+
123
+ if (index) {
124
+ indexes.push(index)
125
+ }
126
+
127
+ if (fieldType instanceof GraphQLNonNull) {
128
+ nullable = false
129
+ fieldType = fieldType.ofType
130
+ }
131
+
132
+ let list = unwrapList(fieldType)
133
+ fieldType = list.item
134
+
135
+ if (fieldType instanceof GraphQLScalarType) {
136
+ properties[key] = {
137
+ type: wrapWithList(list.nulls, {
138
+ kind: 'scalar',
139
+ name: fieldType.name
140
+ }),
141
+ nullable,
142
+ description
143
+ }
144
+ } else if (fieldType instanceof GraphQLEnumType) {
145
+ addEnum(model, fieldType)
146
+ properties[key] = {
147
+ type: wrapWithList(list.nulls, {
148
+ kind: 'enum',
149
+ name: fieldType.name
150
+ }),
151
+ nullable,
152
+ description
153
+ }
154
+ } else if (fieldType instanceof GraphQLUnionType) {
155
+ addUnion(model, fieldType)
156
+ properties[key] = {
157
+ type: wrapWithList(list.nulls, {
158
+ kind: 'union',
159
+ name: fieldType.name
160
+ }),
161
+ nullable,
162
+ description
163
+ }
164
+ } else if (fieldType instanceof GraphQLObjectType) {
165
+ if (isEntityType(fieldType)) {
166
+ switch(list.nulls.length) {
167
+ case 0:
168
+ if (derivedFrom) {
169
+ if (!nullable) {
170
+ throw new SchemaError(`Property ${propName} must be nullable`)
171
+ }
172
+ properties[key] = {
173
+ type: {
174
+ kind: 'lookup',
175
+ entity: fieldType.name,
176
+ field: derivedFrom.field
177
+ },
178
+ nullable,
179
+ description
180
+ }
181
+ } else {
182
+ if (unique && nullable) {
183
+ throw new SchemaError(`Unique property ${propName} must be non-nullable`)
184
+ }
185
+ properties[key] = {
186
+ type: {
187
+ kind: 'fk',
188
+ foreignEntity: fieldType.name
189
+ },
190
+ nullable,
191
+ unique,
192
+ description
193
+ }
194
+ }
195
+ break
196
+ case 1:
197
+ if (derivedFrom == null) {
198
+ throw new SchemaError(`@derivedFrom directive is required on ${propName} declaration`)
199
+ }
200
+ properties[key] = {
201
+ type: {
202
+ kind: 'list-lookup',
203
+ entity: fieldType.name,
204
+ field: derivedFrom.field
205
+ },
206
+ nullable: false,
207
+ description
208
+ }
209
+ break
210
+ default:
211
+ throw unsupportedFieldTypeError(propName)
212
+ }
213
+ } else {
214
+ addEntityOrJsonObjectOrInterface(model, fieldType)
215
+ properties[key] = {
216
+ type: wrapWithList(list.nulls, {
217
+ kind: 'object',
218
+ name: fieldType.name
219
+ }),
220
+ nullable,
221
+ description
222
+ }
223
+ }
224
+ } else {
225
+ throw unsupportedFieldTypeError(propName)
226
+ }
227
+ }
228
+
229
+ if (kind != 'interface') {
230
+ type.getInterfaces().forEach(i => {
231
+ addEntityOrJsonObjectOrInterface(model, i)
232
+ interfaces.push(i.name)
233
+ })
234
+ }
235
+ }
236
+
237
+
238
+ function addUnion(model: Model, type: GraphQLUnionType): void {
239
+ if (model[type.name]) return
240
+ let variants: string[] = []
241
+
242
+ model[type.name] = {
243
+ kind: 'union',
244
+ variants,
245
+ description: type.description || undefined
246
+ }
247
+
248
+ type.getTypes().forEach(obj => {
249
+ if (isEntityType(obj)) {
250
+ throw new Error(`union ${type.name} has entity ${obj.name} as a variant. Entities in union types are not supported`)
251
+ }
252
+ addEntityOrJsonObjectOrInterface(model, obj)
253
+ variants.push(obj.name)
254
+ })
255
+ }
256
+
257
+
258
+ function addEnum(model: Model, type: GraphQLEnumType): void {
259
+ if (model[type.name]) return
260
+ let values: Record<string, {}> = {}
261
+
262
+ model[type.name] = {
263
+ kind: 'enum',
264
+ values,
265
+ description: type.description || undefined
266
+ }
267
+
268
+ type.getValues().forEach(item => {
269
+ values[item.name] = {}
270
+ })
271
+ }
272
+
273
+
274
+ function handleFulltextDirective(model: Model, object: GraphQLNamedType, f: GraphQLField<any, any>): void {
275
+ f.astNode?.directives?.forEach(d => {
276
+ if (d.name.value != 'fulltext') return
277
+ if (!isEntityType(object) || !isStringField(f)) {
278
+ throw new Error(`@fulltext directive can be only applied to String entity fields, but was applied to ${object.name}.${f.name}`)
279
+ }
280
+ let queryArgument = d.arguments?.find(arg => arg.name.value == 'query')
281
+ assert(queryArgument != null)
282
+ assert(queryArgument.value.kind == 'StringValue')
283
+ let queryName = queryArgument.value.value
284
+ let query = model[queryName]
285
+ if (query == null) {
286
+ query = model[queryName] = {
287
+ kind: 'fts',
288
+ sources: []
289
+ }
290
+ }
291
+ assert(query.kind == 'fts')
292
+ let src = query.sources.find(s => s.entity == object.name)
293
+ if (src == null) {
294
+ query.sources.push({
295
+ entity: object.name,
296
+ fields: [f.name]
297
+ })
298
+ } else {
299
+ src.fields.push(f.name)
300
+ }
301
+ })
302
+ }
303
+
304
+
305
+ function isStringField(f: GraphQLField<any, any>): boolean {
306
+ return asScalarField(f)?.name == 'String'
307
+ }
308
+
309
+
310
+ function asScalarField(f: GraphQLField<any, any>): GraphQLScalarType | undefined {
311
+ let type = asNonNull(f)
312
+ return type instanceof GraphQLScalarType ? type : undefined
313
+ }
314
+
315
+
316
+ function asNonNull(f: GraphQLField<any, any>): GraphQLOutputType {
317
+ let type = f.type
318
+ if (type instanceof GraphQLNonNull) {
319
+ type = type.ofType
320
+ }
321
+ return type
322
+ }
323
+
324
+
325
+ function unwrapList(type: GraphQLOutputType): DeepList {
326
+ let nulls: boolean[] = []
327
+ while (type instanceof GraphQLList) {
328
+ type = type.ofType
329
+ if (type instanceof GraphQLNonNull) {
330
+ nulls.push(false)
331
+ type = type.ofType
332
+ } else {
333
+ nulls.push(true)
334
+ }
335
+ }
336
+ return {item: type, nulls}
337
+ }
338
+
339
+
340
+ interface DeepList {
341
+ item: GraphQLOutputType
342
+ nulls: boolean[]
343
+ }
344
+
345
+
346
+ function wrapWithList(nulls: boolean[], dataType: PropType): PropType {
347
+ if (nulls.length == 0) return dataType
348
+ return {
349
+ kind: 'list',
350
+ item: {
351
+ type: wrapWithList(nulls.slice(1), dataType),
352
+ nullable: nulls[0]
353
+ }
354
+ }
355
+ }
356
+
357
+
358
+ function checkFieldIndex(type: GraphQLNamedType, f: GraphQLField<any, any>): Index | undefined {
359
+ let unique = false
360
+ let index = false
361
+
362
+ f.astNode?.directives?.forEach(d => {
363
+ if (d.name.value == 'unique') {
364
+ assertCanBeIndexed(type, f)
365
+ index = true
366
+ unique = true
367
+ } else if (d.name.value == 'index') {
368
+ assertCanBeIndexed(type, f)
369
+ let fieldsArg = d.arguments?.find(arg => arg.name.value == 'fields')
370
+ if (fieldsArg) throw new SchemaError(
371
+ `@index(fields: ...) where applied to ${type.name}.${f.name}, but fields argument is not allowed when @index is applied to a field`
372
+ )
373
+ let uniqueArg = d.arguments?.find(arg => arg.name.value == 'unique')
374
+ if (uniqueArg) {
375
+ assert(uniqueArg.value.kind == 'BooleanValue')
376
+ unique = uniqueArg.value.value
377
+ }
378
+ index = true
379
+ }
380
+ })
381
+
382
+ if (!index) return undefined
383
+
384
+ return {
385
+ fields: [{name: f.name}],
386
+ unique
387
+ }
388
+ }
389
+
390
+
391
+ function assertCanBeIndexed(type: GraphQLNamedType, f: GraphQLField<any, any>): void {
392
+ if (!isEntityType(type)) throw new SchemaError(
393
+ `${type.name}.${f.name} can't be indexed, because ${type.name} is not an entity`
394
+ )
395
+ if (!canBeIndexed(f)) throw new SchemaError(
396
+ `${type.name}.${f.name} can't be indexed, it is not a scalar, enum or foreign key`
397
+ )
398
+ }
399
+
400
+
401
+ function canBeIndexed(f: GraphQLField<any, any>): boolean {
402
+ let type = asNonNull(f)
403
+ if (type instanceof GraphQLScalarType || type instanceof GraphQLEnumType) return true
404
+ return isEntityType(type) && !f.astNode?.directives?.some(d => d.name.value == 'derivedFrom')
405
+ }
406
+
407
+
408
+ function checkEntityIndexes(type: GraphQLObjectType): Index[] {
409
+ let indexes: Index[] = []
410
+ type.astNode?.directives?.forEach(d => {
411
+ if (d.name.value != 'index') return
412
+ if (!isEntityType(type)) throw new SchemaError(
413
+ `@index was applied to ${type.name}, but only entities can have indexes`
414
+ )
415
+ let fieldsArg = d.arguments?.find(arg => arg.name.value == 'fields')
416
+ if (fieldsArg == null) throw new SchemaError(
417
+ `@index was applied to ${type.name}, but no fields were specified`
418
+ )
419
+ assert(fieldsArg.value.kind == 'ListValue')
420
+ if (fieldsArg.value.values.length == 0) throw new SchemaError(
421
+ `@index was applied to ${type.name}, but no fields were specified`
422
+ )
423
+ let fields = fieldsArg.value.values.map(arg => {
424
+ assert(arg.kind == 'StringValue')
425
+ let name = arg.value
426
+ let f = type.getFields()[name]
427
+ if (f == null) throw new SchemaError(
428
+ `Entity ${type.name} doesn't have a field '${name}', but it is a part of @index`
429
+ )
430
+ assertCanBeIndexed(type, f)
431
+ return {name}
432
+ })
433
+ indexes.push({
434
+ fields,
435
+ unique: !!d.arguments?.find(arg => arg.name.value == 'unique')?.value
436
+ })
437
+ })
438
+ return indexes
439
+ }
440
+
441
+
442
+ function checkDerivedFrom(type: GraphQLNamedType, f: GraphQLField<any, any>): {field: string} | undefined {
443
+ let directives = f.astNode?.directives?.filter(d => d.name.value == 'derivedFrom') || []
444
+ if (directives.length == 0) return undefined
445
+ if (!isEntityType(type)) throw new SchemaError(
446
+ `@derivedFrom where applied to ${type.name}.${f.name}, but only entities can have lookup fields`
447
+ )
448
+ if (directives.length > 1) throw new SchemaError(
449
+ `Multiple @derivedFrom where applied to ${type.name}.${f.name}`
450
+ )
451
+ let d = directives[0]
452
+ let fieldArg = assertNotNull(d.arguments?.find(arg => arg.name.value == 'field'))
453
+ assert(fieldArg.value.kind == 'StringValue')
454
+ return {field: fieldArg.value.value}
455
+ }
456
+
457
+
458
+ function unsupportedFieldTypeError(propName: string): Error {
459
+ return new SchemaError(`Property ${propName} has unsupported type`)
460
+ }
461
+
462
+
463
+ export class SchemaError extends Error {}
package/src/main.ts ADDED
@@ -0,0 +1,51 @@
1
+ import {Pool} from "pg"
2
+ import {createPoolConfig} from "./db"
3
+ import {serve} from "./server"
4
+ import {loadModel} from "./tools"
5
+
6
+
7
+ module.exports = function main() {
8
+ let args = process.argv.slice(2)
9
+
10
+ if (args.indexOf('--help') >= 0) {
11
+ help()
12
+ process.exit(1)
13
+ }
14
+
15
+ if (args.length != 1) {
16
+ help()
17
+ process.exit(1)
18
+ }
19
+
20
+ let model = loadModel(args[0])
21
+ let db = new Pool(createPoolConfig())
22
+ let port = process.env.GRAPHQL_SERVER_PORT || 3000
23
+
24
+ serve({model, db, port}).then(
25
+ () => {
26
+ console.log('OpenReader is listening on port ' + port)
27
+ },
28
+ err => {
29
+ console.error(err)
30
+ process.exit(1)
31
+ }
32
+ )
33
+ }
34
+
35
+
36
+ function help() {
37
+ console.error(`
38
+ Usage: openreader SCHEMA
39
+
40
+ OpenCRUD GraphQL server.
41
+
42
+ Can be configured using the following environment variables:
43
+
44
+ DB_NAME
45
+ DB_USER
46
+ DB_PASS
47
+ DB_HOST
48
+ DB_PORT
49
+ GRAPHQL_SERVER_PORT
50
+ `)
51
+ }
@@ -0,0 +1,201 @@
1
+ import assert from "assert"
2
+ import {Entity, FTS_Query, JsonObject, Model, Prop, PropType} from "./model"
3
+
4
+
5
+ const UNION_MAPS = new WeakMap<Model, Record<string, JsonObject>>()
6
+
7
+
8
+ export function getUnionProps(model: Model, unionName: string): JsonObject {
9
+ let map = UNION_MAPS.get(model)
10
+ if (map == null) {
11
+ map = {}
12
+ UNION_MAPS.set(model, map)
13
+ }
14
+ if (map[unionName]) return map[unionName]
15
+ return map[unionName] = buildUnionProps(model, unionName)
16
+ }
17
+
18
+
19
+ export function buildUnionProps(model: Model, unionName: string): JsonObject {
20
+ let union = model[unionName]
21
+ assert(union.kind == 'union')
22
+ let properties: Record<string, Prop> = {}
23
+ for (let i = 0; i < union.variants.length; i++) {
24
+ let objectName = union.variants[i]
25
+ let object = model[objectName]
26
+ assert(object.kind == 'object')
27
+ Object.assign(properties, object.properties)
28
+ }
29
+ properties.isTypeOf = {
30
+ type: {kind: 'scalar', name: 'String'},
31
+ nullable: false
32
+ }
33
+ return {kind: 'object', properties}
34
+ }
35
+
36
+
37
+ export function validateModel(model: Model) {
38
+ // TODO: check all invariants we assume
39
+ validateNames(model)
40
+ validateUnionTypes(model)
41
+ validateLookups(model)
42
+ validateIndexes(model)
43
+ }
44
+
45
+
46
+ const TYPE_NAME_REGEX = /^[A-Z][a-zA-Z0-9]*$/
47
+ const PROP_NAME_REGEX = /^[a-z][a-zA-Z0-9]*$/
48
+
49
+
50
+ export function validateNames(model: Model) {
51
+ for (let name in model) {
52
+ let item = model[name]
53
+ if (item.kind == 'fts') {
54
+ if (!PROP_NAME_REGEX.test(name)) {
55
+ throw new Error(`Invalid fulltext search name: ${name}. It must match ${PROP_NAME_REGEX}.`)
56
+ }
57
+ } else {
58
+ if (!TYPE_NAME_REGEX.test(name)) {
59
+ throw new Error(`Invalid ${item.kind} name: ${name}. It must match ${TYPE_NAME_REGEX}`)
60
+ }
61
+ }
62
+ switch(item.kind) {
63
+ case 'entity':
64
+ case 'object':
65
+ case 'interface':
66
+ for (let prop in item.properties) {
67
+ if (!PROP_NAME_REGEX.test(prop)) {
68
+ throw new Error(`Type ${name} has a property with invalid name: ${prop}. It must match ${PROP_NAME_REGEX}.`)
69
+ }
70
+ }
71
+ break
72
+ }
73
+ }
74
+ }
75
+
76
+
77
+ export function validateUnionTypes(model: Model): void {
78
+ for (let key in model) {
79
+ let item = model[key]
80
+ if (item.kind != 'union') continue
81
+ let properties: Record<string, { objectName: string, type: PropType }> = {}
82
+ item.variants.forEach(objectName => {
83
+ let object = model[objectName]
84
+ assert(object.kind == 'object')
85
+ for (let propName in object.properties) {
86
+ let rec = properties[propName]
87
+ if (rec && !propTypeEquals(rec.type, object.properties[propName].type)) {
88
+ throw new Error(
89
+ `${rec.objectName} and ${objectName} variants of union ${key} both have property '${propName}', but types of ${rec.objectName}.${propName} and ${objectName}.${propName} are different.`
90
+ )
91
+ } else {
92
+ properties[propName] = {objectName, type: object.properties[propName].type}
93
+ }
94
+ }
95
+ })
96
+ }
97
+ }
98
+
99
+
100
+ export function validateLookups(model: Model): void {
101
+ for (let name in model) {
102
+ let item = model[name]
103
+ switch(item.kind) {
104
+ case 'object':
105
+ case 'interface':
106
+ for (let key in item.properties) {
107
+ let prop = item.properties[key]
108
+ if (prop.type.kind == 'lookup' || prop.type.kind == 'list-lookup') {
109
+ throw invalidProperty(name, key, `lookups are only supported on entity types`)
110
+ }
111
+ }
112
+ break
113
+ case 'entity':
114
+ for (let key in item.properties) {
115
+ let prop = item.properties[key]
116
+ if (prop.type.kind == 'lookup' && !prop.nullable) {
117
+ throw invalidProperty(name, key, 'one-to-one lookups must be nullable')
118
+ }
119
+ if (prop.type.kind == 'lookup' || prop.type.kind == 'list-lookup') {
120
+ let lookupEntity = getEntity(model, prop.type.entity)
121
+ let lookupProperty = lookupEntity.properties[prop.type.field]
122
+ if (lookupProperty?.type.kind != 'fk' || lookupProperty.type.foreignEntity != name) {
123
+ throw invalidProperty(name, key, `${prop.type.entity}.${prop.type.field} is not a foreign key pointing to ${name}`)
124
+ }
125
+ if (prop.type.kind == 'lookup' && !lookupProperty.unique) {
126
+ throw invalidProperty(name, key, `${prop.type.entity}.${prop.type.field} is not @unique`)
127
+ }
128
+ }
129
+ }
130
+ break
131
+ }
132
+ }
133
+ }
134
+
135
+
136
+ export function validateIndexes(model: Model): void {
137
+ for (let name in model) {
138
+ const item = model[name]
139
+ if (item.kind != 'entity') continue
140
+ item.indexes?.forEach(index => {
141
+ if (index.fields.length == 0) throw new Error(`Entity ${name} has an index without fields`)
142
+ index.fields.forEach(f => {
143
+ let prop = item.properties[f.name]
144
+ if (prop == null) throw new Error(
145
+ `Entity ${name} doesn't have a property ${f.name}, but is a part of index`
146
+ )
147
+ switch(prop.type.kind) {
148
+ case "scalar":
149
+ case "enum":
150
+ case "fk":
151
+ break
152
+ default:
153
+ throw new Error(
154
+ `Property ${name}.${f.name} can't be a part of index`
155
+ )
156
+ }
157
+ })
158
+ })
159
+ }
160
+ }
161
+
162
+
163
+ function invalidProperty(item: string, key: string, msg: string): Error {
164
+ return new Error(`Invalid property ${item}.${key}: ${msg}`)
165
+ }
166
+
167
+
168
+ export function propTypeEquals(a: PropType, b: PropType): boolean {
169
+ if (a.kind != b.kind) return false
170
+ if (a.kind == 'list') return propTypeEquals(a.item.type, (b as typeof a).item.type)
171
+ switch(a.kind) {
172
+ case 'fk':
173
+ return a.foreignEntity == (b as typeof a).foreignEntity
174
+ case 'lookup':
175
+ case 'list-lookup':
176
+ return a.entity == (b as typeof a).entity && a.field == (b as typeof a).field
177
+ default:
178
+ return a.name == (b as typeof a).name
179
+ }
180
+ }
181
+
182
+
183
+ export function getEntity(model: Model, name: string): Entity {
184
+ let entity = model[name]
185
+ assert(entity.kind == 'entity', `${name} expected to be an entity`)
186
+ return entity
187
+ }
188
+
189
+
190
+ export function getObject(model: Model, name: string): JsonObject {
191
+ let object = model[name]
192
+ assert(object.kind == 'object', `${name} expected to be an object`)
193
+ return object
194
+ }
195
+
196
+
197
+ export function getFtsQuery(model: Model, name: string): FTS_Query {
198
+ let query = model[name]
199
+ assert(query.kind == 'fts', `${name} expected to be FTS query`)
200
+ return query
201
+ }