@subsquid/openreader 0.3.1 → 0.4.1
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/dist/db.d.ts +1 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/gql/opencrud.d.ts +1 -0
- package/dist/gql/opencrud.d.ts.map +1 -0
- package/dist/gql/schema.d.ts +1 -0
- package/dist/gql/schema.d.ts.map +1 -0
- package/dist/gql/schema.js +116 -17
- package/dist/gql/schema.js.map +1 -1
- package/dist/main.d.ts +1 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/model.d.ts +9 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.tools.d.ts +2 -0
- package/dist/model.tools.d.ts.map +1 -0
- package/dist/model.tools.js +27 -1
- package/dist/model.tools.js.map +1 -1
- package/dist/orderBy.d.ts +1 -0
- package/dist/orderBy.d.ts.map +1 -0
- package/dist/queryBuilder.d.ts +1 -0
- package/dist/queryBuilder.d.ts.map +1 -0
- 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/resolver.d.ts +1 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/scalars.d.ts +1 -0
- package/dist/scalars.d.ts.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- 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 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/where.d.ts +1 -0
- package/dist/where.d.ts.map +1 -0
- package/package.json +15 -13
- package/src/db.ts +83 -0
- package/src/gql/opencrud.ts +328 -0
- package/src/gql/schema.ts +463 -0
- package/src/main.ts +51 -0
- package/src/model.tools.ts +201 -0
- package/src/model.ts +137 -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
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
import {toSnakeCase} from "@subsquid/util"
|
|
2
|
+
import assert from "assert"
|
|
3
|
+
import type {ClientBase, QueryArrayResult} from "pg"
|
|
4
|
+
import {Database} from "./db"
|
|
5
|
+
import type {Entity, JsonObject, Model} from "./model"
|
|
6
|
+
import {getEntity, getFtsQuery, getObject, getUnionProps} from "./model.tools"
|
|
7
|
+
import {OpenCrudOrderByValue, OrderBy, parseOrderBy} from "./orderBy"
|
|
8
|
+
import type {FtsRequestedFields, RequestedFields} from "./requestedFields"
|
|
9
|
+
import {fromJsonCast, fromJsonToOutputCast, toOutputArrayCast, toOutputCast} from "./scalars"
|
|
10
|
+
import {ensureArray, toColumn, toFkColumn, toInt, toTable, unsupportedCase} from "./util"
|
|
11
|
+
import {hasConditions, parseWhereField, WhereOp, whereOpToSqlOperator} from "./where"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export interface ListArgs {
|
|
15
|
+
offset?: number
|
|
16
|
+
limit?: number
|
|
17
|
+
orderBy?: OpenCrudOrderByValue[]
|
|
18
|
+
where?: any
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
export class QueryBuilder {
|
|
23
|
+
private params: any[] = []
|
|
24
|
+
private aliases: AliasSet = new AliasSet()
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
private model: Model,
|
|
28
|
+
private db: Database
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
private param(value: any): string {
|
|
32
|
+
return '$' + this.params.push(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private ident(name: string): string {
|
|
36
|
+
return this.db.escapeIdentifier(name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
select(entityName: string, args: ListArgs, fields?: RequestedFields, variant?: SelectVariant): string {
|
|
40
|
+
let entity = getEntity(this.model, entityName)
|
|
41
|
+
let table = toTable(entityName)
|
|
42
|
+
let alias = this.aliases.add(table)
|
|
43
|
+
let join = new JoinSet(this.aliases)
|
|
44
|
+
|
|
45
|
+
let cursor = new Cursor(
|
|
46
|
+
this.model,
|
|
47
|
+
this.ident.bind(this),
|
|
48
|
+
this.aliases,
|
|
49
|
+
join,
|
|
50
|
+
entityName,
|
|
51
|
+
entity,
|
|
52
|
+
alias,
|
|
53
|
+
''
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
let whereExps: string[] = []
|
|
57
|
+
let orderByExps: string[] = []
|
|
58
|
+
let columns = new ColumnSet()
|
|
59
|
+
let out = ''
|
|
60
|
+
|
|
61
|
+
if (fields) {
|
|
62
|
+
this.populateColumns(columns, cursor, fields)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
switch(variant?.kind) {
|
|
66
|
+
case 'fts':
|
|
67
|
+
out += 'SELECT\n'
|
|
68
|
+
out += ` '${entityName}' AS isTypeOf`
|
|
69
|
+
out += ',\n'
|
|
70
|
+
out += ` ts_rank(${cursor.tsv(variant.queryName)}, phraseto_tsquery('english', ${variant.textParam})) AS rank`
|
|
71
|
+
out += ',\n'
|
|
72
|
+
out += ` ts_headline(${cursor.doc(variant.queryName)}, phraseto_tsquery('english', ${variant.textParam})) AS highlight`
|
|
73
|
+
out += ',\n'
|
|
74
|
+
out += columns.size() ? ` json_build_array(${columns.render()})` : " '[]'::json"
|
|
75
|
+
out += ' AS item\n'
|
|
76
|
+
break
|
|
77
|
+
case 'list-subquery':
|
|
78
|
+
if (columns.size()) {
|
|
79
|
+
out += `SELECT json_build_array(${columns.render()}) `
|
|
80
|
+
}
|
|
81
|
+
break
|
|
82
|
+
default:
|
|
83
|
+
if (columns.size()) {
|
|
84
|
+
out += `SELECT ${columns.render()}\n`
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
out += `FROM ${this.ident(table)} ${this.ident(alias)}`
|
|
89
|
+
|
|
90
|
+
if (hasConditions(args.where)) {
|
|
91
|
+
whereExps.push(this.generateWhere(cursor, args.where))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (variant?.kind == 'list-subquery') {
|
|
95
|
+
whereExps.push(`${cursor.fk(variant.field)} = ${variant.parent}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (variant?.kind == 'fts') {
|
|
99
|
+
whereExps.push(`phraseto_tsquery('english', ${variant.textParam}) @@ ${cursor.tsv(variant.queryName)}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let orderByInput = args.orderBy && ensureArray(args.orderBy)
|
|
103
|
+
if (orderByInput?.length) {
|
|
104
|
+
let orderBy = parseOrderBy(this.model, entityName, orderByInput)
|
|
105
|
+
this.populateOrderBy(orderByExps, cursor, orderBy)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
join.forEach(j => {
|
|
109
|
+
let table = this.ident(j.table)
|
|
110
|
+
let alias = this.ident(j.alias)
|
|
111
|
+
out += `\nLEFT OUTER JOIN ${table} ${alias} ON ${alias}.${j.column} = ${j.rhs}`
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (whereExps.length) {
|
|
115
|
+
out += '\nWHERE ' + whereExps.join(' AND ')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (orderByExps.length > 0) {
|
|
119
|
+
out += '\nORDER BY ' + orderByExps.join(', ')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (args.limit) {
|
|
123
|
+
out += '\nLIMIT ' + this.param(args.limit)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (args.offset) {
|
|
127
|
+
out += '\nOFFSET ' + this.param(args.offset)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (variant?.kind == 'list-subquery') {
|
|
131
|
+
out = out.replace(/\n/g, ' ')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return out
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private populateOrderBy(
|
|
138
|
+
exps: string[],
|
|
139
|
+
cursor: Cursor,
|
|
140
|
+
orderBy: OrderBy
|
|
141
|
+
) {
|
|
142
|
+
for (let key in orderBy) {
|
|
143
|
+
let spec = orderBy[key]
|
|
144
|
+
let propType = cursor.object.properties[key].type
|
|
145
|
+
switch(propType.kind) {
|
|
146
|
+
case 'scalar':
|
|
147
|
+
case 'enum':
|
|
148
|
+
assert(typeof spec == 'string')
|
|
149
|
+
exps.push(`${cursor.native(key)} ${spec}`)
|
|
150
|
+
break
|
|
151
|
+
case 'object':
|
|
152
|
+
case 'union':
|
|
153
|
+
case 'fk':
|
|
154
|
+
case 'lookup':
|
|
155
|
+
assert(typeof spec == 'object')
|
|
156
|
+
this.populateOrderBy(
|
|
157
|
+
exps,
|
|
158
|
+
cursor.child(key),
|
|
159
|
+
spec
|
|
160
|
+
)
|
|
161
|
+
break
|
|
162
|
+
default:
|
|
163
|
+
throw unsupportedCase(propType.kind)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private populateColumns(
|
|
169
|
+
columns: ColumnSet,
|
|
170
|
+
cursor: Cursor,
|
|
171
|
+
fields$?: RequestedFields
|
|
172
|
+
): void {
|
|
173
|
+
for (let fieldName in fields$) {
|
|
174
|
+
let field = fields$[fieldName]
|
|
175
|
+
for (let i = 0; i < field.requests.length; i++) {
|
|
176
|
+
let req = field.requests[i]
|
|
177
|
+
switch(field.propType.kind) {
|
|
178
|
+
case 'scalar':
|
|
179
|
+
case 'enum':
|
|
180
|
+
case 'list':
|
|
181
|
+
req.index = columns.add(cursor.transport(fieldName))
|
|
182
|
+
break
|
|
183
|
+
case 'object':
|
|
184
|
+
req.index = columns.add(cursor.field(fieldName) + ' IS NULL')
|
|
185
|
+
this.populateColumns(
|
|
186
|
+
columns,
|
|
187
|
+
cursor.child(fieldName),
|
|
188
|
+
req.children
|
|
189
|
+
)
|
|
190
|
+
break
|
|
191
|
+
case 'union':
|
|
192
|
+
let cu = cursor.child(fieldName)
|
|
193
|
+
req.index = columns.add(cu.transport('isTypeOf'))
|
|
194
|
+
this.populateColumns(
|
|
195
|
+
columns,
|
|
196
|
+
cu,
|
|
197
|
+
req.children
|
|
198
|
+
)
|
|
199
|
+
break
|
|
200
|
+
case 'fk':
|
|
201
|
+
case 'lookup': {
|
|
202
|
+
let cu = cursor.child(fieldName)
|
|
203
|
+
req.index = columns.add(cu.transport('id'))
|
|
204
|
+
this.populateColumns(
|
|
205
|
+
columns,
|
|
206
|
+
cu,
|
|
207
|
+
req.children
|
|
208
|
+
)
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
case 'list-lookup':
|
|
212
|
+
req.index = columns.add(
|
|
213
|
+
'array(' + this.select(field.propType.entity, req.args, req.children, {
|
|
214
|
+
kind: 'list-subquery',
|
|
215
|
+
field: field.propType.field,
|
|
216
|
+
parent: cursor.native('id')
|
|
217
|
+
}) + ')'
|
|
218
|
+
)
|
|
219
|
+
break
|
|
220
|
+
default:
|
|
221
|
+
throw unsupportedCase((field as any).propType.kind)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private generateWhere(cursor: Cursor, where: any): string {
|
|
228
|
+
let {AND, OR, ...conditions} = where
|
|
229
|
+
let exps: string[] = []
|
|
230
|
+
for (let key in conditions) {
|
|
231
|
+
let opArg = conditions[key]
|
|
232
|
+
let f = parseWhereField(key)
|
|
233
|
+
switch(f.op) {
|
|
234
|
+
case 'every':
|
|
235
|
+
if (hasConditions(opArg)) {
|
|
236
|
+
let rel = cursor.object.properties[f.field].type
|
|
237
|
+
assert(rel.kind == 'list-lookup')
|
|
238
|
+
let conditionedFrom = this.select(
|
|
239
|
+
rel.entity,
|
|
240
|
+
{where: opArg},
|
|
241
|
+
undefined,
|
|
242
|
+
{kind: 'list-subquery', parent: cursor.native('id'), field: rel.field}
|
|
243
|
+
)
|
|
244
|
+
let allFrom = this.select(
|
|
245
|
+
rel.entity,
|
|
246
|
+
{},
|
|
247
|
+
undefined,
|
|
248
|
+
{kind: 'list-subquery', parent: cursor.native('id'), field: rel.field}
|
|
249
|
+
)
|
|
250
|
+
exps.push(`(SELECT count(*) ${conditionedFrom}) = (SELECT count(*) ${allFrom})`)
|
|
251
|
+
}
|
|
252
|
+
break
|
|
253
|
+
case 'some':
|
|
254
|
+
case 'none':
|
|
255
|
+
let rel = cursor.object.properties[f.field].type
|
|
256
|
+
assert(rel.kind == 'list-lookup')
|
|
257
|
+
let q = '(SELECT true ' + this.select(
|
|
258
|
+
rel.entity,
|
|
259
|
+
{where: opArg},
|
|
260
|
+
undefined,
|
|
261
|
+
{kind: 'list-subquery', parent: cursor.native('id'), field: rel.field}
|
|
262
|
+
) + ' LIMIT 1)'
|
|
263
|
+
if (f.op == 'some') {
|
|
264
|
+
exps.push(q)
|
|
265
|
+
} else {
|
|
266
|
+
exps.push(`(SELECT count(*) FROM ${q} ${this.ident(this.aliases.add(key))}) = 0`)
|
|
267
|
+
}
|
|
268
|
+
break
|
|
269
|
+
default: {
|
|
270
|
+
let prop = cursor.object.properties[f.field]
|
|
271
|
+
assert(prop != null)
|
|
272
|
+
this.addPropCondition(exps, cursor, f.field, f.op, opArg)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (AND) {
|
|
277
|
+
// We are getting objects here, although we have array in schema
|
|
278
|
+
ensureArray(AND).forEach((andWhere: any) => {
|
|
279
|
+
if (hasConditions(andWhere)) {
|
|
280
|
+
exps.push(
|
|
281
|
+
this.generateWhere(cursor, andWhere)
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
if (OR) {
|
|
287
|
+
let ors: string[] = []
|
|
288
|
+
if (exps.length) {
|
|
289
|
+
ors.push('(' + exps.join(' AND ') + ')')
|
|
290
|
+
}
|
|
291
|
+
// We are getting objects here, although we have array in schema
|
|
292
|
+
ensureArray(OR).forEach((orWhere: any) => {
|
|
293
|
+
if (hasConditions(orWhere)) {
|
|
294
|
+
ors.push(
|
|
295
|
+
'(' + this.generateWhere(cursor, orWhere) + ')'
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
return '(' + ors.join(' OR ') + ')'
|
|
300
|
+
} else {
|
|
301
|
+
return exps.join(' AND ')
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private addPropCondition(exps: string[], cursor: Cursor, field: string, op: WhereOp, arg: any): void {
|
|
306
|
+
let propType = cursor.object.properties[field].type
|
|
307
|
+
switch(propType.kind) {
|
|
308
|
+
case 'scalar':
|
|
309
|
+
case 'enum': {
|
|
310
|
+
let lhs = cursor.native(field)
|
|
311
|
+
switch(op) {
|
|
312
|
+
case 'in':
|
|
313
|
+
case 'not_in': {
|
|
314
|
+
// We have 2 options here
|
|
315
|
+
// 1. use array parameter and do: WHERE col IN (SELECT * FROM unnest($array_param))
|
|
316
|
+
// 2. use arg list
|
|
317
|
+
// Let's try second option first.
|
|
318
|
+
let list = ensureArray(arg).map(a => this.param(a))
|
|
319
|
+
let param = `(${list.join(', ')})`
|
|
320
|
+
exps.push(`${lhs} ${whereOpToSqlOperator(op)} ${param}`)
|
|
321
|
+
break
|
|
322
|
+
}
|
|
323
|
+
case 'startsWith':
|
|
324
|
+
exps.push(`starts_with(${lhs}, ${this.param(arg)})`)
|
|
325
|
+
break
|
|
326
|
+
case 'not_startsWith':
|
|
327
|
+
exps.push(`NOT starts_with(${lhs}, ${this.param(arg)})`)
|
|
328
|
+
break
|
|
329
|
+
case 'endsWith': {
|
|
330
|
+
let param = this.param(arg)
|
|
331
|
+
exps.push(`right(${lhs}, length(${param})) = ${param}`)
|
|
332
|
+
break
|
|
333
|
+
}
|
|
334
|
+
case 'not_endsWith': {
|
|
335
|
+
let param = this.param(arg)
|
|
336
|
+
exps.push(`right(${lhs}, length(${param})) != ${param}`)
|
|
337
|
+
break
|
|
338
|
+
}
|
|
339
|
+
case 'contains':
|
|
340
|
+
exps.push(`position(${this.param(arg)} in ${lhs}) > 0`)
|
|
341
|
+
break
|
|
342
|
+
case 'not_contains':
|
|
343
|
+
exps.push(`position(${this.param(arg)} in ${lhs}) = 0`)
|
|
344
|
+
break
|
|
345
|
+
default: {
|
|
346
|
+
exps.push(`${lhs} ${whereOpToSqlOperator(op)} ${this.param(arg)}`)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
break
|
|
350
|
+
}
|
|
351
|
+
case 'list': {
|
|
352
|
+
let item = propType.item.type
|
|
353
|
+
assert(item.kind == 'scalar' || item.kind == 'enum')
|
|
354
|
+
let param = this.param(arg)
|
|
355
|
+
let lhs = cursor.native(field)
|
|
356
|
+
switch(op) {
|
|
357
|
+
case 'containsAll':
|
|
358
|
+
exps.push(`${lhs} @> ${param}`)
|
|
359
|
+
break
|
|
360
|
+
case 'containsAny':
|
|
361
|
+
exps.push(`${lhs} && ${param}`)
|
|
362
|
+
break
|
|
363
|
+
case 'containsNone':
|
|
364
|
+
exps.push(`NOT (${lhs} && ${param})`)
|
|
365
|
+
break
|
|
366
|
+
default:
|
|
367
|
+
throw unsupportedCase(op)
|
|
368
|
+
}
|
|
369
|
+
break
|
|
370
|
+
}
|
|
371
|
+
case 'object':
|
|
372
|
+
case 'union': {
|
|
373
|
+
assert(op == '-')
|
|
374
|
+
let cu = cursor.child(field)
|
|
375
|
+
for (let key in arg) {
|
|
376
|
+
let f = parseWhereField(key)
|
|
377
|
+
this.addPropCondition(exps, cu, f.field, f.op, arg[key])
|
|
378
|
+
}
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
case 'fk':
|
|
382
|
+
case 'lookup': {
|
|
383
|
+
assert(op == '-')
|
|
384
|
+
if (hasConditions(arg)) {
|
|
385
|
+
exps.push(
|
|
386
|
+
this.generateWhere(cursor.child(field), arg)
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
break
|
|
390
|
+
}
|
|
391
|
+
default:
|
|
392
|
+
throw unsupportedCase(propType.kind)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
toResult(rows: any[][], fields?: RequestedFields): any[] {
|
|
397
|
+
let out: any[] = new Array(rows.length)
|
|
398
|
+
for (let i = 0; i < rows.length; i++) {
|
|
399
|
+
out[i] = this.mapRow(rows[i], fields)
|
|
400
|
+
}
|
|
401
|
+
return out
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private mapRow(row: any[], fields?: RequestedFields, ifType?: string): any {
|
|
405
|
+
let rec: any = {}
|
|
406
|
+
for (let key in fields) {
|
|
407
|
+
let f = fields[key]
|
|
408
|
+
for (let i = 0; i < f.requests.length; i++) {
|
|
409
|
+
let req = f.requests[i]
|
|
410
|
+
if (req.ifType != ifType) continue
|
|
411
|
+
switch(f.propType.kind) {
|
|
412
|
+
case 'scalar':
|
|
413
|
+
case 'enum':
|
|
414
|
+
case 'list':
|
|
415
|
+
rec[req.alias] = row[req.index]
|
|
416
|
+
break
|
|
417
|
+
case 'object': {
|
|
418
|
+
let isNull = row[req.index]
|
|
419
|
+
if (!isNull) {
|
|
420
|
+
rec[req.alias] = this.mapRow(row, req.children)
|
|
421
|
+
}
|
|
422
|
+
break
|
|
423
|
+
}
|
|
424
|
+
case 'union': {
|
|
425
|
+
let isTypeOf = row[req.index]
|
|
426
|
+
if (isTypeOf != null) {
|
|
427
|
+
let obj = this.mapRow(row, req.children, isTypeOf)
|
|
428
|
+
obj.isTypeOf = isTypeOf
|
|
429
|
+
rec[req.alias] = obj
|
|
430
|
+
}
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
case 'fk':
|
|
434
|
+
case 'lookup': {
|
|
435
|
+
let id = row[req.index]
|
|
436
|
+
if (id != null) {
|
|
437
|
+
rec[req.alias] = this.mapRow(row, req.children)
|
|
438
|
+
}
|
|
439
|
+
break
|
|
440
|
+
}
|
|
441
|
+
case 'list-lookup':
|
|
442
|
+
rec[req.alias] = this.toResult(row[req.index], req.children)
|
|
443
|
+
break
|
|
444
|
+
default:
|
|
445
|
+
throw unsupportedCase((f as any).propType.kind)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return rec
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async executeSelect(entityName: string, args: ListArgs, fields$: RequestedFields): Promise<any[]> {
|
|
453
|
+
let sql = this.select(entityName, args, fields$)
|
|
454
|
+
let rows = await this.query(sql)
|
|
455
|
+
return this.toResult(rows, fields$)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async executeSelectCount(entityName: string, where?: any): Promise<number> {
|
|
459
|
+
let sql = `SELECT count(*) ${this.select(entityName, {where})}`
|
|
460
|
+
let rows = await this.query(sql)
|
|
461
|
+
return toInt(rows[0][0])
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async executeListCount(entityName: string, args: ListArgs): Promise<number> {
|
|
465
|
+
let sql = `SELECT count(*) FROM (SELECT true ${this.select(entityName, args)}) AS ${this.aliases.add('list')}`
|
|
466
|
+
let rows = await this.query(sql)
|
|
467
|
+
return toInt(rows[0][0])
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private query(sql: string): Promise<any[][]> {
|
|
471
|
+
return this.db.query(sql, this.params)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
fulltextSearchSelect(queryName: string, args: any, $fields: FtsRequestedFields): string {
|
|
475
|
+
let query = getFtsQuery(this.model, queryName)
|
|
476
|
+
let {limit, offset, text} = args
|
|
477
|
+
let textParam = this.param(text)
|
|
478
|
+
|
|
479
|
+
let srcSelects: string[] = []
|
|
480
|
+
query.sources.forEach(src => {
|
|
481
|
+
let where = args[`where${src.entity}`]
|
|
482
|
+
let itemFields = $fields.item?.[src.entity]
|
|
483
|
+
let sql = this.select(src.entity, {where}, itemFields, {kind: 'fts', textParam, queryName})
|
|
484
|
+
srcSelects.push(sql)
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
let cols: string[] = []
|
|
488
|
+
cols.push('isTypeOf')
|
|
489
|
+
cols.push('rank')
|
|
490
|
+
if ($fields.highlight) {
|
|
491
|
+
cols.push('highlight')
|
|
492
|
+
}
|
|
493
|
+
if ($fields.item) {
|
|
494
|
+
cols.push('item')
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let sql = `SELECT ${cols.join(', ')} FROM (\n\n`
|
|
498
|
+
sql += srcSelects.join('\n\nUNION ALL\n\n')
|
|
499
|
+
sql += `\n\n) AS ${this.aliases.add('tsv')}`
|
|
500
|
+
sql += ` ORDER BY rank DESC`
|
|
501
|
+
if (limit != null) {
|
|
502
|
+
sql += ` LIMIT ${this.param(limit)}`
|
|
503
|
+
}
|
|
504
|
+
if (offset != null) {
|
|
505
|
+
sql += ` OFFSET ${this.param(offset)}`
|
|
506
|
+
}
|
|
507
|
+
return sql
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
toFulltextSearchResult(rows: any[][], fields: FtsRequestedFields): FtsItem[] {
|
|
511
|
+
let out: FtsItem[] = new Array(rows.length)
|
|
512
|
+
for (let i = 0; i < rows.length; i++) {
|
|
513
|
+
let row = rows[i]
|
|
514
|
+
let isTypeOf = row[0]
|
|
515
|
+
let highlight = fields.highlight ? row[2] : undefined
|
|
516
|
+
let itemIdx = fields.highlight ? 3 : 2
|
|
517
|
+
let itemFields = fields.item?.[isTypeOf]
|
|
518
|
+
let item: any
|
|
519
|
+
if (itemFields) {
|
|
520
|
+
item = this.mapRow(row[itemIdx], itemFields)
|
|
521
|
+
item.isTypeOf = isTypeOf
|
|
522
|
+
} else {
|
|
523
|
+
item = {isTypeOf}
|
|
524
|
+
}
|
|
525
|
+
out[i] = {
|
|
526
|
+
rank: row[1],
|
|
527
|
+
highlight,
|
|
528
|
+
item
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return out
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async executeFulltextSearch(queryName: string, args: any, $fields: FtsRequestedFields): Promise<FtsItem[]> {
|
|
535
|
+
let sql = this.fulltextSearchSelect(queryName, args, $fields)
|
|
536
|
+
let rows = await this.query(sql)
|
|
537
|
+
return this.toFulltextSearchResult(rows, $fields)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
export interface FtsItem {
|
|
543
|
+
rank?: number
|
|
544
|
+
highlight?: string
|
|
545
|
+
item?: any
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
type SelectVariant = FtsVariant | ListSubquery
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
interface FtsVariant {
|
|
553
|
+
kind: 'fts'
|
|
554
|
+
queryName: string
|
|
555
|
+
textParam: string // builder.param(text)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* SELECT json_build_array(...fields) FROM ... WHERE {toFkColumn(field)} = {parent}
|
|
561
|
+
*/
|
|
562
|
+
interface ListSubquery {
|
|
563
|
+
kind: 'list-subquery'
|
|
564
|
+
field: string
|
|
565
|
+
parent: string
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* A pointer to an entity or nested json object within SQL query.
|
|
571
|
+
*
|
|
572
|
+
* It has convenience methods for building various SQL expressions
|
|
573
|
+
* related to individual properties of an entity or an object it points to.
|
|
574
|
+
*/
|
|
575
|
+
class Cursor {
|
|
576
|
+
constructor(
|
|
577
|
+
private model: Model,
|
|
578
|
+
private ident: (name: string) => string,
|
|
579
|
+
private aliases: AliasSet,
|
|
580
|
+
private join: JoinSet,
|
|
581
|
+
private name: string,
|
|
582
|
+
public readonly object: Entity | JsonObject,
|
|
583
|
+
private alias: string,
|
|
584
|
+
private prefix: string
|
|
585
|
+
) {}
|
|
586
|
+
|
|
587
|
+
transport(propName: string): string {
|
|
588
|
+
let prop = this.object.properties[propName]
|
|
589
|
+
switch(prop.type.kind) {
|
|
590
|
+
case 'scalar':
|
|
591
|
+
case 'enum':
|
|
592
|
+
if (this.object.kind == 'object') {
|
|
593
|
+
return fromJsonToOutputCast(prop.type.name, this.prefix, propName)
|
|
594
|
+
} else {
|
|
595
|
+
return toOutputCast(prop.type.name, this.column(propName))
|
|
596
|
+
}
|
|
597
|
+
case 'list':
|
|
598
|
+
let itemType = prop.type.item.type
|
|
599
|
+
if (this.object.kind == 'object' || itemType.kind != 'scalar' && itemType.kind != 'enum') {
|
|
600
|
+
// this is json
|
|
601
|
+
return this.field(propName)
|
|
602
|
+
} else {
|
|
603
|
+
return toOutputArrayCast(itemType.name, this.column(propName))
|
|
604
|
+
}
|
|
605
|
+
default:
|
|
606
|
+
throw unsupportedCase(prop.type.kind)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
native(propName: string): string {
|
|
611
|
+
let prop = this.object.properties[propName]
|
|
612
|
+
if (prop.type.kind == 'list') {
|
|
613
|
+
let item = prop.type.item.type
|
|
614
|
+
assert(item.kind == 'scalar' || item.kind == 'enum')
|
|
615
|
+
return this.column(propName)
|
|
616
|
+
}
|
|
617
|
+
assert(prop.type.kind == 'scalar' || prop.type.kind == 'enum')
|
|
618
|
+
if (this.object.kind == 'object') {
|
|
619
|
+
return fromJsonCast(prop.type.name, this.prefix, propName)
|
|
620
|
+
} else {
|
|
621
|
+
return this.column(propName)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
child(propName: string): Cursor {
|
|
626
|
+
let name: string
|
|
627
|
+
let object: Entity | JsonObject
|
|
628
|
+
let alias: string
|
|
629
|
+
let prefix: string
|
|
630
|
+
|
|
631
|
+
let prop = this.object.properties[propName]
|
|
632
|
+
switch(prop.type.kind) {
|
|
633
|
+
case 'object':
|
|
634
|
+
name = prop.type.name
|
|
635
|
+
object = getObject(this.model, name)
|
|
636
|
+
alias = this.alias
|
|
637
|
+
prefix = this.field(propName)
|
|
638
|
+
break
|
|
639
|
+
case 'union':
|
|
640
|
+
name = prop.type.name
|
|
641
|
+
object = getUnionProps(this.model, name)
|
|
642
|
+
alias = this.alias
|
|
643
|
+
prefix = this.field(propName)
|
|
644
|
+
break
|
|
645
|
+
case 'fk':
|
|
646
|
+
name = prop.type.foreignEntity
|
|
647
|
+
object = getEntity(this.model, name)
|
|
648
|
+
alias = this.join.add(
|
|
649
|
+
toTable(name),
|
|
650
|
+
'"id"',
|
|
651
|
+
this.fk(propName)
|
|
652
|
+
)
|
|
653
|
+
prefix = ''
|
|
654
|
+
break
|
|
655
|
+
case 'lookup':
|
|
656
|
+
name = prop.type.entity
|
|
657
|
+
object = getEntity(this.model, name)
|
|
658
|
+
alias = this.join.add(
|
|
659
|
+
toTable(name),
|
|
660
|
+
this.ident(toFkColumn(prop.type.field)),
|
|
661
|
+
this.field('id')
|
|
662
|
+
)
|
|
663
|
+
prefix = ''
|
|
664
|
+
break
|
|
665
|
+
default:
|
|
666
|
+
throw unsupportedCase(prop.type.kind)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return new Cursor(
|
|
670
|
+
this.model,
|
|
671
|
+
this.ident,
|
|
672
|
+
this.aliases,
|
|
673
|
+
this.join,
|
|
674
|
+
name,
|
|
675
|
+
object,
|
|
676
|
+
alias,
|
|
677
|
+
prefix
|
|
678
|
+
)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
field(name: string): string {
|
|
682
|
+
if (this.object.kind == 'entity') {
|
|
683
|
+
return this.column(name)
|
|
684
|
+
} else {
|
|
685
|
+
return `${this.prefix}->'${name}'`
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private column(name: string) {
|
|
690
|
+
assert(this.object.kind == 'entity')
|
|
691
|
+
return this.ident(this.alias) + '.' + this.ident(toColumn(name))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fk(propName: string): string {
|
|
695
|
+
return this.object.kind == 'entity'
|
|
696
|
+
? this.ident(this.alias) + '.' + this.ident(toFkColumn(propName))
|
|
697
|
+
: fromJsonCast('ID', this.prefix, propName)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
tsv(queryName: string): string {
|
|
701
|
+
assert(this.object.kind == 'entity')
|
|
702
|
+
return this.ident(this.alias) + '.' + this.ident(toSnakeCase(queryName) + '_tsv')
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
doc(queryName: string): string {
|
|
706
|
+
assert(this.object.kind == 'entity')
|
|
707
|
+
let query = getFtsQuery(this.model, queryName)
|
|
708
|
+
let src = query.sources.find(src => src.entity == this.name)
|
|
709
|
+
assert(src != null)
|
|
710
|
+
return src.fields.map(f => `coalesce(${this.field(f)}, '')`).join(` || E'\\n\\n' || `)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class ColumnSet {
|
|
716
|
+
private columns: Map<string, number> = new Map()
|
|
717
|
+
|
|
718
|
+
add(column: string): number {
|
|
719
|
+
let idx = this.columns.get(column)
|
|
720
|
+
if (idx == null) {
|
|
721
|
+
idx = this.columns.size
|
|
722
|
+
this.columns.set(column, idx)
|
|
723
|
+
}
|
|
724
|
+
return idx
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
render(): string {
|
|
728
|
+
return Array.from(this.columns.keys()).join(', ')
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
size(): number {
|
|
732
|
+
return this.columns.size
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* LEFT OUTER JOIN "{table}" "{alias}" ON "{alias}".{column} = {rhs}
|
|
739
|
+
*/
|
|
740
|
+
interface Join {
|
|
741
|
+
table: string
|
|
742
|
+
alias: string
|
|
743
|
+
column: string
|
|
744
|
+
rhs: string
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class JoinSet {
|
|
749
|
+
private joins: Map<string, Join> = new Map()
|
|
750
|
+
|
|
751
|
+
constructor(private aliases: AliasSet) {}
|
|
752
|
+
|
|
753
|
+
add(table: string, column: string, rhs: string): string {
|
|
754
|
+
let key = `${table} ${column} ${rhs}`
|
|
755
|
+
let e = this.joins.get(key)
|
|
756
|
+
if (!e) {
|
|
757
|
+
e = {
|
|
758
|
+
table,
|
|
759
|
+
alias: this.aliases.add(table),
|
|
760
|
+
column,
|
|
761
|
+
rhs
|
|
762
|
+
}
|
|
763
|
+
this.joins.set(key, e)
|
|
764
|
+
}
|
|
765
|
+
return e.alias
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
forEach(cb: (join: Join) => void): void {
|
|
769
|
+
this.joins.forEach(join => cb(join))
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class AliasSet {
|
|
775
|
+
private aliases: Record<string, number> = {}
|
|
776
|
+
|
|
777
|
+
add(name: string): string {
|
|
778
|
+
if (this.aliases[name]) {
|
|
779
|
+
return name + '_' + (this.aliases[name]++)
|
|
780
|
+
} else {
|
|
781
|
+
this.aliases[name] = 1
|
|
782
|
+
return name
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|