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.
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/dist/index.cjs +432 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +394 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/filters/filters.ts +93 -0
- package/src/index.ts +24 -0
- package/src/paginators/base.ts +37 -0
- package/src/paginators/cursor.ts +114 -0
- package/src/resolvers/field.ts +51 -0
- package/src/resolvers/graph.ts +92 -0
- package/src/resolvers/page.ts +29 -0
- package/src/resolvers/relation.ts +27 -0
- package/src/resolvers/table.ts +146 -0
- package/src/utils/is-plain-object.ts +3 -0
- package/src/utils/modifier.ts +3 -0
- package/src/utils/query-context.ts +11 -0
- package/src/utils/run-after.ts +19 -0
|
@@ -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,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
|
+
}
|