@subsquid/openreader 0.4.1 → 0.6.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 (104) hide show
  1. package/dist/dialect.d.ts +2 -0
  2. package/dist/dialect.d.ts.map +1 -0
  3. package/dist/dialect.js +3 -0
  4. package/dist/dialect.js.map +1 -0
  5. package/dist/gql/opencrud.d.ts +4 -3
  6. package/dist/gql/opencrud.d.ts.map +1 -1
  7. package/dist/gql/opencrud.js +22 -7
  8. package/dist/gql/opencrud.js.map +1 -1
  9. package/dist/gql/scalars/BigInt.d.ts +3 -0
  10. package/dist/gql/scalars/BigInt.d.ts.map +1 -0
  11. package/dist/gql/scalars/BigInt.js +36 -0
  12. package/dist/gql/scalars/BigInt.js.map +1 -0
  13. package/dist/gql/scalars/Bytes.d.ts +3 -0
  14. package/dist/gql/scalars/Bytes.d.ts.map +1 -0
  15. package/dist/gql/scalars/Bytes.js +32 -0
  16. package/dist/gql/scalars/Bytes.js.map +1 -0
  17. package/dist/gql/scalars/DateTime.d.ts +3 -0
  18. package/dist/gql/scalars/DateTime.d.ts.map +1 -0
  19. package/dist/gql/scalars/DateTime.js +44 -0
  20. package/dist/gql/scalars/DateTime.js.map +1 -0
  21. package/dist/gql/scalars/index.d.ts +6 -0
  22. package/dist/gql/scalars/index.d.ts.map +1 -0
  23. package/dist/gql/scalars/index.js +12 -0
  24. package/dist/gql/scalars/index.js.map +1 -0
  25. package/dist/gql/schema.d.ts.map +1 -1
  26. package/dist/gql/schema.js +3 -2
  27. package/dist/gql/schema.js.map +1 -1
  28. package/dist/queryBuilder.d.ts +3 -1
  29. package/dist/queryBuilder.d.ts.map +1 -1
  30. package/dist/queryBuilder.js +121 -25
  31. package/dist/queryBuilder.js.map +1 -1
  32. package/dist/resolver.d.ts +2 -1
  33. package/dist/resolver.d.ts.map +1 -1
  34. package/dist/resolver.js +12 -12
  35. package/dist/resolver.js.map +1 -1
  36. package/dist/server.d.ts +2 -0
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +3 -2
  39. package/dist/server.js.map +1 -1
  40. package/dist/test/basic.test.js +3 -3
  41. package/dist/test/basic.test.js.map +1 -1
  42. package/dist/test/connection.test.js +1 -1
  43. package/dist/test/connection.test.js.map +1 -1
  44. package/dist/test/fts.test.js +2 -2
  45. package/dist/test/fts.test.js.map +1 -1
  46. package/dist/test/isNull.test.d.ts +2 -0
  47. package/dist/test/isNull.test.d.ts.map +1 -0
  48. package/dist/test/isNull.test.js +75 -0
  49. package/dist/test/isNull.test.js.map +1 -0
  50. package/dist/test/lists.test.js +3 -3
  51. package/dist/test/lists.test.js.map +1 -1
  52. package/dist/test/lookup.test.js +13 -7
  53. package/dist/test/lookup.test.js.map +1 -1
  54. package/dist/test/scalars.test.js +12 -9
  55. package/dist/test/scalars.test.js.map +1 -1
  56. package/dist/test/{util/setup.d.ts → setup.d.ts} +8 -1
  57. package/dist/test/setup.d.ts.map +1 -0
  58. package/dist/test/{util/setup.js → setup.js} +19 -8
  59. package/dist/test/setup.js.map +1 -0
  60. package/dist/test/typed-json.test.js +1 -1
  61. package/dist/test/typed-json.test.js.map +1 -1
  62. package/dist/test/unions.test.js +1 -1
  63. package/dist/test/unions.test.js.map +1 -1
  64. package/dist/test/where.test.js +1 -1
  65. package/dist/test/where.test.js.map +1 -1
  66. package/dist/util.d.ts +1 -0
  67. package/dist/util.d.ts.map +1 -1
  68. package/dist/util.js +5 -1
  69. package/dist/util.js.map +1 -1
  70. package/dist/where.d.ts +1 -1
  71. package/dist/where.d.ts.map +1 -1
  72. package/dist/where.js +3 -0
  73. package/dist/where.js.map +1 -1
  74. package/package.json +4 -4
  75. package/src/dialect.ts +2 -0
  76. package/src/gql/opencrud.ts +24 -8
  77. package/src/gql/scalars/BigInt.ts +34 -0
  78. package/src/gql/scalars/Bytes.ts +28 -0
  79. package/src/gql/scalars/DateTime.ts +45 -0
  80. package/src/gql/scalars/index.ts +10 -0
  81. package/src/gql/schema.ts +3 -2
  82. package/src/queryBuilder.ts +114 -22
  83. package/src/resolver.ts +13 -11
  84. package/src/server.ts +5 -2
  85. package/src/test/basic.test.ts +3 -3
  86. package/src/test/connection.test.ts +1 -1
  87. package/src/test/fts.test.ts +4 -2
  88. package/src/test/isNull.test.ts +79 -0
  89. package/src/test/lists.test.ts +3 -3
  90. package/src/test/lookup.test.ts +13 -7
  91. package/src/test/scalars.test.ts +12 -9
  92. package/src/test/{util/setup.ts → setup.ts} +21 -7
  93. package/src/test/typed-json.test.ts +1 -1
  94. package/src/test/unions.test.ts +1 -1
  95. package/src/test/where.test.ts +1 -1
  96. package/src/util.ts +5 -0
  97. package/src/where.ts +5 -0
  98. package/dist/scalars.d.ts +0 -36
  99. package/dist/scalars.d.ts.map +0 -1
  100. package/dist/scalars.js +0 -229
  101. package/dist/scalars.js.map +0 -1
  102. package/dist/test/util/setup.d.ts.map +0 -1
  103. package/dist/test/util/setup.js.map +0 -1
  104. package/src/scalars.ts +0 -247
