@subsquid/openreader 0.3.2 → 0.3.3

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 (104) hide show
  1. package/dist/db.d.ts +1 -0
  2. package/dist/db.d.ts.map +1 -0
  3. package/dist/gql/opencrud.d.ts +1 -0
  4. package/dist/gql/opencrud.d.ts.map +1 -0
  5. package/dist/gql/schema.d.ts +1 -0
  6. package/dist/gql/schema.d.ts.map +1 -0
  7. package/dist/main.d.ts +1 -0
  8. package/dist/main.d.ts.map +1 -0
  9. package/dist/model.d.ts +1 -0
  10. package/dist/model.d.ts.map +1 -0
  11. package/dist/model.tools.d.ts +1 -0
  12. package/dist/model.tools.d.ts.map +1 -0
  13. package/dist/orderBy.d.ts +1 -0
  14. package/dist/orderBy.d.ts.map +1 -0
  15. package/dist/queryBuilder.d.ts +1 -0
  16. package/dist/queryBuilder.d.ts.map +1 -0
  17. package/dist/relayConnection.d.ts +1 -0
  18. package/dist/relayConnection.d.ts.map +1 -0
  19. package/dist/requestedFields.d.ts +1 -0
  20. package/dist/requestedFields.d.ts.map +1 -0
  21. package/dist/resolver.d.ts +1 -0
  22. package/dist/resolver.d.ts.map +1 -0
  23. package/dist/scalars.d.ts +1 -0
  24. package/dist/scalars.d.ts.map +1 -0
  25. package/dist/server.d.ts +1 -0
  26. package/dist/server.d.ts.map +1 -0
  27. package/dist/test/basic.test.d.ts +2 -0
  28. package/dist/test/basic.test.d.ts.map +1 -0
  29. package/dist/test/basic.test.js +286 -0
  30. package/dist/test/basic.test.js.map +1 -0
  31. package/dist/test/connection.test.d.ts +2 -0
  32. package/dist/test/connection.test.d.ts.map +1 -0
  33. package/dist/test/connection.test.js +193 -0
  34. package/dist/test/connection.test.js.map +1 -0
  35. package/dist/test/fts.test.d.ts +2 -0
  36. package/dist/test/fts.test.d.ts.map +1 -0
  37. package/dist/test/fts.test.js +110 -0
  38. package/dist/test/fts.test.js.map +1 -0
  39. package/dist/test/lists.test.d.ts +2 -0
  40. package/dist/test/lists.test.d.ts.map +1 -0
  41. package/dist/test/lists.test.js +266 -0
  42. package/dist/test/lists.test.js.map +1 -0
  43. package/dist/test/lookup.test.d.ts +2 -0
  44. package/dist/test/lookup.test.d.ts.map +1 -0
  45. package/dist/test/lookup.test.js +109 -0
  46. package/dist/test/lookup.test.js.map +1 -0
  47. package/dist/test/scalars.test.d.ts +2 -0
  48. package/dist/test/scalars.test.d.ts.map +1 -0
  49. package/dist/test/scalars.test.js +303 -0
  50. package/dist/test/scalars.test.js.map +1 -0
  51. package/dist/test/tools.test.d.ts +2 -0
  52. package/dist/test/tools.test.d.ts.map +1 -0
  53. package/dist/test/tools.test.js +49 -0
  54. package/dist/test/tools.test.js.map +1 -0
  55. package/dist/test/typed-json.test.d.ts +2 -0
  56. package/dist/test/typed-json.test.d.ts.map +1 -0
  57. package/dist/test/typed-json.test.js +75 -0
  58. package/dist/test/typed-json.test.js.map +1 -0
  59. package/dist/test/unions.test.d.ts +2 -0
  60. package/dist/test/unions.test.d.ts.map +1 -0
  61. package/dist/test/unions.test.js +84 -0
  62. package/dist/test/unions.test.js.map +1 -0
  63. package/dist/test/util/setup.d.ts +7 -0
  64. package/dist/test/util/setup.d.ts.map +1 -0
  65. package/dist/test/util/setup.js +60 -0
  66. package/dist/test/util/setup.js.map +1 -0
  67. package/dist/test/where.test.d.ts +2 -0
  68. package/dist/test/where.test.d.ts.map +1 -0
  69. package/dist/test/where.test.js +127 -0
  70. package/dist/test/where.test.js.map +1 -0
  71. package/dist/tools.d.ts +1 -0
  72. package/dist/tools.d.ts.map +1 -0
  73. package/dist/util.d.ts +1 -0
  74. package/dist/util.d.ts.map +1 -0
  75. package/dist/where.d.ts +1 -0
  76. package/dist/where.d.ts.map +1 -0
  77. package/package.json +4 -3
  78. package/src/db.ts +83 -0
  79. package/src/gql/opencrud.ts +328 -0
  80. package/src/gql/schema.ts +337 -0
  81. package/src/main.ts +51 -0
  82. package/src/model.tools.ts +173 -0
  83. package/src/model.ts +125 -0
  84. package/src/orderBy.ts +105 -0
  85. package/src/queryBuilder.ts +785 -0
  86. package/src/relayConnection.ts +80 -0
  87. package/src/requestedFields.ts +246 -0
  88. package/src/resolver.ts +199 -0
  89. package/src/scalars.ts +247 -0
  90. package/src/server.ts +115 -0
  91. package/src/test/basic.test.ts +339 -0
  92. package/src/test/connection.test.ts +195 -0
  93. package/src/test/fts.test.ts +114 -0
  94. package/src/test/lists.test.ts +278 -0
  95. package/src/test/lookup.test.ts +111 -0
  96. package/src/test/scalars.test.ts +316 -0
  97. package/src/test/tools.test.ts +27 -0
  98. package/src/test/typed-json.test.ts +76 -0
  99. package/src/test/unions.test.ts +85 -0
  100. package/src/test/util/setup.ts +63 -0
  101. package/src/test/where.test.ts +135 -0
  102. package/src/tools.ts +33 -0
  103. package/src/util.ts +39 -0
  104. package/src/where.ts +110 -0
