@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.
- package/README.md +2 -15
- package/bin/main.js +2 -0
- package/dist/db.d.ts +28 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +69 -0
- package/dist/db.js.map +1 -0
- package/dist/gql/opencrud.d.ts +1 -0
- package/dist/gql/opencrud.d.ts.map +1 -0
- package/dist/gql/opencrud.js +10 -9
- package/dist/gql/opencrud.js.map +1 -1
- package/dist/gql/schema.d.ts +1 -0
- package/dist/gql/schema.d.ts.map +1 -0
- package/dist/main.d.ts +2 -3
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +6 -30
- package/dist/main.js.map +1 -1
- package/dist/model.d.ts +1 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.tools.d.ts +1 -0
- package/dist/model.tools.d.ts.map +1 -0
- package/dist/orderBy.d.ts +1 -0
- package/dist/orderBy.d.ts.map +1 -0
- package/dist/queryBuilder.d.ts +3 -2
- package/dist/queryBuilder.d.ts.map +1 -0
- package/dist/queryBuilder.js +28 -27
- package/dist/queryBuilder.js.map +1 -1
- package/dist/relayConnection.d.ts +1 -0
- package/dist/relayConnection.d.ts.map +1 -0
- package/dist/requestedFields.d.ts +1 -0
- package/dist/requestedFields.d.ts.map +1 -0
- package/dist/requestedFields.js +3 -3
- package/dist/requestedFields.js.map +1 -1
- package/dist/resolver.d.ts +3 -2
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +12 -11
- package/dist/resolver.js.map +1 -1
- package/dist/scalars.d.ts +2 -2
- package/dist/scalars.d.ts.map +1 -0
- package/dist/scalars.js +6 -2
- package/dist/scalars.js.map +1 -1
- package/dist/server.d.ts +5 -11
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +13 -31
- package/dist/server.js.map +1 -1
- package/dist/test/basic.test.d.ts +2 -0
- package/dist/test/basic.test.d.ts.map +1 -0
- package/dist/test/basic.test.js +286 -0
- package/dist/test/basic.test.js.map +1 -0
- package/dist/test/connection.test.d.ts +2 -0
- package/dist/test/connection.test.d.ts.map +1 -0
- package/dist/test/connection.test.js +193 -0
- package/dist/test/connection.test.js.map +1 -0
- package/dist/test/fts.test.d.ts +2 -0
- package/dist/test/fts.test.d.ts.map +1 -0
- package/dist/test/fts.test.js +110 -0
- package/dist/test/fts.test.js.map +1 -0
- package/dist/test/lists.test.d.ts +2 -0
- package/dist/test/lists.test.d.ts.map +1 -0
- package/dist/test/lists.test.js +266 -0
- package/dist/test/lists.test.js.map +1 -0
- package/dist/test/lookup.test.d.ts +2 -0
- package/dist/test/lookup.test.d.ts.map +1 -0
- package/dist/test/lookup.test.js +109 -0
- package/dist/test/lookup.test.js.map +1 -0
- package/dist/test/scalars.test.d.ts +2 -0
- package/dist/test/scalars.test.d.ts.map +1 -0
- package/dist/test/scalars.test.js +303 -0
- package/dist/test/scalars.test.js.map +1 -0
- package/dist/test/tools.test.d.ts +2 -0
- package/dist/test/tools.test.d.ts.map +1 -0
- package/dist/test/tools.test.js +49 -0
- package/dist/test/tools.test.js.map +1 -0
- package/dist/test/typed-json.test.d.ts +2 -0
- package/dist/test/typed-json.test.d.ts.map +1 -0
- package/dist/test/typed-json.test.js +75 -0
- package/dist/test/typed-json.test.js.map +1 -0
- package/dist/test/unions.test.d.ts +2 -0
- package/dist/test/unions.test.d.ts.map +1 -0
- package/dist/test/unions.test.js +84 -0
- package/dist/test/unions.test.js.map +1 -0
- package/dist/test/util/setup.d.ts +7 -0
- package/dist/test/util/setup.d.ts.map +1 -0
- package/dist/test/util/setup.js +60 -0
- package/dist/test/util/setup.js.map +1 -0
- package/dist/test/where.test.d.ts +2 -0
- package/dist/test/where.test.d.ts.map +1 -0
- package/dist/test/where.test.js +127 -0
- package/dist/test/where.test.js.map +1 -0
- package/dist/tools.d.ts +1 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/util.d.ts +1 -13
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +6 -79
- package/dist/util.js.map +1 -1
- package/dist/where.d.ts +1 -0
- package/dist/where.d.ts.map +1 -0
- package/package.json +26 -20
- package/src/db.ts +83 -0
- package/src/gql/opencrud.ts +328 -0
- package/src/gql/schema.ts +337 -0
- package/src/main.ts +51 -0
- package/src/model.tools.ts +173 -0
- package/src/model.ts +125 -0
- package/src/orderBy.ts +105 -0
- package/src/queryBuilder.ts +785 -0
- package/src/relayConnection.ts +80 -0
- package/src/requestedFields.ts +246 -0
- package/src/resolver.ts +199 -0
- package/src/scalars.ts +247 -0
- package/src/server.ts +115 -0
- package/src/test/basic.test.ts +339 -0
- package/src/test/connection.test.ts +195 -0
- package/src/test/fts.test.ts +114 -0
- package/src/test/lists.test.ts +278 -0
- package/src/test/lookup.test.ts +111 -0
- package/src/test/scalars.test.ts +316 -0
- package/src/test/tools.test.ts +27 -0
- package/src/test/typed-json.test.ts +76 -0
- package/src/test/unions.test.ts +85 -0
- package/src/test/util/setup.ts +63 -0
- package/src/test/where.test.ts +135 -0
- package/src/tools.ts +33 -0
- package/src/util.ts +39 -0
- package/src/where.ts +110 -0
- package/CHANGELOG.md +0 -16
- package/dist/transaction.d.ts +0 -10
- package/dist/transaction.js +0 -47
- 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 {}
|