@@ -1,13 +1,14 @@
1
1
  import {Output, toCamelCase, toPlural} from "@subsquid/util"
2
2
  import assert from "assert"
3
3
  import {DocumentNode, parse, print} from "graphql"
4
- import {Entity, Enum, FTS_Query, Interface, JsonObject, Model, Prop, Union} from "../model"
4
+ import type {Dialect} from "../dialect"
5
+ import type {Entity, Enum, FTS_Query, Interface, JsonObject, Model, Prop, Union} from "../model"
5
6
  import {getOrderByMapping} from "../orderBy"
6
- import {scalars_list} from "../scalars"
7
7
  import {toQueryListField} from "../util"
8
+ import {customScalars} from "./scalars"
8
9
 
9
10
 
10
- export function generateOpenCrudQueries(model: Model): string {
11
+ export function generateOpenCrudQueries(model: Model, dialect: Dialect): string {
11
12
  let out = new Output()
12
13
 
13
14
  generatePageInfoType()
@@ -39,6 +40,7 @@ export function generateOpenCrudQueries(model: Model): string {
39
40
  generateEnumType(name, item)
40
41
  break
41
42
  case 'fts':
43
+ assert(dialect == 'postgres', `Full-text search queries are not supported by ${dialect}`)
42
44
  generateFtsTypes(name, item)
43
45
  break
44
46
  }
@@ -141,9 +143,11 @@ export function generateOpenCrudQueries(model: Model): string {
141
143
  switch(prop.type.kind) {
142
144
  case 'scalar':
143
145
  case 'enum':
146
+ generateIsNullFilter(key, prop)
144
147
  generateScalarFilters(key, prop.type.name)
145
148
  break
146
149
  case 'list':
150
+ generateIsNullFilter(key, prop)
147
151
  if (prop.type.item.type.kind == 'scalar' || prop.type.item.type.kind == 'enum') {
148
152
  let item = prop.type.item.type.name
149
153
  out.line(`${key}_containsAll: [${item}!]`)
@@ -152,14 +156,17 @@ export function generateOpenCrudQueries(model: Model): string {
152
156
  }
153
157
  break
154
158
  case 'object':
159
+ generateIsNullFilter(key, prop)
155
160
  if (hasFilters(getObject(prop.type.name))) {
156
161
  out.line(`${key}: ${prop.type.name}WhereInput`)
157
162
  }
158
163
  break
159
164
  case 'union':
165
+ generateIsNullFilter(key, prop)
160
166
  out.line(`${key}: ${prop.type.name}WhereInput`)
161
167
  break
162
168
  case 'fk':
169
+ generateIsNullFilter(key, prop)
163
170
  out.line(`${key}: ${prop.type.foreignEntity}WhereInput`)
164
171
  break
165
172
  case 'lookup':
@@ -174,6 +181,11 @@ export function generateOpenCrudQueries(model: Model): string {
174
181
  }
175
182
  }
176
183
 
184
+ function generateIsNullFilter(key: string, prop: Prop): void {
185
+ if (!prop.nullable) return
186
+ out.line(`${key}_isNull: Boolean`)
187
+ }
188
+
177
189
  function hasFilters(obj: JsonObject): boolean {
178
190
  for (let key in obj.properties) {
179
191
  let propType = obj.properties[key].type
@@ -182,10 +194,12 @@ export function generateOpenCrudQueries(model: Model): string {
182
194
  case 'enum':
183
195
  case 'union':
184
196
  return true
185
- case 'object':
186
- if (hasFilters(getObject(propType.name))) {
197
+ case 'object': {
198
+ let ref = getObject(propType.name)
199
+ if (ref !== obj && hasFilters(ref)) {
187
200
  return true
188
201
  }
202
+ }
189
203
  }
190
204
  }
191
205
  return false
@@ -238,6 +252,8 @@ export function generateOpenCrudQueries(model: Model): string {
238
252
  if (graphqlType == 'String' || graphqlType == 'ID') {
239
253
  out.line(`${fieldName}_contains: ${graphqlType}`)
240
254
  out.line(`${fieldName}_not_contains: ${graphqlType}`)
255
+ out.line(`${fieldName}_containsInsensitive: ${graphqlType}`)
256
+ out.line(`${fieldName}_not_containsInsensitive: ${graphqlType}`)
241
257
  out.line(`${fieldName}_startsWith: ${graphqlType}`)
242
258
  out.line(`${fieldName}_not_startsWith: ${graphqlType}`)
243
259
  out.line(`${fieldName}_endsWith: ${graphqlType}`)
@@ -321,8 +337,8 @@ export function generateOpenCrudQueries(model: Model): string {
321
337
  }
322
338
 
323
339
 
324
- export function buildServerSchema(model: Model): DocumentNode {
325
- let scalars = scalars_list.map(name => 'scalar ' + name).join('\n')
326
- let queries = generateOpenCrudQueries(model)
340
+ export function buildServerSchema(model: Model, dialect: Dialect): DocumentNode {
341
+ let scalars = ['ID'].concat(Object.keys(customScalars)).map(name => 'scalar ' + name).join('\n')
342
+ let queries = generateOpenCrudQueries(model, dialect)
327
343
  return parse(scalars + '\n\n' + queries)
328
344
  }
@@ -0,0 +1,34 @@
1
+ import {GraphQLScalarType} from "graphql"
2
+ import {invalidFormat} from "../../util"
3
+
4
+
5
+ export const BigIntScalar = new GraphQLScalarType({
6
+ name: 'BigInt',
7
+ description: 'Big number integer',
8
+ serialize(value: number | string | bigint) {
9
+ return ''+value
10
+ },
11
+ parseValue(value: string) {
12
+ if (!isBigInt(value)) throw invalidFormat('BigInt', value)
13
+ return BigInt(value)
14
+ },
15
+ parseLiteral(ast) {
16
+ switch(ast.kind) {
17
+ case "StringValue":
18
+ if (isBigInt(ast.value)) {
19
+ return BigInt(ast.value)
20
+ } else {
21
+ throw invalidFormat('BigInt', ast.value)
22
+ }
23
+ case "IntValue":
24
+ return BigInt(ast.value)
25
+ default:
26
+ return null
27
+ }
28
+ }
29
+ })
30
+
31
+
32
+ function isBigInt(s: string): boolean {
33
+ return /^[+\-]?\d+$/.test(s)
34
+ }
@@ -0,0 +1,28 @@
1
+ import {decodeHex, isHex} from "@subsquid/util"
2
+ import {GraphQLScalarType} from "graphql"
3
+ import {invalidFormat} from "../../util"
4
+
5
+
6
+ export const BytesScalar = new GraphQLScalarType({
7
+ name: 'Bytes',
8
+ description: 'Binary data encoded as a hex string always prefixed with 0x',
9
+ serialize(value: string | Buffer) {
10
+ if (typeof value == 'string') {
11
+ if (!isHex(value)) throw invalidFormat('Bytes', value)
12
+ return value.toLowerCase()
13
+ } else {
14
+ return '0x' + value.toString('hex')
15
+ }
16
+ },
17
+ parseValue(value: string) {
18
+ return decodeHex(value)
19
+ },
20
+ parseLiteral(ast) {
21
+ switch(ast.kind) {
22
+ case "StringValue":
23
+ return decodeHex(ast.value)
24
+ default:
25
+ return null
26
+ }
27
+ }
28
+ })
@@ -0,0 +1,45 @@
1
+ import {GraphQLScalarType} from "graphql"
2
+ import {invalidFormat} from "../../util"
3
+
4
+
5
+ export const DateTimeScalar = new GraphQLScalarType({
6
+ name: 'DateTime',
7
+ description:
8
+ 'A date-time string in simplified extended ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ)',
9
+ serialize(value: Date | string) {
10
+ if (value instanceof Date) {
11
+ return value.toISOString()
12
+ } else {
13
+ if (!isIsoDateTimeString(value)) throw invalidFormat('DateTime', value)
14
+ return value
15
+ }
16
+ },
17
+ parseValue(value: string) {
18
+ return parseDateTime(value)
19
+ },
20
+ parseLiteral(ast) {
21
+ switch(ast.kind) {
22
+ case "StringValue":
23
+ return parseDateTime(ast.value)
24
+ default:
25
+ return null
26
+ }
27
+ }
28
+ })
29
+
30
+
31
+ // credit - https://github.com/Urigo/graphql-scalars/blob/91b4ea8df891be8af7904cf84751930cc0c6613d/src/scalars/iso-date/validator.ts#L122
32
+ const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?([Z])$/
33
+
34
+
35
+ function isIsoDateTimeString(s: string): boolean {
36
+ return RFC_3339_REGEX.test(s)
37
+ }
38
+
39
+
40
+ function parseDateTime(s: string): Date {
41
+ if (!isIsoDateTimeString(s)) throw invalidFormat('DateTime', s)
42
+ let timestamp = Date.parse(s)
43
+ if (isNaN(timestamp)) throw invalidFormat('DateTime', s)
44
+ return new Date(timestamp)
45
+ }
@@ -0,0 +1,10 @@
1
+ import {BigIntScalar} from "./BigInt"
2
+ import {BytesScalar} from "./Bytes"
3
+ import {DateTimeScalar} from "./DateTime"
4
+
5
+
6
+ export const customScalars = {
7
+ BigInt: BigIntScalar,
8
+ Bytes: BytesScalar,
9
+ DateTime: DateTimeScalar
10
+ }
package/src/gql/schema.ts CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  } from "graphql"
21
21
  import {Index, Model, Prop, PropType} from "../model"
22
22
  import {validateModel} from "../model.tools"
23
- import {scalars_list} from "../scalars"
23
+ import {customScalars} from "./scalars"
24
24
 
25
25
 
26
26
  const baseSchema = buildASTSchema(parse(`
@@ -31,7 +31,8 @@ const baseSchema = buildASTSchema(parse(`
31
31
  directive @fulltext(query: String!) on FIELD_DEFINITION
32
32
  directive @variant on OBJECT # legacy
33
33
  directive @jsonField on OBJECT # legacy
34
- ${scalars_list.map(name => 'scalar ' + name).join('\n')}
34
+ scalar ID
35
+ ${Object.keys(customScalars).map(name => 'scalar ' + name).join('\n')}
35
36
  `))
36
37
 
37
38
 
@@ -1,12 +1,11 @@
1
1
  import {toSnakeCase} from "@subsquid/util"
2
2
  import assert from "assert"
3
- import type {ClientBase, QueryArrayResult} from "pg"
4
3
  import {Database} from "./db"
4
+ import type {Dialect} from "./dialect"
5
5
  import type {Entity, JsonObject, Model} from "./model"
6
6
  import {getEntity, getFtsQuery, getObject, getUnionProps} from "./model.tools"
7
7
  import {OpenCrudOrderByValue, OrderBy, parseOrderBy} from "./orderBy"
8
8
  import type {FtsRequestedFields, RequestedFields} from "./requestedFields"
9
- import {fromJsonCast, fromJsonToOutputCast, toOutputArrayCast, toOutputCast} from "./scalars"
10
9
  import {ensureArray, toColumn, toFkColumn, toInt, toTable, unsupportedCase} from "./util"
11
10
  import {hasConditions, parseWhereField, WhereOp, whereOpToSqlOperator} from "./where"
12
11
 
@@ -25,6 +24,7 @@ export class QueryBuilder {
25
24
 
26
25
  constructor(
27
26
  private model: Model,
27
+ private dialect: Dialect,
28
28
  private db: Database
29
29
  ) {}
30
30
 
@@ -44,6 +44,7 @@ export class QueryBuilder {
44
44
 
45
45
  let cursor = new Cursor(
46
46
  this.model,
47
+ this.dialect,
47
48
  this.ident.bind(this),
48
49
  this.aliases,
49
50
  join,
@@ -76,12 +77,12 @@ export class QueryBuilder {
76
77
  break
77
78
  case 'list-subquery':
78
79
  if (columns.size()) {
79
- out += `SELECT json_build_array(${columns.render()}) `
80
+ out += `SELECT json_build_array(${columns.render()}) AS row `
80
81
  }
81
82
  break
82
83
  default:
83
84
  if (columns.size()) {
84
- out += `SELECT ${columns.render()}\n`
85
+ out += `SELECT ${columns.render(true)}\n`
85
86
  }
86
87
  }
87
88
 
@@ -178,7 +179,7 @@ export class QueryBuilder {
178
179
  case 'scalar':
179
180
  case 'enum':
180
181
  case 'list':
181
- req.index = columns.add(cursor.transport(fieldName))
182
+ req.index = columns.add(cursor.output(fieldName))
182
183
  break
183
184
  case 'object':
184
185
  req.index = columns.add(cursor.field(fieldName) + ' IS NULL')
@@ -190,7 +191,7 @@ export class QueryBuilder {
190
191
  break
191
192
  case 'union':
192
193
  let cu = cursor.child(fieldName)
193
- req.index = columns.add(cu.transport('isTypeOf'))
194
+ req.index = columns.add(cu.output('isTypeOf'))
194
195
  this.populateColumns(
195
196
  columns,
196
197
  cu,
@@ -200,7 +201,7 @@ export class QueryBuilder {
200
201
  case 'fk':
201
202
  case 'lookup': {
202
203
  let cu = cursor.child(fieldName)
203
- req.index = columns.add(cu.transport('id'))
204
+ req.index = columns.add(cu.output('id'))
204
205
  this.populateColumns(
205
206
  columns,
206
207
  cu,
@@ -210,11 +211,11 @@ export class QueryBuilder {
210
211
  }
211
212
  case 'list-lookup':
212
213
  req.index = columns.add(
213
- 'array(' + this.select(field.propType.entity, req.args, req.children, {
214
+ '(SELECT jsonb_agg(row) FROM (' + this.select(field.propType.entity, req.args, req.children, {
214
215
  kind: 'list-subquery',
215
216
  field: field.propType.field,
216
217
  parent: cursor.native('id')
217
- }) + ')'
218
+ }) + ') as rows)'
218
219
  )
219
220
  break
220
221
  default:
@@ -304,6 +305,15 @@ export class QueryBuilder {
304
305
 
305
306
  private addPropCondition(exps: string[], cursor: Cursor, field: string, op: WhereOp, arg: any): void {
306
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
+ }
307
317
  switch(propType.kind) {
308
318
  case 'scalar':
309
319
  case 'enum': {
@@ -321,18 +331,29 @@ export class QueryBuilder {
321
331
  break
322
332
  }
323
333
  case 'startsWith':
324
- exps.push(`starts_with(${lhs}, ${this.param(arg)})`)
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
+ }
325
341
  break
326
342
  case 'not_startsWith':
327
- exps.push(`NOT starts_with(${lhs}, ${this.param(arg)})`)
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
+ }
328
349
  break
329
350
  case 'endsWith': {
330
- let param = this.param(arg)
351
+ let param = this.param(arg) + '::text'
331
352
  exps.push(`right(${lhs}, length(${param})) = ${param}`)
332
353
  break
333
354
  }
334
355
  case 'not_endsWith': {
335
- let param = this.param(arg)
356
+ let param = this.param(arg) + '::text'
336
357
  exps.push(`right(${lhs}, length(${param})) != ${param}`)
337
358
  break
338
359
  }
@@ -342,6 +363,12 @@ export class QueryBuilder {
342
363
  case 'not_contains':
343
364
  exps.push(`position(${this.param(arg)} in ${lhs}) = 0`)
344
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
345
372
  default: {
346
373
  exps.push(`${lhs} ${whereOpToSqlOperator(op)} ${this.param(arg)}`)
347
374
  }
@@ -570,11 +597,12 @@ interface ListSubquery {
570
597
  * A pointer to an entity or nested json object within SQL query.
571
598
  *
572
599
  * It has convenience methods for building various SQL expressions
573
- * related to individual properties of an entity or an object it points to.
600
+ * related to individual properties of an entity or of an object it points to.
574
601
  */
575
602
  class Cursor {
576
603
  constructor(
577
604
  private model: Model,
605
+ private dialect: Dialect,
578
606
  private ident: (name: string) => string,
579
607
  private aliases: AliasSet,
580
608
  private join: JoinSet,
@@ -584,15 +612,43 @@ class Cursor {
584
612
  private prefix: string
585
613
  ) {}
586
614
 
587
- transport(propName: string): string {
615
+ output(propName: string): string {
588
616
  let prop = this.object.properties[propName]
589
617
  switch(prop.type.kind) {
590
618
  case 'scalar':
619
+ if (this.object.kind == 'object') {
620
+ switch(prop.type.name) {
621
+ case 'Int':
622
+ return `(${this.prefix}->'${propName}')::integer`
623
+ case 'Float':
624
+ return `(${this.prefix}->'${propName}')::numeric`
625
+ case 'Boolean':
626
+ return `(${this.prefix}->>'${propName}')::bool`
627
+ default:
628
+ return `${this.prefix}->>'${propName}'`
629
+ }
630
+ } else {
631
+ let exp = this.column(propName)
632
+ switch(prop.type.name) {
633
+ case 'BigInt':
634
+ return `(${exp})::text`
635
+ case 'Bytes':
636
+ return `'0x' || encode(${exp}, 'hex')`
637
+ case 'DateTime':
638
+ if (this.dialect == 'cockroach') {
639
+ return `experimental_strftime((${exp}) at time zone 'UTC', '%Y-%m-%dT%H:%M:%S.%fZ')`
640
+ } else {
641
+ return `to_char((${exp}) at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')`
642
+ }
643
+ default:
644
+ return exp
645
+ }
646
+ }
591
647
  case 'enum':
592
648
  if (this.object.kind == 'object') {
593
- return fromJsonToOutputCast(prop.type.name, this.prefix, propName)
649
+ return `${this.prefix}->>'${propName}'`
594
650
  } else {
595
- return toOutputCast(prop.type.name, this.column(propName))
651
+ return this.column(propName)
596
652
  }
597
653
  case 'list':
598
654
  let itemType = prop.type.item.type
@@ -600,7 +656,21 @@ class Cursor {
600
656
  // this is json
601
657
  return this.field(propName)
602
658
  } else {
603
- return toOutputArrayCast(itemType.name, this.column(propName))
659
+ let exp = this.column(propName)
660
+ switch(itemType.name) {
661
+ case 'BigInt':
662
+ return `(${exp})::text[]`
663
+ case 'Bytes':
664
+ return `array(select '0x' || encode(i, 'hex') from unnest(${exp}) as i)`
665
+ case 'DateTime':
666
+ if (this.dialect == 'cockroach') {
667
+ return `array(select experimental_strftime(i at time zone 'UTC', '%Y-%m-%dT%H:%M:%S.%fZ') from unnest(${exp}) as i)`
668
+ } else {
669
+ return `array(select to_char(i at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') from unnest(${exp}) as i)`
670
+ }
671
+ default:
672
+ return exp
673
+ }
604
674
  }
605
675
  default:
606
676
  throw unsupportedCase(prop.type.kind)
@@ -616,7 +686,24 @@ class Cursor {
616
686
  }
617
687
  assert(prop.type.kind == 'scalar' || prop.type.kind == 'enum')
618
688
  if (this.object.kind == 'object') {
619
- return fromJsonCast(prop.type.name, this.prefix, propName)
689
+ let js = `${this.prefix}->'${propName}'`
690
+ let str = `${this.prefix}->>'${propName}'`
691
+ switch(prop.type.name) {
692
+ case 'Int':
693
+ return `(${js})::integer`
694
+ case 'Float':
695
+ return `(${js})::numeric`
696
+ case 'Boolean':
697
+ return `(${str})::bool`
698
+ case 'BigInt':
699
+ return `(${str})::numeric`
700
+ case 'Bytes':
701
+ return `decode(substr(${str}, 3), 'hex')`
702
+ case 'DateTime':
703
+ return `(${str})::timestamptz`
704
+ default:
705
+ return str
706
+ }
620
707
  } else {
621
708
  return this.column(propName)
622
709
  }
@@ -668,6 +755,7 @@ class Cursor {
668
755
 
669
756
  return new Cursor(
670
757
  this.model,
758
+ this.dialect,
671
759
  this.ident,
672
760
  this.aliases,
673
761
  this.join,
@@ -694,7 +782,7 @@ class Cursor {
694
782
  fk(propName: string): string {
695
783
  return this.object.kind == 'entity'
696
784
  ? this.ident(this.alias) + '.' + this.ident(toFkColumn(propName))
697
- : fromJsonCast('ID', this.prefix, propName)
785
+ : `${this.prefix}->>'${propName}'`
698
786
  }
699
787
 
700
788
  tsv(queryName: string): string {
@@ -724,8 +812,12 @@ class ColumnSet {
724
812
  return idx
725
813
  }
726
814
 
727
- render(): string {
728
- return Array.from(this.columns.keys()).join(', ')
815
+ render(withAliases?: boolean): string {
816
+ let cols = Array.from(this.columns.keys())
817
+ if (withAliases) {
818
+ cols = cols.map((col, idx) => `${col} AS _c${idx}`)
819
+ }
820
+ return cols.join(', ')
729
821
  }
730
822
 
731
823
  size(): number {
package/src/resolver.ts CHANGED
@@ -4,6 +4,8 @@ import {UserInputError} from "apollo-server-core"
4
4
  import assert from "assert"
5
5
  import type {GraphQLResolveInfo} from "graphql"
6
6
  import type {Database, Transaction} from "./db"
7
+ import type {Dialect} from "./dialect"
8
+ import {customScalars} from "./gql/scalars"
7
9
  import type {Entity, JsonObject, Model} from "./model"
8
10
  import {QueryBuilder} from "./queryBuilder"
9
11
  import {
@@ -15,7 +17,6 @@ import {
15
17
  PageInfo
16
18
  } from "./relayConnection"
17
19
  import {connectionRequestedFields, ftsRequestedFields, requestedFields} from "./requestedFields"
18
- import {getScalarResolvers} from "./scalars"
19
20
  import {ensureArray, toQueryListField, unsupportedCase} from "./util"
20
21
 
21
22
 
@@ -24,9 +25,9 @@ export interface ResolverContext {
24
25
  }
25
26
 
26
27
 
27
- export function buildResolvers(model: Model): IResolvers<unknown, ResolverContext> {
28
+ export function buildResolvers(model: Model, dialect: Dialect): IResolvers<unknown, ResolverContext> {
28
29
  let Query: Record<string, IFieldResolver<unknown, ResolverContext>> = {}
29
- let resolvers: IResolvers = {Query, ...getScalarResolvers()}
30
+ let resolvers: IResolvers = {Query, ...customScalars}
30
31
 
31
32
  for (let name in model) {
32
33
  let item = model[name]
@@ -35,16 +36,16 @@ export function buildResolvers(model: Model): IResolvers<unknown, ResolverContex
35
36
  Query[toQueryListField(name)] = async (source, args, context, info) => {
36
37
  let fields = requestedFields(model, name, info)
37
38
  let db = await context.openReaderTransaction.get()
38
- return new QueryBuilder(model, db).executeSelect(name, args, fields)
39
+ return new QueryBuilder(model, dialect, db).executeSelect(name, args, fields)
39
40
  }
40
41
  Query[toQueryListField(name) + 'Connection'] = async (source, args, context, info) => {
41
42
  let db = await context.openReaderTransaction.get()
42
- return resolveEntityConnection(model, name, args, info, db)
43
+ return resolveEntityConnection(model, dialect, name, args, info, db)
43
44
  }
44
45
  Query[`${toCamelCase(name)}ById`] = async (source, args, context, info) => {
45
46
  let fields = requestedFields(model, name, info)
46
47
  let db = await context.openReaderTransaction.get()
47
- let result = await new QueryBuilder(model, db)
48
+ let result = await new QueryBuilder(model, dialect, db)
48
49
  .executeSelect(name, {where: {id_eq: args.id}}, fields)
49
50
  assert(result.length < 2)
50
51
  return result[0]
@@ -52,7 +53,7 @@ export function buildResolvers(model: Model): IResolvers<unknown, ResolverContex
52
53
  Query[`${toCamelCase(name)}ByUniqueInput`] = async (source, args, context, info) => {
53
54
  let fields = requestedFields(model, name, info)
54
55
  let db = await context.openReaderTransaction.get()
55
- let result = await new QueryBuilder(model, db)
56
+ let result = await new QueryBuilder(model, dialect, db)
56
57
  .executeSelect(name, {where: {id_eq: args.where.id}}, fields)
57
58
  assert(result.length < 2)
58
59
  return result[0]
@@ -71,7 +72,7 @@ export function buildResolvers(model: Model): IResolvers<unknown, ResolverContex
71
72
  Query[name] = async (source, args, context, info) => {
72
73
  let fields = ftsRequestedFields(model, name, info)
73
74
  let db = await context.openReaderTransaction.get()
74
- return new QueryBuilder(model, db).executeFulltextSearch(name, args, fields)
75
+ return new QueryBuilder(model, dialect, db).executeFulltextSearch(name, args, fields)
75
76
  }
76
77
  resolvers[`${name}_Item`] = {
77
78
  __resolveType: resolveUnionType
@@ -130,6 +131,7 @@ interface ConnectionResponse extends RelayConnectionResponse<any> {
130
131
 
131
132
  async function resolveEntityConnection(
132
133
  model: Model,
134
+ dialect: Dialect,
133
135
  entityName: string,
134
136
  args: ConnectionArgs,
135
137
  info: GraphQLResolveInfo,
@@ -162,7 +164,7 @@ async function resolveEntityConnection(
162
164
 
163
165
  let fields = connectionRequestedFields(model, entityName, info)
164
166
  if (fields.edges?.node) {
165
- let nodes = await new QueryBuilder(model, db).executeSelect(entityName, listArgs, fields.edges.node)
167
+ let nodes = await new QueryBuilder(model, dialect, db).executeSelect(entityName, listArgs, fields.edges.node)
166
168
  let edges: ConnectionEdge<any>[] = new Array(Math.min(limit, nodes.length))
167
169
  for (let i = 0; i < edges.length; i++) {
168
170
  edges[i] = {
@@ -176,7 +178,7 @@ async function resolveEntityConnection(
176
178
  response.totalCount = offset + nodes.length
177
179
  }
178
180
  } else if (fields.edges?.cursor || fields.pageInfo) {
179
- let listLength = await new QueryBuilder(model, db).executeListCount(entityName, listArgs)
181
+ let listLength = await new QueryBuilder(model, dialect, db).executeListCount(entityName, listArgs)
180
182
  response.pageInfo = pageInfo(listLength)
181
183
  if (fields.edges?.cursor) {
182
184
  response.edges = []
@@ -192,7 +194,7 @@ async function resolveEntityConnection(
192
194
  }
193
195
 
194
196
  if (fields.totalCount && response.totalCount == null) {
195
- response.totalCount = await new QueryBuilder(model, db).executeSelectCount(entityName, listArgs.where)
197
+ response.totalCount = await new QueryBuilder(model, dialect, db).executeSelectCount(entityName, listArgs.where)
196
198
  }
197
199
 
198
200
  return response
package/src/server.ts CHANGED
@@ -7,6 +7,7 @@ import http from "http"
7
7
  import path from "path"
8
8
  import type {Pool} from "pg"
9
9
  import {PoolTransaction} from "./db"
10
+ import {Dialect} from "./dialect"
10
11
  import {buildServerSchema} from "./gql/opencrud"
11
12
  import type {Model} from "./model"
12
13
  import {buildResolvers} from "./resolver"
@@ -22,14 +23,16 @@ export interface ServerOptions {
22
23
  model: Model
23
24
  db: Pool
24
25
  port: number | string
26
+ dialect?: Dialect
25
27
  graphiqlConsole?: boolean
26
28
  }
27
29
 
28
30
 
29
31
  export async function serve(options: ServerOptions): Promise<ListeningServer> {
30
32
  let {model, db} = options
31
- let resolvers = buildResolvers(model)
32
- let typeDefs = buildServerSchema(model)
33
+ let dialect = options.dialect ?? 'postgres'
34
+ let resolvers = buildResolvers(model, dialect)
35
+ let typeDefs = buildServerSchema(model, dialect)
33
36
  let app = express()
34
37
  let server = http.createServer(app)
35
38
 
@@ -1,4 +1,4 @@
1
- import {useDatabase, useServer} from "./util/setup"
1
+ import {useDatabase, useServer} from "./setup"
2
2
 
3
3
 
4
4
  describe('basic tests', function() {
@@ -44,11 +44,11 @@ describe('basic tests', function() {
44
44
  it('can fetch all accounts', function() {
45
45
  return client.test(
46
46
  `query {
47
- accounts {
47
+ accounts(orderBy: id_ASC) {
48
48
  id
49
49
  wallet
50
50
  balance
51
- history { balance }
51
+ history(orderBy: id_ASC) { balance }
52
52
  }
53
53
  }`,
54
54
  {