@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.
- 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 +22 -7
- 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 +121 -25
- 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.d.ts +2 -0
- package/dist/test/isNull.test.d.ts.map +1 -0
- package/dist/test/isNull.test.js +75 -0
- package/dist/test/isNull.test.js.map +1 -0
- 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 +3 -0
- package/dist/where.js.map +1 -1
- package/package.json +4 -4
- package/src/dialect.ts +2 -0
- package/src/gql/opencrud.ts +24 -8
- 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 +114 -22
- 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 +79 -0
- 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 +5 -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
package/src/gql/opencrud.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
+
}
|
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,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.
|
|
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.
|
|
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.
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
649
|
+
return `${this.prefix}->>'${propName}'`
|
|
594
650
|
} else {
|
|
595
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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, ...
|
|
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
|
{
|