@subsquid/openreader 0.2.0 → 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 (128) 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/main.d.ts +2 -3
  14. package/dist/main.d.ts.map +1 -0
  15. package/dist/main.js +6 -30
  16. package/dist/main.js.map +1 -1
  17. package/dist/model.d.ts +1 -0
  18. package/dist/model.d.ts.map +1 -0
  19. package/dist/model.tools.d.ts +1 -0
  20. package/dist/model.tools.d.ts.map +1 -0
  21. package/dist/orderBy.d.ts +1 -0
  22. package/dist/orderBy.d.ts.map +1 -0
  23. package/dist/queryBuilder.d.ts +3 -2
  24. package/dist/queryBuilder.d.ts.map +1 -0
  25. package/dist/queryBuilder.js +28 -27
  26. package/dist/queryBuilder.js.map +1 -1
  27. package/dist/relayConnection.d.ts +1 -0
  28. package/dist/relayConnection.d.ts.map +1 -0
  29. package/dist/requestedFields.d.ts +1 -0
  30. package/dist/requestedFields.d.ts.map +1 -0
  31. package/dist/requestedFields.js +3 -3
  32. package/dist/requestedFields.js.map +1 -1
  33. package/dist/resolver.d.ts +3 -2
  34. package/dist/resolver.d.ts.map +1 -0
  35. package/dist/resolver.js +12 -11
  36. package/dist/resolver.js.map +1 -1
  37. package/dist/scalars.d.ts +2 -2
  38. package/dist/scalars.d.ts.map +1 -0
  39. package/dist/scalars.js +6 -2
  40. package/dist/scalars.js.map +1 -1
  41. package/dist/server.d.ts +5 -11
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +13 -31
  44. package/dist/server.js.map +1 -1
  45. package/dist/test/basic.test.d.ts +2 -0
  46. package/dist/test/basic.test.d.ts.map +1 -0
  47. package/dist/test/basic.test.js +286 -0
  48. package/dist/test/basic.test.js.map +1 -0
  49. package/dist/test/connection.test.d.ts +2 -0
  50. package/dist/test/connection.test.d.ts.map +1 -0
  51. package/dist/test/connection.test.js +193 -0
  52. package/dist/test/connection.test.js.map +1 -0
  53. package/dist/test/fts.test.d.ts +2 -0
  54. package/dist/test/fts.test.d.ts.map +1 -0
  55. package/dist/test/fts.test.js +110 -0
  56. package/dist/test/fts.test.js.map +1 -0
  57. package/dist/test/lists.test.d.ts +2 -0
  58. package/dist/test/lists.test.d.ts.map +1 -0
  59. package/dist/test/lists.test.js +266 -0
  60. package/dist/test/lists.test.js.map +1 -0
  61. package/dist/test/lookup.test.d.ts +2 -0
  62. package/dist/test/lookup.test.d.ts.map +1 -0
  63. package/dist/test/lookup.test.js +109 -0
  64. package/dist/test/lookup.test.js.map +1 -0
  65. package/dist/test/scalars.test.d.ts +2 -0
  66. package/dist/test/scalars.test.d.ts.map +1 -0
  67. package/dist/test/scalars.test.js +303 -0
  68. package/dist/test/scalars.test.js.map +1 -0
  69. package/dist/test/tools.test.d.ts +2 -0
  70. package/dist/test/tools.test.d.ts.map +1 -0
  71. package/dist/test/tools.test.js +49 -0
  72. package/dist/test/tools.test.js.map +1 -0
  73. package/dist/test/typed-json.test.d.ts +2 -0
  74. package/dist/test/typed-json.test.d.ts.map +1 -0
  75. package/dist/test/typed-json.test.js +75 -0
  76. package/dist/test/typed-json.test.js.map +1 -0
  77. package/dist/test/unions.test.d.ts +2 -0
  78. package/dist/test/unions.test.d.ts.map +1 -0
  79. package/dist/test/unions.test.js +84 -0
  80. package/dist/test/unions.test.js.map +1 -0
  81. package/dist/test/util/setup.d.ts +7 -0
  82. package/dist/test/util/setup.d.ts.map +1 -0
  83. package/dist/test/util/setup.js +60 -0
  84. package/dist/test/util/setup.js.map +1 -0
  85. package/dist/test/where.test.d.ts +2 -0
  86. package/dist/test/where.test.d.ts.map +1 -0
  87. package/dist/test/where.test.js +127 -0
  88. package/dist/test/where.test.js.map +1 -0
  89. package/dist/tools.d.ts +1 -0
  90. package/dist/tools.d.ts.map +1 -0
  91. package/dist/util.d.ts +1 -13
  92. package/dist/util.d.ts.map +1 -0
  93. package/dist/util.js +6 -79
  94. package/dist/util.js.map +1 -1
  95. package/dist/where.d.ts +1 -0
  96. package/dist/where.d.ts.map +1 -0
  97. package/package.json +26 -20
  98. package/src/db.ts +83 -0
  99. package/src/gql/opencrud.ts +328 -0
  100. package/src/gql/schema.ts +337 -0
  101. package/src/main.ts +51 -0
  102. package/src/model.tools.ts +173 -0
  103. package/src/model.ts +125 -0
  104. package/src/orderBy.ts +105 -0
  105. package/src/queryBuilder.ts +785 -0
  106. package/src/relayConnection.ts +80 -0
  107. package/src/requestedFields.ts +246 -0
  108. package/src/resolver.ts +199 -0
  109. package/src/scalars.ts +247 -0
  110. package/src/server.ts +115 -0
  111. package/src/test/basic.test.ts +339 -0
  112. package/src/test/connection.test.ts +195 -0
  113. package/src/test/fts.test.ts +114 -0
  114. package/src/test/lists.test.ts +278 -0
  115. package/src/test/lookup.test.ts +111 -0
  116. package/src/test/scalars.test.ts +316 -0
  117. package/src/test/tools.test.ts +27 -0
  118. package/src/test/typed-json.test.ts +76 -0
  119. package/src/test/unions.test.ts +85 -0
  120. package/src/test/util/setup.ts +63 -0
  121. package/src/test/where.test.ts +135 -0
  122. package/src/tools.ts +33 -0
  123. package/src/util.ts +39 -0
  124. package/src/where.ts +110 -0
  125. package/CHANGELOG.md +0 -16
  126. package/dist/transaction.d.ts +0 -10
  127. package/dist/transaction.js +0 -47
  128. package/dist/transaction.js.map +0 -1
