@subsquid/openreader 1.0.2 → 2.1.0

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