@@ -0,0 +1,337 @@
1
+ import assert from "assert"
2
+ import {
3
+ buildASTSchema,
4
+ DocumentNode,
5
+ extendSchema,
6
+ GraphQLEnumType,
7
+ GraphQLField,
8
+ GraphQLInterfaceType,
9
+ GraphQLList,
10
+ GraphQLNamedType,
11
+ GraphQLNonNull,
12
+ GraphQLObjectType,
13
+ GraphQLOutputType,
14
+ GraphQLScalarType,
15
+ GraphQLSchema,
16
+ GraphQLUnionType,
17
+ parse,
18
+ validateSchema
19
+ } from "graphql"
20
+ import {Model, Prop, PropType} from "../model"
21
+ import {validateModel} from "../model.tools"
22
+ import {scalars_list} from "../scalars"
23
+
24
+
25
+ const baseSchema = buildASTSchema(parse(`
26
+ directive @entity on OBJECT
27
+ directive @derivedFrom(field: String!) on FIELD_DEFINITION
28
+ directive @unique on FIELD_DEFINITION
29
+ directive @fulltext(query: String!) on FIELD_DEFINITION
30
+ directive @variant on OBJECT # legacy
31
+ directive @jsonField on OBJECT # legacy
32
+ ${scalars_list.map(name => 'scalar ' + name).join('\n')}
33
+ `))
34
+
35
+
36
+ export function buildSchema(doc: DocumentNode): GraphQLSchema {
37
+ let schema = extendSchema(baseSchema, doc)
38
+ let errors = validateSchema(schema).filter(err => !/query root/i.test(err.message))
39
+ if (errors.length > 0) {
40
+ throw errors[0]
41
+ }
42
+ return schema
43
+ }
44
+
45
+
46
+ export function buildModel(schema: GraphQLSchema): Model {
47
+ let types = schema.getTypeMap()
48
+ let model: Model = {}
49
+ for (let key in types) {
50
+ let type = types[key]
51
+ if (isEntityType(type)) {
52
+ addEntityOrJsonObjectOrInterface(model, type as GraphQLObjectType)
53
+ }
54
+ }
55
+ validateModel(model)
56
+ return model
57
+ }
58
+
59
+
60
+ function isEntityType(type: GraphQLNamedType): boolean {
61
+ return type instanceof GraphQLObjectType && !!type.astNode?.directives?.some(d => d.name.value == 'entity')
62
+ }
63
+
64
+
65
+ function addEntityOrJsonObjectOrInterface(model: Model, type: GraphQLObjectType | GraphQLInterfaceType): void {
66
+ if (model[type.name]) return
67
+
68
+ let kind: 'entity' | 'object' | 'interface' = isEntityType(type)
69
+ ? 'entity'
70
+ : type instanceof GraphQLInterfaceType ? 'interface' : 'object'
71
+
72
+ let properties: Record<string, Prop> = {}
73
+ let interfaces: string[] = []
74
+ let description = type.description || undefined
75
+
76
+ if (kind != 'interface') {
77
+ model[type.name] = {kind, properties, interfaces, description}
78
+ } else {
79
+ model[type.name] = {kind, properties, description}
80
+ }
81
+
82
+ let fields = type.getFields()
83
+ if (kind == 'entity') {
84
+ if (fields.id == null) {
85
+ properties.id = {
86
+ type: {kind: 'scalar', name: 'ID'},
87
+ nullable: false
88
+ }
89
+ } else {
90
+ let correctIdType = fields.id.type instanceof GraphQLNonNull
91
+ && fields.id.type.ofType instanceof GraphQLScalarType
92
+ && fields.id.type.ofType.name === 'ID'
93
+ if (!correctIdType) {
94
+ throw unsupportedFieldTypeError(type.name + '.id')
95
+ }
96
+ }
97
+ }
98
+
99
+ for (let key in fields) {
100
+ let f: GraphQLField<any, any> = fields[key]
101
+ let fieldType = f.type
102
+ let nullable = true
103
+ let description = f.description || undefined
104
+ let propName = `${type.name}.${f.name}`
105
+ let unique = f.astNode?.directives?.some(d => d.name.value == 'unique')
106
+ handleFulltextDirective(model, type, f)
107
+ if (fieldType instanceof GraphQLNonNull) {
108
+ nullable = false
109
+ fieldType = fieldType.ofType
110
+ }
111
+ let list = unwrapList(fieldType)
112
+ fieldType = list.item
113
+ if (fieldType instanceof GraphQLScalarType) {
114
+ properties[key] = {
115
+ type: wrapWithList(list.nulls, {
116
+ kind: 'scalar',
117
+ name: fieldType.name
118
+ }),
119
+ nullable,
120
+ description
121
+ }
122
+ } else if (fieldType instanceof GraphQLEnumType) {
123
+ addEnum(model, fieldType)
124
+ properties[key] = {
125
+ type: wrapWithList(list.nulls, {
126
+ kind: 'enum',
127
+ name: fieldType.name
128
+ }),
129
+ nullable,
130
+ description
131
+ }
132
+ } else if (fieldType instanceof GraphQLUnionType) {
133
+ addUnion(model, fieldType)
134
+ properties[key] = {
135
+ type: wrapWithList(list.nulls, {
136
+ kind: 'union',
137
+ name: fieldType.name
138
+ }),
139
+ nullable,
140
+ description
141
+ }
142
+ } else if (fieldType instanceof GraphQLObjectType) {
143
+ if (isEntityType(fieldType)) {
144
+ let derivedFrom = f.astNode?.directives?.filter(d => d.name.value == 'derivedFrom').map(d => {
145
+ let valueNode = d.arguments?.[0].value
146
+ assert(valueNode != null)
147
+ assert(valueNode.kind == 'StringValue')
148
+ return valueNode.value
149
+ })[0]
150
+
151
+ switch(list.nulls.length) {
152
+ case 0:
153
+ if (derivedFrom) {
154
+ if (!nullable) {
155
+ throw new SchemaError(`Property ${propName} must be nullable`)
156
+ }
157
+ properties[key] = {
158
+ type: {
159
+ kind: 'lookup',
160
+ entity: fieldType.name,
161
+ field: derivedFrom
162
+ },
163
+ nullable,
164
+ description
165
+ }
166
+ } else {
167
+ if (unique && nullable) {
168
+ throw new SchemaError(`Unique property ${propName} must be non-nullable`)
169
+ }
170
+ properties[key] = {
171
+ type: {
172
+ kind: 'fk',
173
+ foreignEntity: fieldType.name
174
+ },
175
+ nullable,
176
+ unique,
177
+ description
178
+ }
179
+ }
180
+ break
181
+ case 1:
182
+ if (derivedFrom == null) {
183
+ throw new SchemaError(`@derivedFrom directive is required on ${propName} declaration`)
184
+ }
185
+ properties[key] = {
186
+ type: {
187
+ kind: 'list-lookup',
188
+ entity: fieldType.name,
189
+ field: derivedFrom
190
+ },
191
+ nullable: false,
192
+ description
193
+ }
194
+ break
195
+ default:
196
+ throw unsupportedFieldTypeError(propName)
197
+ }
198
+ } else {
199
+ addEntityOrJsonObjectOrInterface(model, fieldType)
200
+ properties[key] = {
201
+ type: wrapWithList(list.nulls, {
202
+ kind: 'object',
203
+ name: fieldType.name
204
+ }),
205
+ nullable,
206
+ description
207
+ }
208
+ }
209
+ } else {
210
+ throw unsupportedFieldTypeError(propName)
211
+ }
212
+ }
213
+
214
+ if (kind != 'interface') {
215
+ type.getInterfaces().forEach(i => {
216
+ addEntityOrJsonObjectOrInterface(model, i)
217
+ interfaces.push(i.name)
218
+ })
219
+ }
220
+ }
221
+
222
+
223
+ function addUnion(model: Model, type: GraphQLUnionType): void {
224
+ if (model[type.name]) return
225
+ let variants: string[] = []
226
+
227
+ model[type.name] = {
228
+ kind: 'union',
229
+ variants,
230
+ description: type.description || undefined
231
+ }
232
+
233
+ type.getTypes().forEach(obj => {
234
+ if (isEntityType(obj)) {
235
+ throw new Error(`union ${type.name} has entity ${obj.name} as a variant. Entities in union types are not supported`)
236
+ }
237
+ addEntityOrJsonObjectOrInterface(model, obj)
238
+ variants.push(obj.name)
239
+ })
240
+ }
241
+
242
+
243
+ function addEnum(model: Model, type: GraphQLEnumType): void {
244
+ if (model[type.name]) return
245
+ let values: Record<string, {}> = {}
246
+
247
+ model[type.name] = {
248
+ kind: 'enum',
249
+ values,
250
+ description: type.description || undefined
251
+ }
252
+
253
+ type.getValues().forEach(item => {
254
+ values[item.name] = {}
255
+ })
256
+ }
257
+
258
+
259
+ function handleFulltextDirective(model: Model, object: GraphQLNamedType, f: GraphQLField<any, any>): void {
260
+ f.astNode?.directives?.forEach(d => {
261
+ if (d.name.value != 'fulltext') return
262
+ if (!isEntityType(object) || !isStringField(f)) {
263
+ throw new Error(`@fulltext directive can be only applied to String entity fields, but was applied to ${object.name}.${f.name}`)
264
+ }
265
+ let queryArgument = d.arguments?.find(arg => arg.name.value == 'query')
266
+ assert(queryArgument != null)
267
+ assert(queryArgument.value.kind == 'StringValue')
268
+ let queryName = queryArgument.value.value
269
+ let query = model[queryName]
270
+ if (query == null) {
271
+ query = model[queryName] = {
272
+ kind: 'fts',
273
+ sources: []
274
+ }
275
+ }
276
+ assert(query.kind == 'fts')
277
+ let src = query.sources.find(s => s.entity == object.name)
278
+ if (src == null) {
279
+ query.sources.push({
280
+ entity: object.name,
281
+ fields: [f.name]
282
+ })
283
+ } else {
284
+ src.fields.push(f.name)
285
+ }
286
+ })
287
+ }
288
+
289
+
290
+ function isStringField(f: GraphQLField<any, any>): boolean {
291
+ let type = f.type
292
+ if (type instanceof GraphQLNonNull) {
293
+ type = type.ofType
294
+ }
295
+ return type instanceof GraphQLScalarType && type.name == 'String'
296
+ }
297
+
298
+
299
+ function unwrapList(type: GraphQLOutputType): DeepList {
300
+ let nulls: boolean[] = []
301
+ while (type instanceof GraphQLList) {
302
+ type = type.ofType
303
+ if (type instanceof GraphQLNonNull) {
304
+ nulls.push(false)
305
+ type = type.ofType
306
+ } else {
307
+ nulls.push(true)
308
+ }
309
+ }
310
+ return {item: type, nulls}
311
+ }
312
+
313
+
314
+ interface DeepList {
315
+ item: GraphQLOutputType
316
+ nulls: boolean[]
317
+ }
318
+
319
+
320
+ function wrapWithList(nulls: boolean[], dataType: PropType): PropType {
321
+ if (nulls.length == 0) return dataType
322
+ return {
323
+ kind: 'list',
324
+ item: {
325
+ type: wrapWithList(nulls.slice(1), dataType),
326
+ nullable: nulls[0]
327
+ }
328
+ }
329
+ }
330
+
331
+
332
+ function unsupportedFieldTypeError(propName: string): Error {
333
+ return new SchemaError(`Property ${propName} has unsupported type`)
334
+ }
335
+
336
+
337
+ 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,173 @@
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
+ }
43
+
44
+
45
+ const TYPE_NAME_REGEX = /^[A-Z][a-zA-Z0-9]*$/
46
+ const PROP_NAME_REGEX = /^[a-z][a-zA-Z0-9]*$/
47
+
48
+
49
+ export function validateNames(model: Model) {
50
+ for (let name in model) {
51
+ let item = model[name]
52
+ if (item.kind == 'fts') {
53
+ if (!PROP_NAME_REGEX.test(name)) {
54
+ throw new Error(`Invalid fulltext search name: ${name}. It must match ${PROP_NAME_REGEX}.`)
55
+ }
56
+ } else {
57
+ if (!TYPE_NAME_REGEX.test(name)) {
58
+ throw new Error(`Invalid ${item.kind} name: ${name}. It must match ${TYPE_NAME_REGEX}`)
59
+ }
60
+ }
61
+ switch(item.kind) {
62
+ case 'entity':
63
+ case 'object':
64
+ case 'interface':
65
+ for (let prop in item.properties) {
66
+ if (!PROP_NAME_REGEX.test(prop)) {
67
+ throw new Error(`Type ${name} has a property with invalid name: ${prop}. It must match ${PROP_NAME_REGEX}.`)
68
+ }
69
+ }
70
+ break
71
+ }
72
+ }
73
+ }
74
+
75
+
76
+ export function validateUnionTypes(model: Model): void {
77
+ for (let key in model) {
78
+ let item = model[key]
79
+ if (item.kind != 'union') continue
80
+ let properties: Record<string, { objectName: string, type: PropType }> = {}
81
+ item.variants.forEach(objectName => {
82
+ let object = model[objectName]
83
+ assert(object.kind == 'object')
84
+ for (let propName in object.properties) {
85
+ let rec = properties[propName]
86
+ if (rec && !propTypeEquals(rec.type, object.properties[propName].type)) {
87
+ throw new Error(
88
+ `${rec.objectName} and ${objectName} variants of union ${key} both have property '${propName}', but types of ${rec.objectName}.${propName} and ${objectName}.${propName} are different.`
89
+ )
90
+ } else {
91
+ properties[propName] = {objectName, type: object.properties[propName].type}
92
+ }
93
+ }
94
+ })
95
+ }
96
+ }
97
+
98
+
99
+ export function validateLookups(model: Model): void {
100
+ for (let name in model) {
101
+ let item = model[name]
102
+ switch(item.kind) {
103
+ case 'object':
104
+ case 'interface':
105
+ for (let key in item.properties) {
106
+ let prop = item.properties[key]
107
+ if (prop.type.kind == 'lookup' || prop.type.kind == 'list-lookup') {
108
+ throw invalidProperty(name, key, `lookups are only supported on entity types`)
109
+ }
110
+ }
111
+ break
112
+ case 'entity':
113
+ for (let key in item.properties) {
114
+ let prop = item.properties[key]
115
+ if (prop.type.kind == 'lookup' && !prop.nullable) {
116
+ throw invalidProperty(name, key, 'one-to-one lookups must be nullable')
117
+ }
118
+ if (prop.type.kind == 'lookup' || prop.type.kind == 'list-lookup') {
119
+ let lookupEntity = getEntity(model, prop.type.entity)
120
+ let lookupProperty = lookupEntity.properties[prop.type.field]
121
+ if (lookupProperty?.type.kind != 'fk' || lookupProperty.type.foreignEntity != name) {
122
+ throw invalidProperty(name, key, `${prop.type.entity}.${prop.type.field} is not a foreign key pointing to ${name}`)
123
+ }
124
+ if (prop.type.kind == 'lookup' && !lookupProperty.unique) {
125
+ throw invalidProperty(name, key, `${prop.type.entity}.${prop.type.field} is not @unique`)
126
+ }
127
+ }
128
+ }
129
+ break
130
+ }
131
+ }
132
+ }
133
+
134
+
135
+ function invalidProperty(item: string, key: string, msg: string): Error {
136
+ return new Error(`Invalid property ${item}.${key}: ${msg}`)
137
+ }
138
+
139
+
140
+ export function propTypeEquals(a: PropType, b: PropType): boolean {
141
+ if (a.kind != b.kind) return false
142
+ if (a.kind == 'list') return propTypeEquals(a.item.type, (b as typeof a).item.type)
143
+ switch(a.kind) {
144
+ case 'fk':
145
+ return a.foreignEntity == (b as typeof a).foreignEntity
146
+ case 'lookup':
147
+ case 'list-lookup':
148
+ return a.entity == (b as typeof a).entity && a.field == (b as typeof a).field
149
+ default:
150
+ return a.name == (b as typeof a).name
151
+ }
152
+ }
153
+
154
+
155
+ export function getEntity(model: Model, name: string): Entity {
156
+ let entity = model[name]
157
+ assert(entity.kind == 'entity', `${name} expected to be an entity`)
158
+ return entity
159
+ }
160
+
161
+
162
+ export function getObject(model: Model, name: string): JsonObject {
163
+ let object = model[name]
164
+ assert(object.kind == 'object', `${name} expected to be an object`)
165
+ return object
166
+ }
167
+
168
+
169
+ export function getFtsQuery(model: Model, name: string): FTS_Query {
170
+ let query = model[name]
171
+ assert(query.kind == 'fts', `${name} expected to be FTS query`)
172
+ return query
173
+ }
package/src/model.ts ADDED
@@ -0,0 +1,125 @@
1
+ export type Name = string
2
+
3
+
4
+ export type Model = Record<Name, Entity | JsonObject | Interface | Union | Enum | FTS_Query>
5
+
6
+
7
+ export interface Entity extends TypeMeta {
8
+ kind: 'entity'
9
+ properties: Record<Name, Prop>
10
+ interfaces?: Name[]
11
+ }
12
+
13
+
14
+ export interface JsonObject extends TypeMeta {
15
+ kind: 'object'
16
+ properties: Record<Name, Prop>
17
+ interfaces?: Name[]
18
+ }
19
+
20
+
21
+ export interface Interface extends TypeMeta {
22
+ kind: 'interface'
23
+ properties: Record<Name, Prop>
24
+ }
25
+
26
+
27
+ export interface Union extends TypeMeta {
28
+ kind: 'union'
29
+ variants: Name[]
30
+ }
31
+
32
+
33
+ export interface Enum extends TypeMeta {
34
+ kind: 'enum'
35
+ values: Record<string, {}>
36
+ }
37
+
38
+
39
+ export interface TypeMeta {
40
+ description?: string
41
+ }
42
+
43
+
44
+ export interface Prop {
45
+ type: PropType
46
+ nullable: boolean
47
+ description?: string
48
+ /**
49
+ * Whether the values in the column must be unique. Applicable only to entities.
50
+ */
51
+ unique?: boolean
52
+ }
53
+
54
+
55
+ export type PropType =
56
+ ScalarPropType |
57
+ EnumPropType |
58
+ ListPropType |
59
+ ObjectPropType |
60
+ UnionPropType |
61
+ FkPropType |
62
+ LookupPropType |
63
+ ListLookupPropType
64
+
65
+
66
+ export interface ScalarPropType {
67
+ kind: 'scalar'
68
+ name: Name
69
+ }
70
+
71
+
72
+ export interface EnumPropType {
73
+ kind: 'enum'
74
+ name: Name
75
+ }
76
+
77
+
78
+ export interface ObjectPropType {
79
+ kind: 'object'
80
+ name: Name
81
+ }
82
+
83
+
84
+ export interface UnionPropType {
85
+ kind: 'union'
86
+ name: Name
87
+ }
88
+
89
+
90
+ export interface ListPropType {
91
+ kind: 'list'
92
+ item: Prop
93
+ }
94
+
95
+
96
+ export interface FkPropType {
97
+ kind: 'fk'
98
+ foreignEntity: Name
99
+ }
100
+
101
+
102
+ export interface LookupPropType {
103
+ kind: 'lookup'
104
+ entity: Name
105
+ field: Name
106
+ }
107
+
108
+
109
+ export interface ListLookupPropType {
110
+ kind: 'list-lookup'
111
+ entity: Name
112
+ field: Name
113
+ }
114
+
115
+
116
+ export interface FTS_Query {
117
+ kind: 'fts'
118
+ sources: FTS_Source[]
119
+ }
120
+
121
+
122
+ export interface FTS_Source {
123
+ entity: Name
124
+ fields: Name[]
125
+ }