orchid-graphql 0.0.4 → 1.0.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/README.md +6 -9
- package/dist/index.cjs +236 -185
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +119 -65
- package/dist/index.js +236 -185
- package/dist/index.js.map +1 -1
- package/package.json +13 -27
- package/src/index.ts +12 -12
- package/src/orm/orm.ts +112 -0
- package/src/resolvers/graph.ts +9 -78
- package/src/resolvers/table.ts +5 -138
- package/src/filters/filters.ts +0 -93
- package/src/paginators/base.ts +0 -40
- package/src/paginators/cursor.ts +0 -113
- package/src/resolvers/field.ts +0 -46
- package/src/resolvers/page.ts +0 -34
- package/src/resolvers/relation.ts +0 -28
- package/src/utils/is-plain-object.ts +0 -3
- package/src/utils/modifier.ts +0 -3
- package/src/utils/run-after.ts +0 -19
package/src/filters/filters.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { Query } from "pqb"
|
|
2
|
-
|
|
3
|
-
import { is_plain_object } from "../utils/is-plain-object"
|
|
4
|
-
import { Modifier } from "../utils/modifier"
|
|
5
|
-
|
|
6
|
-
// TODO: per-field definitions
|
|
7
|
-
export type FiltersDef = boolean
|
|
8
|
-
|
|
9
|
-
export type FilterScalarValue = null | string | number | boolean
|
|
10
|
-
export type FilterValue = FilterScalarValue | Exclude<FilterScalarValue, null>[]
|
|
11
|
-
export type Filter = { [property: string]: FilterValue }
|
|
12
|
-
|
|
13
|
-
function is_scalar(value: any) {
|
|
14
|
-
return (
|
|
15
|
-
value === null ||
|
|
16
|
-
typeof value === "string" ||
|
|
17
|
-
typeof value === "number" ||
|
|
18
|
-
typeof value === "boolean"
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function apply_filters({
|
|
23
|
-
query,
|
|
24
|
-
filters,
|
|
25
|
-
args,
|
|
26
|
-
modifiers,
|
|
27
|
-
}: {
|
|
28
|
-
query: Query
|
|
29
|
-
filters: FiltersDef
|
|
30
|
-
args?: Record<string, any>
|
|
31
|
-
modifiers?: Record<string, Modifier>
|
|
32
|
-
}) {
|
|
33
|
-
if (!filters) {
|
|
34
|
-
return query
|
|
35
|
-
}
|
|
36
|
-
const filter_obj = args?.filter
|
|
37
|
-
if (!filter_obj) {
|
|
38
|
-
return query
|
|
39
|
-
}
|
|
40
|
-
if (!is_plain_object(filter_obj)) {
|
|
41
|
-
throw new Error(`Invalid filter: ${filter_obj}, must be object.`)
|
|
42
|
-
}
|
|
43
|
-
for (const [field, value] of Object.entries(filter_obj)) {
|
|
44
|
-
if (value === undefined) {
|
|
45
|
-
// Support optional GraphQL arguments in filter
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
if (modifiers?.[field]) {
|
|
49
|
-
query = modifiers[field](query, value)
|
|
50
|
-
} else {
|
|
51
|
-
// Normal filter
|
|
52
|
-
const [dbfield, op0] = field.split("__")
|
|
53
|
-
const op = op0?.toLowerCase() || "exact"
|
|
54
|
-
if (
|
|
55
|
-
// TODO: add the rest of orchid ops (or allow all of them?)
|
|
56
|
-
[
|
|
57
|
-
"exact",
|
|
58
|
-
"lt",
|
|
59
|
-
"lte",
|
|
60
|
-
"gt",
|
|
61
|
-
"gte",
|
|
62
|
-
"like",
|
|
63
|
-
"ilike",
|
|
64
|
-
"contains",
|
|
65
|
-
].includes(op)
|
|
66
|
-
) {
|
|
67
|
-
if (!is_scalar(value)) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`Unsupported filter value for ${query.table}.${field}: must be scalar.`
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
if (op === "exact") {
|
|
73
|
-
query = query.where({ [dbfield]: value })
|
|
74
|
-
} else {
|
|
75
|
-
const query_op = { like: "contains" }[op] || op
|
|
76
|
-
query = query.where({ [dbfield]: { [query_op]: value } })
|
|
77
|
-
}
|
|
78
|
-
} else if (op === "in") {
|
|
79
|
-
if (!(Array.isArray(value) && value.every(is_scalar))) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`Invalid filter value for ${query.table}.${field}: must be an array of scalars.`
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
query = query.whereIn(dbfield, value)
|
|
85
|
-
} else {
|
|
86
|
-
throw new Error(
|
|
87
|
-
`Invalid filter ${query.table}.${field}: unsupported operator ${op}.`
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return query
|
|
93
|
-
}
|
package/src/paginators/base.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { Query } from "pqb"
|
|
2
|
-
|
|
3
|
-
export interface Paginator {
|
|
4
|
-
/** GraphQL path, for example: `["nodes"]` */
|
|
5
|
-
readonly path: string[]
|
|
6
|
-
paginate(query: Query, ...args: any[]): Query
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
type GetPageFn<P> = (nodes: any[]) => P
|
|
10
|
-
|
|
11
|
-
const s = Symbol()
|
|
12
|
-
|
|
13
|
-
// FIXME use proper orchid methods, set result type
|
|
14
|
-
export function set_query_page_result<P>(query: Query, get_page: GetPageFn<P>) {
|
|
15
|
-
function _get_page(data: any) {
|
|
16
|
-
if (!Array.isArray(data)) {
|
|
17
|
-
throw new Error(`Paginator called for single result query.`)
|
|
18
|
-
}
|
|
19
|
-
return get_page(data)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// For root query
|
|
23
|
-
const _query = query.query
|
|
24
|
-
const { handleResult } = _query
|
|
25
|
-
_query.handleResult = (q, result, s) => {
|
|
26
|
-
const data = handleResult(q, result, s)
|
|
27
|
-
// Sometimes this is called for subqueries, sometimes not.
|
|
28
|
-
// TODO: investigate and use always.
|
|
29
|
-
return s ? data : _get_page(data)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// For subqueries
|
|
33
|
-
;(_query as any)[s] = _get_page
|
|
34
|
-
|
|
35
|
-
return query
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function get_query_pagination_handler(query: Query) {
|
|
39
|
-
return (query.query as any)[s]
|
|
40
|
-
}
|
package/src/paginators/cursor.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { Query } from "pqb"
|
|
2
|
-
|
|
3
|
-
import { Paginator, set_query_page_result } from "./base"
|
|
4
|
-
|
|
5
|
-
export function defineCursorPaginator(
|
|
6
|
-
options: Partial<CursorPaginatorOptions> = {}
|
|
7
|
-
) {
|
|
8
|
-
return new CursorPaginator(options)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface CursorPaginatorOptions {
|
|
12
|
-
fields: string[]
|
|
13
|
-
take: number
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface CursorPaginatorArgs {
|
|
17
|
-
cursor?: string
|
|
18
|
-
take?: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface CursorPaginatorPage<M> {
|
|
22
|
-
nodes: M[]
|
|
23
|
-
cursor?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class CursorPaginator implements Paginator {
|
|
27
|
-
readonly path = ["nodes"]
|
|
28
|
-
readonly options: CursorPaginatorOptions
|
|
29
|
-
|
|
30
|
-
readonly fields: Array<{
|
|
31
|
-
name: string
|
|
32
|
-
desc: boolean
|
|
33
|
-
}>
|
|
34
|
-
|
|
35
|
-
constructor(options: Partial<CursorPaginatorOptions> = {}) {
|
|
36
|
-
this.options = {
|
|
37
|
-
fields: ["id"],
|
|
38
|
-
take: 10,
|
|
39
|
-
...options,
|
|
40
|
-
}
|
|
41
|
-
this.fields = this.options.fields.map((field) => {
|
|
42
|
-
if (field.startsWith("-")) {
|
|
43
|
-
return { name: field.slice(1), desc: true }
|
|
44
|
-
} else {
|
|
45
|
-
return { name: field, desc: false }
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
paginate(query: Query, args: CursorPaginatorArgs = {}) {
|
|
51
|
-
const take = args.take ?? this.options.take
|
|
52
|
-
const { cursor } = args
|
|
53
|
-
|
|
54
|
-
// Set query order
|
|
55
|
-
query = query.clear("order")
|
|
56
|
-
for (const field of this.fields) {
|
|
57
|
-
// TODO: prevent potential name clash with aliases like .as(`_${table_ref}_order_key_0`)
|
|
58
|
-
query = query
|
|
59
|
-
.select(field.name)
|
|
60
|
-
.order({ [field.name]: field.desc ? "DESC" : "ASC" })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (cursor) {
|
|
64
|
-
query = this._set_query_cursor(query, cursor)
|
|
65
|
-
}
|
|
66
|
-
query = query.limit(take + 1)
|
|
67
|
-
|
|
68
|
-
query = set_query_page_result(query, (nodes) => {
|
|
69
|
-
let cursor: string | undefined
|
|
70
|
-
if (nodes.length > take) {
|
|
71
|
-
cursor = this._create_cursor(nodes[take - 1])
|
|
72
|
-
nodes = nodes.slice(0, take)
|
|
73
|
-
}
|
|
74
|
-
return { nodes, cursor }
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
return query
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
_create_cursor(instance: any) {
|
|
81
|
-
return JSON.stringify(
|
|
82
|
-
this.fields.map((field) => {
|
|
83
|
-
const value = instance[field.name]
|
|
84
|
-
if (value === undefined) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`Unable to create cursor: undefined field ${field.name}`
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
return String(value)
|
|
90
|
-
})
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
_set_query_cursor(query: Query, cursor: string) {
|
|
95
|
-
const values = JSON.parse(cursor)
|
|
96
|
-
const left: string[] = []
|
|
97
|
-
const right: string[] = []
|
|
98
|
-
const expr_values: Record<string, any> = {}
|
|
99
|
-
// TODO: refactor
|
|
100
|
-
for (let i = 0; i < this.fields.length; ++i) {
|
|
101
|
-
const field = this.fields[i]
|
|
102
|
-
const expressions = field.desc ? right : left
|
|
103
|
-
const placeholders = field.desc ? left : right
|
|
104
|
-
expressions.push(`"${field.name}"`)
|
|
105
|
-
const placeholder = `value${i}`
|
|
106
|
-
placeholders.push("$" + placeholder)
|
|
107
|
-
expr_values[placeholder] = values[i]
|
|
108
|
-
}
|
|
109
|
-
return query.where(
|
|
110
|
-
query.raw(`(${left.join(",")}) > (${right.join(",")})`, expr_values)
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
}
|
package/src/resolvers/field.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Query } from "pqb"
|
|
2
|
-
|
|
3
|
-
import { run_after_query } from "../utils/run-after"
|
|
4
|
-
import { TableResolveContext } from "./table"
|
|
5
|
-
|
|
6
|
-
export interface FieldResolveContext extends TableResolveContext {
|
|
7
|
-
/** GraphQL field */
|
|
8
|
-
field: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/* A function that modifies the query to select a field. */
|
|
12
|
-
export type FieldResolverFn = (
|
|
13
|
-
query: Query,
|
|
14
|
-
context: FieldResolveContext
|
|
15
|
-
) => Query
|
|
16
|
-
|
|
17
|
-
export interface FieldResolverOptions {
|
|
18
|
-
/** Table field (if different from GraphQL field) */
|
|
19
|
-
tableField?: string
|
|
20
|
-
/** Custom query modifier (if different than simply selecting a field). */
|
|
21
|
-
modify?: FieldResolverFn
|
|
22
|
-
/** Post-process selected value. Return a new value or a promise. */
|
|
23
|
-
transform?(value: any, instance: any, context: FieldResolveContext): any
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function defineFieldResolver(
|
|
27
|
-
options: FieldResolverOptions = {}
|
|
28
|
-
): FieldResolverFn {
|
|
29
|
-
const { tableField, modify, transform } = options
|
|
30
|
-
|
|
31
|
-
return function resolve(query, context) {
|
|
32
|
-
const { field } = context
|
|
33
|
-
if (modify) {
|
|
34
|
-
query = modify(query, context)
|
|
35
|
-
} else {
|
|
36
|
-
query = query.select({ [field]: tableField || field })
|
|
37
|
-
}
|
|
38
|
-
if (transform) {
|
|
39
|
-
query = run_after_query(query, async (instance) => {
|
|
40
|
-
instance[field] = await transform(instance[field], instance, context)
|
|
41
|
-
return instance
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
return query
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/resolvers/page.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { overrideParserInQuery } from "orchid-core"
|
|
2
|
-
|
|
3
|
-
import { get_query_pagination_handler, Paginator } from "../paginators/base"
|
|
4
|
-
import { defineFieldResolver } from "./field"
|
|
5
|
-
import { RelationResolverOptions } from "./relation"
|
|
6
|
-
|
|
7
|
-
export function definePageResolver(
|
|
8
|
-
paginator: Paginator,
|
|
9
|
-
options: RelationResolverOptions = {}
|
|
10
|
-
) {
|
|
11
|
-
const { tableField, modify } = options
|
|
12
|
-
|
|
13
|
-
return defineFieldResolver({
|
|
14
|
-
modify(query, context) {
|
|
15
|
-
const { field, graph } = context
|
|
16
|
-
// FIXME: refactor, add types
|
|
17
|
-
let get_page: any
|
|
18
|
-
query = query.select({
|
|
19
|
-
[field]: (q) => {
|
|
20
|
-
q = graph._resolve_page(
|
|
21
|
-
(q as any)[tableField || field],
|
|
22
|
-
paginator,
|
|
23
|
-
context
|
|
24
|
-
)
|
|
25
|
-
// Calling addParserToQuery here doesn't work, save the paginator for later.
|
|
26
|
-
get_page = get_query_pagination_handler(q)
|
|
27
|
-
return modify ? modify(q, context) : q
|
|
28
|
-
},
|
|
29
|
-
})
|
|
30
|
-
overrideParserInQuery(query.query, field, get_page)
|
|
31
|
-
return query
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { FiltersDef } from "../filters/filters"
|
|
2
|
-
import { defineFieldResolver, FieldResolverOptions } from "./field"
|
|
3
|
-
import { TableResolverFn } from "./table"
|
|
4
|
-
|
|
5
|
-
export interface RelationResolverOptions
|
|
6
|
-
extends Omit<FieldResolverOptions, "modify"> {
|
|
7
|
-
filters?: FiltersDef
|
|
8
|
-
modify?: TableResolverFn
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function defineRelationResolver(options: RelationResolverOptions = {}) {
|
|
12
|
-
const { tableField, filters, modify } = options
|
|
13
|
-
|
|
14
|
-
return defineFieldResolver({
|
|
15
|
-
modify(query, context) {
|
|
16
|
-
const { graph, field } = context
|
|
17
|
-
return query.select({
|
|
18
|
-
[field]: (q) => {
|
|
19
|
-
const relation_query = graph._resolve_type(
|
|
20
|
-
(q as any)[tableField || field],
|
|
21
|
-
{ ...context, filters }
|
|
22
|
-
)
|
|
23
|
-
return modify ? modify(relation_query, context) : relation_query
|
|
24
|
-
},
|
|
25
|
-
})
|
|
26
|
-
},
|
|
27
|
-
})
|
|
28
|
-
}
|
package/src/utils/modifier.ts
DELETED
package/src/utils/run-after.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Query } from "pqb"
|
|
2
|
-
|
|
3
|
-
export function run_after_query(
|
|
4
|
-
query: Query,
|
|
5
|
-
transform: (instance: any) => any
|
|
6
|
-
) {
|
|
7
|
-
return query.afterQuery(async (_, result) => {
|
|
8
|
-
// FIXME: returning is ignored in orchid (unlike objection). Add some hacks instead.
|
|
9
|
-
if (Array.isArray(result)) {
|
|
10
|
-
const result1 = await Promise.all(result.map(transform))
|
|
11
|
-
return result1.filter(Boolean) // Skip empty objects
|
|
12
|
-
} else if (result) {
|
|
13
|
-
return transform(result)
|
|
14
|
-
} else {
|
|
15
|
-
// Supposedly, single query builder returning NULL
|
|
16
|
-
return result
|
|
17
|
-
}
|
|
18
|
-
})
|
|
19
|
-
}
|