orchid-graphql 0.0.3 → 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,51 +0,0 @@
1
- import { ResolveTree } from "graphql-parse-resolve-info"
2
- import { Query } from "pqb"
3
-
4
- import { get_query_context } from "../utils/query-context"
5
- import { run_after_query } from "../utils/run-after"
6
- import type { GraphResolver } from "./graph"
7
-
8
- /* A function that modifies the query to select a field. */
9
- export type FieldResolverFn = (
10
- query: Query,
11
- options: {
12
- /** GraphQL field */
13
- field: string
14
- /** GraphQL resolve tree */
15
- tree: ResolveTree
16
- /** Current graph resolver */
17
- graph: GraphResolver
18
- }
19
- ) => Query
20
-
21
- export interface FieldResolverOptions {
22
- /** Table field (if different from GraphQL field) */
23
- tableField?: string
24
- /** Custom query modifier (if different than simply selecting a field). */
25
- modify?: FieldResolverFn
26
- /** Post-process selected value. Return a new value or a promise. */
27
- transform?(value: any, instance: any, context: any): any
28
- }
29
-
30
- export function defineFieldResolver(
31
- options: FieldResolverOptions = {}
32
- ): FieldResolverFn {
33
- const { tableField, modify, transform } = options
34
-
35
- return function resolve(query, options) {
36
- const { field } = options
37
- if (modify) {
38
- return modify(query, options)
39
- } else {
40
- query = query.select({ [field]: tableField || field })
41
- }
42
- if (transform) {
43
- const context = get_query_context(query)
44
- query = run_after_query(query, async (instance) => {
45
- instance[field] = await transform(instance[field], instance, context)
46
- return instance
47
- })
48
- }
49
- return query
50
- }
51
- }
@@ -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, filters, modify } = options
12
-
13
- return defineFieldResolver({
14
- modify: (query, { field, tree, graph }) => {
15
- // FIXME: refactor, add types
16
- let get_page: any
17
- query = query.select({
18
- [field]: (q) => {
19
- q = graph._resolve_page({
20
- tree,
21
- query: (q as any)[tableField || field],
22
- paginator,
23
- filters,
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, tree) : q
28
- },
29
- })
30
- overrideParserInQuery(query.query, field, get_page)
31
- return query
32
- },
33
- })
34
- }
@@ -1,27 +0,0 @@
1
- import { FiltersDef } from "../filters/filters"
2
- import { defineFieldResolver, FieldResolverOptions } from "./field"
3
- import { QueryTreeModifier } from "./table"
4
-
5
- export interface RelationResolverOptions
6
- extends Omit<FieldResolverOptions, "modify"> {
7
- filters?: FiltersDef
8
- modify?: QueryTreeModifier
9
- }
10
-
11
- export function defineRelationResolver(options: RelationResolverOptions = {}) {
12
- const { tableField, filters, modify } = options
13
-
14
- return defineFieldResolver({
15
- modify: (query, { field, tree, graph }) =>
16
- query.select({
17
- [field]: (q) => {
18
- const relation_query = graph._resolve_type({
19
- tree,
20
- query: (q as any)[tableField || field],
21
- filters,
22
- })
23
- return modify ? modify(relation_query, tree) : relation_query
24
- },
25
- }),
26
- })
27
- }
@@ -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,11 +0,0 @@
1
- import { Query } from "pqb"
2
-
3
- const graphql = Symbol("graphql")
4
-
5
- export function set_query_context(query: Query, context: any) {
6
- ;(query.query as any)[graphql] = context
7
- }
8
-
9
- export function get_query_context(query: Query) {
10
- return (query.query as any)[graphql]
11
- }
@@ -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
- }