@@ -0,0 +1,328 @@
1
+ import {Output, toCamelCase, toPlural} from "@subsquid/util"
2
+ import assert from "assert"
3
+ import {DocumentNode, parse, print} from "graphql"
4
+ import {Entity, Enum, FTS_Query, Interface, JsonObject, Model, Prop, Union} from "../model"
5
+ import {getOrderByMapping} from "../orderBy"
6
+ import {scalars_list} from "../scalars"
7
+ import {toQueryListField} from "../util"
8
+
9
+
10
+ export function generateOpenCrudQueries(model: Model): string {
11
+ let out = new Output()
12
+
13
+ generatePageInfoType()
14
+
15
+ for (let name in model) {
16
+ let item = model[name]
17
+ switch(item.kind) {
18
+ case 'entity':
19
+ generateOrderByInput(name)
20
+ generateWhereUniqueInput(name)
21
+ generateWhereInput(name, item)
22
+ generateObjectType(name, item)
23
+ generateEntityConnection(name)
24
+ break
25
+ case 'object':
26
+ if (hasFilters(item)) {
27
+ generateWhereInput(name, item)
28
+ }
29
+ generateObjectType(name, item)
30
+ break
31
+ case 'interface':
32
+ generateObjectType(name, item)
33
+ break
34
+ case 'union':
35
+ generateUnionWhereInput(name, item)
36
+ generateUnionType(name, item)
37
+ break
38
+ case 'enum':
39
+ generateEnumType(name, item)
40
+ break
41
+ case 'fts':
42
+ generateFtsTypes(name, item)
43
+ break
44
+ }
45
+ }
46
+
47
+ out.block('type Query', () => {
48
+ for (let name in model) {
49
+ let item = model[name]
50
+ if (item.kind == 'entity') {
51
+ out.line(`${toCamelCase(name)}ById(id: ID!): ${name}`)
52
+ out.line(`${toCamelCase(name)}ByUniqueInput(where: ${name}WhereUniqueInput!): ${name} @deprecated(reason: "Use \`${toCamelCase(name)}ById\`")`)
53
+ out.line(`${toQueryListField(name)}${manyArguments(name)}: [${name}!]!`)
54
+ out.line(`${toQueryListField(name)}Connection${connectionArguments(name)}: ${toPlural(name)}Connection!`)
55
+ }
56
+ if (item.kind == 'fts') {
57
+ generateFtsQuery(name, item)
58
+ }
59
+ }
60
+ })
61
+
62
+ function generateObjectType(name: string, object: Entity | JsonObject | Interface): void {
63
+ let head: string
64
+ if (object.kind == 'interface') {
65
+ head = `interface ${name}`
66
+ } else {
67
+ head = `type ${name}`
68
+ if (object.interfaces?.length) {
69
+ head += ` implements ${object.interfaces.join(' & ')}`
70
+ }
71
+ }
72
+ generateDescription(object.description)
73
+ out.block(head, () => {
74
+ for (let key in object.properties) {
75
+ let prop = object.properties[key]
76
+ let gqlType = renderPropType(prop)
77
+ generateDescription(prop.description)
78
+ if (prop.type.kind == 'list-lookup') {
79
+ out.line(`${key}${manyArguments(prop.type.entity)}: ${gqlType}`)
80
+ } else {
81
+ out.line(`${key}: ${gqlType}`)
82
+ }
83
+ }
84
+ })
85
+ out.line()
86
+ }
87
+
88
+ function renderPropType(prop: Prop): string {
89
+ switch(prop.type.kind) {
90
+ case "list":
91
+ return `[${renderPropType(prop.type.item)}]${prop.nullable ? '' : '!'}`
92
+ case 'fk':
93
+ return `${prop.type.foreignEntity}${prop.nullable ? '' : '!'}`
94
+ case 'lookup':
95
+ return prop.type.entity
96
+ case 'list-lookup':
97
+ return `[${prop.type.entity}!]!`
98
+ default:
99
+ return prop.type.name + (prop.nullable ? '' : '!')
100
+ }
101
+ }
102
+
103
+ function manyArguments(entityName: string): string {
104
+ return `(where: ${entityName}WhereInput orderBy: [${entityName}OrderByInput] offset: Int limit: Int)`
105
+ }
106
+
107
+ function connectionArguments(entityName: string): string {
108
+ return `(orderBy: [${entityName}OrderByInput!]! after: String first: Int where: ${entityName}WhereInput)`
109
+ }
110
+
111
+ function generateOrderByInput(entityName: string): void {
112
+ out.block(`enum ${entityName}OrderByInput`, () => {
113
+ let mapping = getOrderByMapping(model, entityName)
114
+ for (let key of mapping.keys()) {
115
+ out.line(key)
116
+ }
117
+ })
118
+ out.line()
119
+ }
120
+
121
+ function generateWhereUniqueInput(entityName: string): void {
122
+ out.block(`input ${entityName}WhereUniqueInput`, () => {
123
+ out.line('id: ID!')
124
+ })
125
+ }
126
+
127
+ function generateWhereInput(name: string, object: Entity | JsonObject): void {
128
+ out.block(`input ${name}WhereInput`, () => {
129
+ generatePropsFilters(object.properties)
130
+ if (object.kind == 'entity') {
131
+ out.line(`AND: [${name}WhereInput!]`)
132
+ out.line(`OR: [${name}WhereInput!]`)
133
+ }
134
+ })
135
+ out.line()
136
+ }
137
+
138
+ function generatePropsFilters(props: Record<string, Prop>): void {
139
+ for (let key in props) {
140
+ let prop = props[key]
141
+ switch(prop.type.kind) {
142
+ case 'scalar':
143
+ case 'enum':
144
+ generateScalarFilters(key, prop.type.name)
145
+ break
146
+ case 'list':
147
+ if (prop.type.item.type.kind == 'scalar' || prop.type.item.type.kind == 'enum') {
148
+ let item = prop.type.item.type.name
149
+ out.line(`${key}_containsAll: [${item}!]`)
150
+ out.line(`${key}_containsAny: [${item}!]`)
151
+ out.line(`${key}_containsNone: [${item}!]`)
152
+ }
153
+ break
154
+ case 'object':
155
+ if (hasFilters(getObject(prop.type.name))) {
156
+ out.line(`${key}: ${prop.type.name}WhereInput`)
157
+ }
158
+ break
159
+ case 'union':
160
+ out.line(`${key}: ${prop.type.name}WhereInput`)
161
+ break
162
+ case 'fk':
163
+ out.line(`${key}: ${prop.type.foreignEntity}WhereInput`)
164
+ break
165
+ case 'lookup':
166
+ out.line(`${key}: ${prop.type.entity}WhereInput`)
167
+ break
168
+ case 'list-lookup':
169
+ out.line(`${key}_every: ${prop.type.entity}WhereInput`)
170
+ out.line(`${key}_some: ${prop.type.entity}WhereInput`)
171
+ out.line(`${key}_none: ${prop.type.entity}WhereInput`)
172
+ break
173
+ }
174
+ }
175
+ }
176
+
177
+ function hasFilters(obj: JsonObject): boolean {
178
+ for (let key in obj.properties) {
179
+ let propType = obj.properties[key].type
180
+ switch(propType.kind) {
181
+ case 'scalar':
182
+ case 'enum':
183
+ case 'union':
184
+ return true
185
+ case 'object':
186
+ if (hasFilters(getObject(propType.name))) {
187
+ return true
188
+ }
189
+ }
190
+ }
191
+ return false
192
+ }
193
+
194
+ function getObject(name: string): JsonObject {
195
+ let obj = model[name]
196
+ assert(obj.kind == 'object')
197
+ return obj
198
+ }
199
+
200
+ function generateUnionWhereInput(name: string, union: Union): void {
201
+ out.block(`input ${name}WhereInput`, () => {
202
+ // TODO: unify and use enum
203
+ out.line('isTypeOf_eq: String')
204
+ out.line('isTypeOf_not_eq: String')
205
+ out.line('isTypeOf_in: [String!]')
206
+ out.line('isTypeOf_not_in: [String!]')
207
+
208
+ let props: Record<string, Prop> = {}
209
+ union.variants.forEach(variant => {
210
+ let obj = getObject(variant)
211
+ Object.assign(props, obj.properties)
212
+ })
213
+
214
+ generatePropsFilters(props)
215
+ })
216
+ }
217
+
218
+ function generateScalarFilters(fieldName: string, graphqlType: string): void {
219
+ out.line(`${fieldName}_eq: ${graphqlType}`)
220
+ out.line(`${fieldName}_not_eq: ${graphqlType}`)
221
+
222
+ switch(graphqlType) {
223
+ case 'ID':
224
+ case 'String':
225
+ case 'Int':
226
+ case 'Float':
227
+ case 'DateTime':
228
+ case 'BigInt':
229
+ out.line(`${fieldName}_gt: ${graphqlType}`)
230
+ out.line(`${fieldName}_gte: ${graphqlType}`)
231
+ out.line(`${fieldName}_lt: ${graphqlType}`)
232
+ out.line(`${fieldName}_lte: ${graphqlType}`)
233
+ out.line(`${fieldName}_in: [${graphqlType}!]`)
234
+ out.line(`${fieldName}_not_in: [${graphqlType}!]`)
235
+ break
236
+ }
237
+
238
+ if (graphqlType == 'String' || graphqlType == 'ID') {
239
+ out.line(`${fieldName}_contains: ${graphqlType}`)
240
+ out.line(`${fieldName}_not_contains: ${graphqlType}`)
241
+ out.line(`${fieldName}_startsWith: ${graphqlType}`)
242
+ out.line(`${fieldName}_not_startsWith: ${graphqlType}`)
243
+ out.line(`${fieldName}_endsWith: ${graphqlType}`)
244
+ out.line(`${fieldName}_not_endsWith: ${graphqlType}`)
245
+ }
246
+
247
+ if (model[graphqlType]?.kind == 'enum') {
248
+ out.line(`${fieldName}_in: [${graphqlType}!]`)
249
+ out.line(`${fieldName}_not_in: [${graphqlType}!]`)
250
+ }
251
+ }
252
+
253
+ function generateUnionType(name: string, union: Union) {
254
+ generateDescription(union.description)
255
+ out.line(`union ${name} = ${union.variants.join(' | ')}`)
256
+ out.line()
257
+ }
258
+
259
+ function generateEnumType(name: string, e: Enum): void {
260
+ generateDescription(e.description)
261
+ out.block(`enum ${name}`, () => {
262
+ for (let key in e.values) {
263
+ out.line(key)
264
+ }
265
+ })
266
+ }
267
+
268
+ function generatePageInfoType(): void {
269
+ out.block(`type PageInfo`, () => {
270
+ out.line('hasNextPage: Boolean!')
271
+ out.line('hasPreviousPage: Boolean!')
272
+ out.line('startCursor: String!')
273
+ out.line('endCursor: String!')
274
+ })
275
+ out.line()
276
+ }
277
+
278
+ function generateEntityConnection(name: string): void {
279
+ out.block(`type ${name}Edge`, () => {
280
+ out.line(`node: ${name}!`)
281
+ out.line(`cursor: String!`)
282
+ })
283
+ out.line()
284
+ out.block(`type ${toPlural(name)}Connection`, () => {
285
+ out.line(`edges: [${name}Edge!]!`)
286
+ out.line(`pageInfo: PageInfo!`)
287
+ out.line(`totalCount: Int!`)
288
+ })
289
+ out.line()
290
+ }
291
+
292
+ function generateFtsTypes(name: string, query: FTS_Query): void {
293
+ let itemType = name + '_Item'
294
+ out.line(`union ${itemType} = ${query.sources.map(s => s.entity).join(' | ')}`)
295
+ out.line()
296
+ out.block(`type ${name}_Output`, () => {
297
+ out.line(`item: ${itemType}!`)
298
+ out.line(`rank: Float!`)
299
+ out.line(`highlight: String!`)
300
+ })
301
+ out.line()
302
+ }
303
+
304
+ function generateFtsQuery(name: string, query: FTS_Query): void {
305
+ let where = query.sources.map(src => {
306
+ return `where${src.entity}: ${src.entity}WhereInput`
307
+ })
308
+ out.line(`${name}(text: String! ${where.join(' ')} limit: Int offset: Int): [${name}_Output!]!`)
309
+ }
310
+
311
+ function generateDescription(description?: string): void {
312
+ if (description) {
313
+ out.line(print({
314
+ kind: 'StringValue',
315
+ value: description
316
+ }))
317
+ }
318
+ }
319
+
320
+ return out.toString()
321
+ }
322
+
323
+
324
+ export function buildServerSchema(model: Model): DocumentNode {
325
+ let scalars = scalars_list.map(name => 'scalar ' + name).join('\n')
326
+ let queries = generateOpenCrudQueries(model)
327
+ return parse(scalars + '\n\n' + queries)
328
+ }
@@ -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 {}