@subsquid/openreader 0.5.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.
- package/dist/dialect.d.ts +2 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +3 -0
- package/dist/dialect.js.map +1 -0
- package/dist/gql/opencrud.d.ts +4 -3
- package/dist/gql/opencrud.d.ts.map +1 -1
- package/dist/gql/opencrud.js +8 -5
- package/dist/gql/opencrud.js.map +1 -1
- package/dist/gql/scalars/BigInt.d.ts +3 -0
- package/dist/gql/scalars/BigInt.d.ts.map +1 -0
- package/dist/gql/scalars/BigInt.js +36 -0
- package/dist/gql/scalars/BigInt.js.map +1 -0
- package/dist/gql/scalars/Bytes.d.ts +3 -0
- package/dist/gql/scalars/Bytes.d.ts.map +1 -0
- package/dist/gql/scalars/Bytes.js +32 -0
- package/dist/gql/scalars/Bytes.js.map +1 -0
- package/dist/gql/scalars/DateTime.d.ts +3 -0
- package/dist/gql/scalars/DateTime.d.ts.map +1 -0
- package/dist/gql/scalars/DateTime.js +44 -0
- package/dist/gql/scalars/DateTime.js.map +1 -0
- package/dist/gql/scalars/index.d.ts +6 -0
- package/dist/gql/scalars/index.d.ts.map +1 -0
- package/dist/gql/scalars/index.js +12 -0
- package/dist/gql/scalars/index.js.map +1 -0
- package/dist/gql/schema.d.ts.map +1 -1
- package/dist/gql/schema.js +3 -2
- package/dist/gql/schema.js.map +1 -1
- package/dist/queryBuilder.d.ts +3 -1
- package/dist/queryBuilder.d.ts.map +1 -1
- package/dist/queryBuilder.js +104 -22
- package/dist/queryBuilder.js.map +1 -1
- package/dist/resolver.d.ts +2 -1
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +12 -12
- package/dist/resolver.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -2
- package/dist/server.js.map +1 -1
- package/dist/test/basic.test.js +3 -3
- package/dist/test/basic.test.js.map +1 -1
- package/dist/test/connection.test.js +1 -1
- package/dist/test/connection.test.js.map +1 -1
- package/dist/test/fts.test.js +2 -2
- package/dist/test/fts.test.js.map +1 -1
- package/dist/test/isNull.test.js +1 -1
- package/dist/test/isNull.test.js.map +1 -1
- package/dist/test/lists.test.js +3 -3
- package/dist/test/lists.test.js.map +1 -1
- package/dist/test/lookup.test.js +13 -7
- package/dist/test/lookup.test.js.map +1 -1
- package/dist/test/scalars.test.js +12 -9
- package/dist/test/scalars.test.js.map +1 -1
- package/dist/test/{util/setup.d.ts → setup.d.ts} +8 -1
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/{util/setup.js → setup.js} +19 -8
- package/dist/test/setup.js.map +1 -0
- package/dist/test/typed-json.test.js +1 -1
- package/dist/test/typed-json.test.js.map +1 -1
- package/dist/test/unions.test.js +1 -1
- package/dist/test/unions.test.js.map +1 -1
- package/dist/test/where.test.js +1 -1
- package/dist/test/where.test.js.map +1 -1
- package/dist/util.d.ts +1 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +5 -1
- package/dist/util.js.map +1 -1
- package/dist/where.d.ts +1 -1
- package/dist/where.d.ts.map +1 -1
- package/dist/where.js +2 -0
- package/dist/where.js.map +1 -1
- package/package.json +4 -4
- package/src/dialect.ts +2 -0
- package/src/gql/opencrud.ts +10 -6
- package/src/gql/scalars/BigInt.ts +34 -0
- package/src/gql/scalars/Bytes.ts +28 -0
- package/src/gql/scalars/DateTime.ts +45 -0
- package/src/gql/scalars/index.ts +10 -0
- package/src/gql/schema.ts +3 -2
- package/src/queryBuilder.ts +98 -18
- package/src/resolver.ts +13 -11
- package/src/server.ts +5 -2
- package/src/test/basic.test.ts +3 -3
- package/src/test/connection.test.ts +1 -1
- package/src/test/fts.test.ts +4 -2
- package/src/test/isNull.test.ts +1 -1
- package/src/test/lists.test.ts +3 -3
- package/src/test/lookup.test.ts +13 -7
- package/src/test/scalars.test.ts +12 -9
- package/src/test/{util/setup.ts → setup.ts} +21 -7
- package/src/test/typed-json.test.ts +1 -1
- package/src/test/unions.test.ts +1 -1
- package/src/test/where.test.ts +1 -1
- package/src/util.ts +5 -0
- package/src/where.ts +3 -0
- package/dist/scalars.d.ts +0 -36
- package/dist/scalars.d.ts.map +0 -1
- package/dist/scalars.js +0 -229
- package/dist/scalars.js.map +0 -1
- package/dist/test/util/setup.d.ts.map +0 -1
- package/dist/test/util/setup.js.map +0 -1
- package/src/scalars.ts +0 -247
|
@@ -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
|
+
}
|
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 {
|
|
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
|
-
|
|
34
|
+
scalar ID
|
|
35
|
+
${Object.keys(customScalars).map(name => 'scalar ' + name).join('\n')}
|
|
35
36
|
`))
|
|
36
37
|
|
|
37
38
|
|
package/src/queryBuilder.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {toSnakeCase} from "@subsquid/util"
|
|
2
2
|
import assert from "assert"
|
|
3
3
|
import {Database} from "./db"
|
|
4
|
+
import type {Dialect} from "./dialect"
|
|
4
5
|
import type {Entity, JsonObject, Model} from "./model"
|
|
5
6
|
import {getEntity, getFtsQuery, getObject, getUnionProps} from "./model.tools"
|
|
6
7
|
import {OpenCrudOrderByValue, OrderBy, parseOrderBy} from "./orderBy"
|
|
7
8
|
import type {FtsRequestedFields, RequestedFields} from "./requestedFields"
|
|
8
|
-
import {fromJsonCast, fromJsonToOutputCast, toOutputArrayCast, toOutputCast} from "./scalars"
|
|
9
9
|
import {ensureArray, toColumn, toFkColumn, toInt, toTable, unsupportedCase} from "./util"
|
|
10
10
|
import {hasConditions, parseWhereField, WhereOp, whereOpToSqlOperator} from "./where"
|
|
11
11
|
|
|
@@ -24,6 +24,7 @@ export class QueryBuilder {
|
|
|
24
24
|
|
|
25
25
|
constructor(
|
|
26
26
|
private model: Model,
|
|
27
|
+
private dialect: Dialect,
|
|
27
28
|
private db: Database
|
|
28
29
|
) {}
|
|
29
30
|
|
|
@@ -43,6 +44,7 @@ export class QueryBuilder {
|
|
|
43
44
|
|
|
44
45
|
let cursor = new Cursor(
|
|
45
46
|
this.model,
|
|
47
|
+
this.dialect,
|
|
46
48
|
this.ident.bind(this),
|
|
47
49
|
this.aliases,
|
|
48
50
|
join,
|
|
@@ -75,7 +77,7 @@ export class QueryBuilder {
|
|
|
75
77
|
break
|
|
76
78
|
case 'list-subquery':
|
|
77
79
|
if (columns.size()) {
|
|
78
|
-
out += `SELECT json_build_array(${columns.render()}) `
|
|
80
|
+
out += `SELECT json_build_array(${columns.render()}) AS row `
|
|
79
81
|
}
|
|
80
82
|
break
|
|
81
83
|
default:
|
|
@@ -177,7 +179,7 @@ export class QueryBuilder {
|
|
|
177
179
|
case 'scalar':
|
|
178
180
|
case 'enum':
|
|
179
181
|
case 'list':
|
|
180
|
-
req.index = columns.add(cursor.
|
|
182
|
+
req.index = columns.add(cursor.output(fieldName))
|
|
181
183
|
break
|
|
182
184
|
case 'object':
|
|
183
185
|
req.index = columns.add(cursor.field(fieldName) + ' IS NULL')
|
|
@@ -189,7 +191,7 @@ export class QueryBuilder {
|
|
|
189
191
|
break
|
|
190
192
|
case 'union':
|
|
191
193
|
let cu = cursor.child(fieldName)
|
|
192
|
-
req.index = columns.add(cu.
|
|
194
|
+
req.index = columns.add(cu.output('isTypeOf'))
|
|
193
195
|
this.populateColumns(
|
|
194
196
|
columns,
|
|
195
197
|
cu,
|
|
@@ -199,7 +201,7 @@ export class QueryBuilder {
|
|
|
199
201
|
case 'fk':
|
|
200
202
|
case 'lookup': {
|
|
201
203
|
let cu = cursor.child(fieldName)
|
|
202
|
-
req.index = columns.add(cu.
|
|
204
|
+
req.index = columns.add(cu.output('id'))
|
|
203
205
|
this.populateColumns(
|
|
204
206
|
columns,
|
|
205
207
|
cu,
|
|
@@ -209,11 +211,11 @@ export class QueryBuilder {
|
|
|
209
211
|
}
|
|
210
212
|
case 'list-lookup':
|
|
211
213
|
req.index = columns.add(
|
|
212
|
-
'
|
|
214
|
+
'(SELECT jsonb_agg(row) FROM (' + this.select(field.propType.entity, req.args, req.children, {
|
|
213
215
|
kind: 'list-subquery',
|
|
214
216
|
field: field.propType.field,
|
|
215
217
|
parent: cursor.native('id')
|
|
216
|
-
}) + ')'
|
|
218
|
+
}) + ') as rows)'
|
|
217
219
|
)
|
|
218
220
|
break
|
|
219
221
|
default:
|
|
@@ -329,18 +331,29 @@ export class QueryBuilder {
|
|
|
329
331
|
break
|
|
330
332
|
}
|
|
331
333
|
case 'startsWith':
|
|
332
|
-
|
|
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
|
+
}
|
|
333
341
|
break
|
|
334
342
|
case 'not_startsWith':
|
|
335
|
-
|
|
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
|
+
}
|
|
336
349
|
break
|
|
337
350
|
case 'endsWith': {
|
|
338
|
-
let param = this.param(arg)
|
|
351
|
+
let param = this.param(arg) + '::text'
|
|
339
352
|
exps.push(`right(${lhs}, length(${param})) = ${param}`)
|
|
340
353
|
break
|
|
341
354
|
}
|
|
342
355
|
case 'not_endsWith': {
|
|
343
|
-
let param = this.param(arg)
|
|
356
|
+
let param = this.param(arg) + '::text'
|
|
344
357
|
exps.push(`right(${lhs}, length(${param})) != ${param}`)
|
|
345
358
|
break
|
|
346
359
|
}
|
|
@@ -350,6 +363,12 @@ export class QueryBuilder {
|
|
|
350
363
|
case 'not_contains':
|
|
351
364
|
exps.push(`position(${this.param(arg)} in ${lhs}) = 0`)
|
|
352
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
|
|
353
372
|
default: {
|
|
354
373
|
exps.push(`${lhs} ${whereOpToSqlOperator(op)} ${this.param(arg)}`)
|
|
355
374
|
}
|
|
@@ -578,11 +597,12 @@ interface ListSubquery {
|
|
|
578
597
|
* A pointer to an entity or nested json object within SQL query.
|
|
579
598
|
*
|
|
580
599
|
* It has convenience methods for building various SQL expressions
|
|
581
|
-
* 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.
|
|
582
601
|
*/
|
|
583
602
|
class Cursor {
|
|
584
603
|
constructor(
|
|
585
604
|
private model: Model,
|
|
605
|
+
private dialect: Dialect,
|
|
586
606
|
private ident: (name: string) => string,
|
|
587
607
|
private aliases: AliasSet,
|
|
588
608
|
private join: JoinSet,
|
|
@@ -592,15 +612,43 @@ class Cursor {
|
|
|
592
612
|
private prefix: string
|
|
593
613
|
) {}
|
|
594
614
|
|
|
595
|
-
|
|
615
|
+
output(propName: string): string {
|
|
596
616
|
let prop = this.object.properties[propName]
|
|
597
617
|
switch(prop.type.kind) {
|
|
598
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
|
+
}
|
|
599
647
|
case 'enum':
|
|
600
648
|
if (this.object.kind == 'object') {
|
|
601
|
-
return
|
|
649
|
+
return `${this.prefix}->>'${propName}'`
|
|
602
650
|
} else {
|
|
603
|
-
return
|
|
651
|
+
return this.column(propName)
|
|
604
652
|
}
|
|
605
653
|
case 'list':
|
|
606
654
|
let itemType = prop.type.item.type
|
|
@@ -608,7 +656,21 @@ class Cursor {
|
|
|
608
656
|
// this is json
|
|
609
657
|
return this.field(propName)
|
|
610
658
|
} else {
|
|
611
|
-
|
|
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
|
+
}
|
|
612
674
|
}
|
|
613
675
|
default:
|
|
614
676
|
throw unsupportedCase(prop.type.kind)
|
|
@@ -624,7 +686,24 @@ class Cursor {
|
|
|
624
686
|
}
|
|
625
687
|
assert(prop.type.kind == 'scalar' || prop.type.kind == 'enum')
|
|
626
688
|
if (this.object.kind == 'object') {
|
|
627
|
-
|
|
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
|
+
}
|
|
628
707
|
} else {
|
|
629
708
|
return this.column(propName)
|
|
630
709
|
}
|
|
@@ -676,6 +755,7 @@ class Cursor {
|
|
|
676
755
|
|
|
677
756
|
return new Cursor(
|
|
678
757
|
this.model,
|
|
758
|
+
this.dialect,
|
|
679
759
|
this.ident,
|
|
680
760
|
this.aliases,
|
|
681
761
|
this.join,
|
|
@@ -702,7 +782,7 @@ class Cursor {
|
|
|
702
782
|
fk(propName: string): string {
|
|
703
783
|
return this.object.kind == 'entity'
|
|
704
784
|
? this.ident(this.alias) + '.' + this.ident(toFkColumn(propName))
|
|
705
|
-
:
|
|
785
|
+
: `${this.prefix}->>'${propName}'`
|
|
706
786
|
}
|
|
707
787
|
|
|
708
788
|
tsv(queryName: string): string {
|
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, ...
|
|
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
|
|
32
|
-
let
|
|
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
|
|
package/src/test/basic.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useDatabase, useServer} from "./
|
|
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
|
{
|
package/src/test/fts.test.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {useDatabase, useServer} from "./
|
|
1
|
+
import {isCockroach, useDatabase, useServer} from "./setup"
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
function tsvector(columns: string[]) {
|
|
4
5
|
return columns.map(col => `setweight(to_tsvector('english', coalesce(${col}, '')), 'A')`).join(' || ')
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
|
|
9
|
+
isCockroach() || describe('full text search', function () {
|
|
8
10
|
useDatabase([
|
|
9
11
|
`create table foo (
|
|
10
12
|
id text primary key,
|
package/src/test/isNull.test.ts
CHANGED
package/src/test/lists.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useDatabase, useServer} from "./
|
|
1
|
+
import {useDatabase, useServer} from "./setup"
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
describe('lists', function () {
|
|
@@ -172,8 +172,8 @@ describe('lists', function () {
|
|
|
172
172
|
`, {
|
|
173
173
|
lists: [{
|
|
174
174
|
datetimeArray: [
|
|
175
|
-
'2020-01-01T00:00:00.
|
|
176
|
-
'2021-01-01T00:00:00.
|
|
175
|
+
'2020-01-01T00:00:00.000000Z',
|
|
176
|
+
'2021-01-01T00:00:00.000000Z'
|
|
177
177
|
]
|
|
178
178
|
}]
|
|
179
179
|
})
|
package/src/test/lookup.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useDatabase, useServer} from "./
|
|
1
|
+
import {isCockroach, useDatabase, useServer} from "./setup"
|
|
2
2
|
|
|
3
3
|
describe('lookup test', function () {
|
|
4
4
|
useDatabase([
|
|
@@ -82,16 +82,22 @@ describe('lookup test', function () {
|
|
|
82
82
|
it('supports sorting on lookup fields', function () {
|
|
83
83
|
return client.test(`
|
|
84
84
|
query {
|
|
85
|
-
issues(orderBy: [
|
|
85
|
+
issues(orderBy: [payment_amount_ASC]) {
|
|
86
86
|
id
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
`, {
|
|
90
|
-
issues:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
issues: isCockroach()
|
|
91
|
+
? [
|
|
92
|
+
{id: '3'},
|
|
93
|
+
{id: '2'},
|
|
94
|
+
{id: '1'}
|
|
95
|
+
]
|
|
96
|
+
: [
|
|
97
|
+
{id: '2'},
|
|
98
|
+
{id: '1'},
|
|
99
|
+
{id: '3'}
|
|
100
|
+
]
|
|
95
101
|
})
|
|
96
102
|
})
|
|
97
103
|
|
package/src/test/scalars.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useDatabase, useServer} from "./
|
|
1
|
+
import {useDatabase, useServer} from "./setup"
|
|
2
2
|
|
|
3
3
|
describe('scalars', function() {
|
|
4
4
|
useDatabase([
|
|
@@ -12,6 +12,7 @@ describe('scalars', function() {
|
|
|
12
12
|
`insert into scalar (id, "string") values ('7', 'bar baz foo')`,
|
|
13
13
|
`insert into scalar (id, "string") values ('8', 'baz foo bar')`,
|
|
14
14
|
`insert into scalar (id, "string") values ('9', 'hello')`,
|
|
15
|
+
`insert into scalar (id, "string") values ('9-1', 'A fOo B')`,
|
|
15
16
|
`insert into scalar (id, "date_time", deep) values ('10', '2021-09-24T15:43:13.400Z', '{"dateTime": "2021-09-24T00:00:00.120Z"}'::jsonb)`,
|
|
16
17
|
`insert into scalar (id, "date_time", deep) values ('11', '2021-09-24T00:00:00.000Z', '{"dateTime": "2021-09-24T00:00:00Z"}'::jsonb)`,
|
|
17
18
|
`insert into scalar (id, "date_time", deep) values ('12', '2021-09-24 02:00:00.001 +01:00', '{"dateTime": "2021-09-24T00:00:00.1Z"}'::jsonb)`,
|
|
@@ -106,16 +107,18 @@ describe('scalars', function() {
|
|
|
106
107
|
not_ends_with: scalars(where: {string_not_endsWith: "foo"} orderBy: id_ASC) { id }
|
|
107
108
|
contains: scalars(where: {string_contains: "foo"} orderBy: id_ASC) { id }
|
|
108
109
|
not_contains: scalars(where: {string_not_contains: "foo"} orderBy: id_ASC) { id }
|
|
109
|
-
|
|
110
|
+
contains_insensitive: scalars(where: {string_containsInsensitive: "FoO"} orderBy: id_ASC) { id }
|
|
111
|
+
not_contains_insensitive: scalars(where: {string_not_containsInsensitive: "FoO"} orderBy: id_ASC) { id }
|
|
110
112
|
}
|
|
111
113
|
`, {
|
|
112
114
|
starts_with: [{id: '6'}],
|
|
113
|
-
not_starts_with: [{id: '7'}, {id: '8'}, {id: '9'}],
|
|
115
|
+
not_starts_with: [{id: '7'}, {id: '8'}, {id: '9'}, {id: '9-1'}],
|
|
114
116
|
ends_with: [{id: '7'}],
|
|
115
|
-
not_ends_with: [{id: '6'}, {id: '8'}, {id: '9'}],
|
|
117
|
+
not_ends_with: [{id: '6'}, {id: '8'}, {id: '9'}, {id: '9-1'}],
|
|
116
118
|
contains: [{id: '6'}, {id: '7'}, {id: '8'}],
|
|
117
|
-
not_contains: [{id: '9'}],
|
|
118
|
-
|
|
119
|
+
not_contains: [{id: '9'}, {id: '9-1'}],
|
|
120
|
+
contains_insensitive: [{id: '6'}, {id: '7'}, {id: '8'}, {id: '9-1'}],
|
|
121
|
+
not_contains_insensitive: [{id: '9'}]
|
|
119
122
|
})
|
|
120
123
|
})
|
|
121
124
|
})
|
|
@@ -239,9 +242,9 @@ describe('scalars', function() {
|
|
|
239
242
|
}
|
|
240
243
|
`, {
|
|
241
244
|
scalars: [
|
|
242
|
-
{id: '10', dateTime: '2021-09-24T15:43:13.
|
|
243
|
-
{id: '11', dateTime: '2021-09-24T00:00:00.
|
|
244
|
-
{id: '12', dateTime: '2021-09-24T01:00:00.
|
|
245
|
+
{id: '10', dateTime: '2021-09-24T15:43:13.400000Z', deep: {dateTime: '2021-09-24T00:00:00.120Z'}},
|
|
246
|
+
{id: '11', dateTime: '2021-09-24T00:00:00.000000Z', deep: {dateTime: '2021-09-24T00:00:00Z'}},
|
|
247
|
+
{id: '12', dateTime: '2021-09-24T01:00:00.001000Z', deep: {dateTime: '2021-09-24T00:00:00.1Z'}}
|
|
245
248
|
]
|
|
246
249
|
})
|
|
247
250
|
})
|
|
@@ -1,12 +1,25 @@
|
|
|
1
|
+
import {assertNotNull} from "@subsquid/util"
|
|
1
2
|
import {Client} from "gql-test-client"
|
|
2
3
|
import {parse} from "graphql"
|
|
3
4
|
import {Client as PgClient, ClientBase, Pool} from "pg"
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {ListeningServer, serve} from "../../server"
|
|
5
|
+
import {buildModel, buildSchema} from "../gql/schema"
|
|
6
|
+
import {ListeningServer, serve} from "../server"
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function isCockroach(): boolean {
|
|
10
|
+
return process.env.DB_TYPE == 'cockroach'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export const db_config = {
|
|
15
|
+
host: 'localhost',
|
|
16
|
+
port: parseInt(assertNotNull(
|
|
17
|
+
isCockroach() ? process.env.DB_PORT_COCKROACH : process.env.DB_PORT_PG
|
|
18
|
+
)),
|
|
19
|
+
user: 'root',
|
|
20
|
+
password: 'root',
|
|
21
|
+
database: 'defaultdb'
|
|
22
|
+
}
|
|
10
23
|
|
|
11
24
|
|
|
12
25
|
async function withClient(block: (client: ClientBase) => Promise<void>): Promise<void> {
|
|
@@ -31,8 +44,8 @@ export function databaseInit(sql: string[]): Promise<void> {
|
|
|
31
44
|
|
|
32
45
|
export function databaseDelete(): Promise<void> {
|
|
33
46
|
return withClient(async client => {
|
|
34
|
-
await client.query(`DROP SCHEMA IF EXISTS
|
|
35
|
-
await client.query(`CREATE SCHEMA
|
|
47
|
+
await client.query(`DROP SCHEMA IF EXISTS root CASCADE`)
|
|
48
|
+
await client.query(`CREATE SCHEMA root`)
|
|
36
49
|
})
|
|
37
50
|
}
|
|
38
51
|
|
|
@@ -53,7 +66,8 @@ export function useServer(schema: string): Client {
|
|
|
53
66
|
info = await serve({
|
|
54
67
|
db,
|
|
55
68
|
model: buildModel(buildSchema(parse(schema))),
|
|
56
|
-
port: 0
|
|
69
|
+
port: 0,
|
|
70
|
+
dialect: isCockroach() ? 'cockroach' : 'postgres'
|
|
57
71
|
})
|
|
58
72
|
client.endpoint = `http://localhost:${info.port}/graphql`
|
|
59
73
|
})
|