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.
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -1,3 +0,0 @@
1
- export function is_plain_object(obj: any): obj is Record<string, any> {
2
- return typeof obj === "object" && !Array.isArray(obj)
3
- }
@@ -1,3 +0,0 @@
1
- import { Query } from "pqb"
2
-
3
- export type Modifier = (query: Query, param?: any) => Query
@@ -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
- }