orchid-graphql 0.0.1

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.
@@ -0,0 +1,114 @@
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
+ console.log("paginating query", query)
54
+
55
+ // Set query order
56
+ query = query.clear("order")
57
+ for (const field of this.fields) {
58
+ // TODO: prevent potential name clash with aliases like .as(`_${table_ref}_order_key_0`)
59
+ query = query
60
+ .select(field.name)
61
+ .order({ [field.name]: field.desc ? "DESC" : "ASC" })
62
+ }
63
+
64
+ if (cursor) {
65
+ query = this._set_query_cursor(query, cursor)
66
+ }
67
+ query = query.limit(take + 1)
68
+
69
+ query = set_query_page_result(query, (nodes) => {
70
+ let cursor: string | undefined
71
+ if (nodes.length > take) {
72
+ cursor = this._create_cursor(nodes[take - 1])
73
+ nodes = nodes.slice(0, take)
74
+ }
75
+ return { nodes, cursor }
76
+ })
77
+
78
+ return query
79
+ }
80
+
81
+ _create_cursor(instance: any) {
82
+ return JSON.stringify(
83
+ this.fields.map((field) => {
84
+ const value = instance[field.name]
85
+ if (value === undefined) {
86
+ throw new Error(
87
+ `Unable to create cursor: undefined field ${field.name}`
88
+ )
89
+ }
90
+ return String(value)
91
+ })
92
+ )
93
+ }
94
+
95
+ _set_query_cursor(query: Query, cursor: string) {
96
+ const values = JSON.parse(cursor)
97
+ const left: string[] = []
98
+ const right: string[] = []
99
+ const expr_values: Record<string, any> = {}
100
+ // TODO: refactor
101
+ for (let i = 0; i < this.fields.length; ++i) {
102
+ const field = this.fields[i]
103
+ const expressions = field.desc ? right : left
104
+ const placeholders = field.desc ? left : right
105
+ expressions.push(`"${field.name}"`)
106
+ const placeholder = `value${i}`
107
+ placeholders.push("$" + placeholder)
108
+ expr_values[placeholder] = values[i]
109
+ }
110
+ return query.where(
111
+ query.raw(`(${left.join(",")}) > (${right.join(",")})`, expr_values)
112
+ )
113
+ }
114
+ }
@@ -0,0 +1,51 @@
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
+ }
@@ -0,0 +1,92 @@
1
+ import { GraphQLResolveInfo } from "graphql"
2
+ import { parseResolveInfo, ResolveTree } from "graphql-parse-resolve-info"
3
+ import { Query } from "pqb"
4
+
5
+ import { FiltersDef } from "../filters/filters"
6
+ import { Paginator } from "../paginators/base"
7
+ import { set_query_context } from "../utils/query-context"
8
+ import type { TableResolverFn, TableResolverOptions } from "./table"
9
+
10
+ export type GraphResolverOptions = Pick<
11
+ TableResolverOptions,
12
+ "allowAllFields" | "allowAllFilters"
13
+ >
14
+
15
+ export function createGraphResolver(
16
+ types: Record<string, TableResolverFn>,
17
+ options?: GraphResolverOptions
18
+ ) {
19
+ return new GraphResolver(types, options)
20
+ }
21
+
22
+ export interface GraphResolveOptions {
23
+ info: GraphQLResolveInfo
24
+ context: any
25
+ filters?: FiltersDef
26
+ }
27
+
28
+ export class GraphResolver {
29
+ constructor(
30
+ public readonly tables: Record<string, TableResolverFn>,
31
+ public readonly options: GraphResolverOptions = {}
32
+ ) {}
33
+
34
+ resolve(query: Query, { info, context, filters }: GraphResolveOptions) {
35
+ set_query_context(query, context)
36
+ const tree = this._get_resolve_tree(info)
37
+ return this._resolve_type({ query, tree, filters })
38
+ }
39
+
40
+ resolvePage(
41
+ query: Query,
42
+ paginator: Paginator,
43
+ { info, context, filters }: GraphResolveOptions
44
+ ) {
45
+ set_query_context(query, context)
46
+ const tree = this._get_resolve_tree(info)
47
+ return this._resolve_page({ query, tree, paginator, filters })
48
+ }
49
+
50
+ _get_resolve_tree(info: GraphQLResolveInfo) {
51
+ return parseResolveInfo(info) as ResolveTree
52
+ }
53
+
54
+ _resolve_type({
55
+ tree,
56
+ query,
57
+ filters,
58
+ }: {
59
+ query: Query
60
+ tree: ResolveTree
61
+ filters?: FiltersDef
62
+ }) {
63
+ const type = Object.keys(tree.fieldsByTypeName)[0]
64
+ const table_resolver = this.tables[type]
65
+ if (!table_resolver) {
66
+ throw new Error(`Table resolver not found for type ${type}.`)
67
+ }
68
+ return table_resolver({ tree, type, query, filters, graph: this })
69
+ }
70
+
71
+ _resolve_page({
72
+ tree,
73
+ query,
74
+ paginator,
75
+ filters,
76
+ }: {
77
+ query: Query
78
+ tree: ResolveTree
79
+ paginator: Paginator
80
+ filters?: FiltersDef
81
+ }) {
82
+ const { args } = tree
83
+ // Skip page subtree(s)
84
+ for (const field of paginator.path) {
85
+ const type = Object.keys(tree.fieldsByTypeName)[0]
86
+ tree = tree.fieldsByTypeName[type][field]
87
+ // TODO: raise exception if not found
88
+ }
89
+ query = this._resolve_type({ query, tree, filters })
90
+ return paginator.paginate(query, args)
91
+ }
92
+ }
@@ -0,0 +1,29 @@
1
+ import { addParserToQuery } from "pqb"
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
+ query.select({
16
+ [field]: (q) => {
17
+ const page_query = graph._resolve_page({
18
+ tree,
19
+ query: (q as any)[tableField || field],
20
+ paginator,
21
+ filters,
22
+ })
23
+ const get_page = get_query_pagination_handler(page_query)
24
+ addParserToQuery(page_query.query, field, get_page as any)
25
+ return modify ? modify(page_query, tree) : page_query
26
+ },
27
+ }),
28
+ })
29
+ }
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1,146 @@
1
+ import { ResolveTree } from "graphql-parse-resolve-info"
2
+ import { DbTable, TableClass } from "orchid-orm"
3
+ import { Query } from "pqb"
4
+
5
+ import { apply_filters, FiltersDef } from "../filters/filters"
6
+ import { Modifier } from "../utils/modifier"
7
+ import { get_query_context } from "../utils/query-context"
8
+ import { run_after_query } from "../utils/run-after"
9
+ import { defineFieldResolver, FieldResolverFn } from "./field"
10
+ import type { GraphResolver } from "./graph"
11
+ import { defineRelationResolver } from "./relation"
12
+
13
+ /** A function that modifies the query to select fields/relations and filter the result set. */
14
+ export type TableResolverFn = (args: {
15
+ tree: ResolveTree
16
+ /** GraphQL type */
17
+ type: string
18
+ query: Query
19
+ filters?: FiltersDef
20
+ graph: GraphResolver
21
+ }) => Query
22
+
23
+ export interface TableResolverOptions {
24
+ /** allow to resolve all table fields without explicitly listing them */
25
+ allowAllFields?: boolean
26
+ /** allow to filter all table fields without explicitly listing them */
27
+ allowAllFilters?: boolean
28
+ fields?: Record<string, SimpleFieldResolver>
29
+ modifiers?: Record<string, Modifier>
30
+ modify?: QueryTreeModifier
31
+ transform?(instance: any, context: any): void | PromiseLike<void>
32
+ }
33
+
34
+ export type SimpleFieldResolver = true | string | FieldResolverFn
35
+
36
+ export type QueryTreeModifier = (query: Query, tree: ResolveTree) => Query
37
+
38
+ export function defineTableResolver<T extends TableClass>(
39
+ table: DbTable<T>,
40
+ options: TableResolverOptions = {}
41
+ ): TableResolverFn {
42
+ /**
43
+ * Create a field resolver which will modify the query to resolve a GraphQL field.
44
+ */
45
+ const get_default_field_resolver = (
46
+ field: string,
47
+ tableField?: string
48
+ ): FieldResolverFn | undefined => {
49
+ const table_field_lookup = tableField || field
50
+ if (options.modifiers?.[table_field_lookup]) {
51
+ // Keep query as is.
52
+ return (query) => query
53
+ } else if ((table.relations as any)?.[table_field_lookup]) {
54
+ return defineRelationResolver({ tableField })
55
+ } else {
56
+ return defineFieldResolver({ tableField })
57
+ }
58
+ }
59
+
60
+ // Pre-create field resolvers
61
+ const { fields } = options
62
+ const table_field_resolvers = fields
63
+ ? Object.keys(fields).reduce<Record<string, FieldResolverFn>>(
64
+ (resolvers, field) => {
65
+ const r0 = fields[field]
66
+ const r: FieldResolverFn | undefined =
67
+ typeof r0 === "function"
68
+ ? r0
69
+ : r0 === true
70
+ ? get_default_field_resolver(field)
71
+ : typeof r0 === "string"
72
+ ? get_default_field_resolver(field, r0)
73
+ : undefined
74
+ if (r === undefined) {
75
+ throw new Error(
76
+ `Field resolver must be a function, string, or true; found ${r0}`
77
+ )
78
+ }
79
+ resolvers[field] = r
80
+ return resolvers
81
+ },
82
+ {}
83
+ )
84
+ : undefined
85
+
86
+ const { modify, transform } = options
87
+
88
+ return function resolve({ tree, type, query, filters, graph }) {
89
+ if (query.table !== table.table) {
90
+ throw new Error(
91
+ `Mismatching query type (expected ${table.table}, found ${query.table}).`
92
+ )
93
+ }
94
+
95
+ const allow_all_fields =
96
+ options.allowAllFields ??
97
+ graph.options.allowAllFields ??
98
+ table_field_resolvers === undefined
99
+
100
+ if (!allow_all_fields && !table_field_resolvers) {
101
+ throw new Error(
102
+ `Table resolver for ${type} must either allow all fields or specify options.fields`
103
+ )
104
+ }
105
+
106
+ if (modify) {
107
+ query = modify(query, tree)
108
+ }
109
+
110
+ for (const subtree of Object.values(tree.fieldsByTypeName[type])) {
111
+ const field = subtree.name
112
+ const r =
113
+ table_field_resolvers?.[field] ||
114
+ (allow_all_fields ? get_default_field_resolver(field) : undefined)
115
+ if (!r) {
116
+ throw new Error(`No field resolver defined for field ${type}.${field}`)
117
+ }
118
+ if (r) {
119
+ query = r(query, { field, tree: subtree, graph })
120
+ }
121
+ }
122
+
123
+ const allow_all_filters =
124
+ options.allowAllFilters ?? graph.options.allowAllFilters
125
+
126
+ const effective_filters = allow_all_filters ? true : filters
127
+
128
+ if (effective_filters) {
129
+ query = apply_filters({
130
+ query,
131
+ filters: effective_filters,
132
+ args: tree.args,
133
+ modifiers: options.modifiers,
134
+ })
135
+ }
136
+
137
+ if (transform) {
138
+ const context = get_query_context(query)
139
+ query = run_after_query(query, (instance) => {
140
+ return transform(instance, context)
141
+ })
142
+ }
143
+
144
+ return query
145
+ }
146
+ }
@@ -0,0 +1,3 @@
1
+ export function is_plain_object(obj: any): obj is Record<string, any> {
2
+ return typeof obj === "object" && !Array.isArray(obj)
3
+ }
@@ -0,0 +1,3 @@
1
+ import { Query } from "pqb"
2
+
3
+ export type Modifier = (query: Query, param?: any) => Query
@@ -0,0 +1,11 @@
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
+ }
@@ -0,0 +1,19 @@
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
+ }