orchid-graphql 0.0.4 → 1.1.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 CHANGED
@@ -2,10 +2,6 @@
2
2
 
3
3
  A helper library to resolve GraphQL queries directly with [Orchid ORM](https://orchid-orm.netlify.app) tables and relations.
4
4
 
5
- This is a fork of [objection-graphql-resolver](https://github.com/IlyaSemenov/objection-graphql-resolver) which does the same for [Objection.js ORM](https://vincit.github.io/objection.js/).
6
-
7
- ## Features
8
-
9
5
  - Highly effective: selects only requested fields and relations.
10
6
  - Unlimited nested resolvers.
11
7
  - Pagination.
@@ -13,11 +9,13 @@ This is a fork of [objection-graphql-resolver](https://github.com/IlyaSemenov/ob
13
9
  - Hook into (sub)queries with query modifiers.
14
10
  - Hook into field results to restrict access to sensitive information.
15
11
 
12
+ _Note that there is a sister project: `objection-graphql-resolver` which does the same for [Objection.js ORM](https://vincit.github.io/objection.js/)._
13
+
16
14
  ## Status and limitations
17
15
 
18
- This is pre-release and not battle tested. `orchid-graphql` is a quick fork of `objection-graphql-resolver`.
16
+ This is pre-release and not battle tested. `orchid-graphql` is a quick adaptation of `objection-graphql-resolver`.
19
17
 
20
- In particular (unlike its sister project) `orchid-graphql` currently doesn't handle types well and returns `any` most of the time.
18
+ In particular the typings are not ready. The resolver returns a generic `Query` most of the time.
21
19
 
22
20
  ## Install
23
21
 
@@ -299,8 +297,7 @@ const graph = r.graph(
299
297
  }),
300
298
  preview2: r.field({
301
299
  // Modify query
302
- modify: (query) =>
303
- query.select(raw("substr(text,1,100) as preview2")),
300
+ modify: (q) => q.select({ preview2: q.raw("substr(text,1,100)") }),
304
301
  // Post-process selected value
305
302
  transform(
306
303
  // Selected value
@@ -390,7 +387,7 @@ const resolvers = {
390
387
  }
391
388
  )
392
389
  },
393
- posts_page: (parent, args, context, info) => {
390
+ posts_page: async (parent, args, context, info) => {
394
391
  return await graph.resolvePage(
395
392
  // Root query (required)
396
393
  db.post,
package/dist/index.cjs CHANGED
@@ -35,29 +35,7 @@ __export(src_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(src_exports);
37
37
 
38
- // src/paginators/base.ts
39
- var s = Symbol();
40
- function set_query_page_result(query, get_page) {
41
- function _get_page(data) {
42
- if (!Array.isArray(data)) {
43
- throw new Error(`Paginator called for single result query.`);
44
- }
45
- return get_page(data);
46
- }
47
- const _query = query.query;
48
- const { handleResult } = _query;
49
- _query.handleResult = (q, result, s2) => {
50
- const data = handleResult(q, result, s2);
51
- return s2 ? data : _get_page(data);
52
- };
53
- _query[s] = _get_page;
54
- return query;
55
- }
56
- function get_query_pagination_handler(query) {
57
- return query.query[s];
58
- }
59
-
60
- // src/paginators/cursor.ts
38
+ // ../graphql-orm/src/paginators/cursor.ts
61
39
  function defineCursorPaginator(options = {}) {
62
40
  return new CursorPaginator(options);
63
41
  }
@@ -79,18 +57,22 @@ var CursorPaginator = class {
79
57
  }
80
58
  });
81
59
  }
82
- paginate(query, args = {}) {
60
+ paginate(query, context) {
61
+ const { orm: orm2 } = context.graph;
62
+ const { args } = context.tree;
83
63
  const take = args.take ?? this.options.take;
84
- const { cursor } = args;
85
- query = query.clear("order");
64
+ const cursor = args.cursor;
65
+ query = orm2.reset_query_order(query);
86
66
  for (const field of this.fields) {
87
- query = query.select(field.name).order({ [field.name]: field.desc ? "DESC" : "ASC" });
67
+ query = orm2.select_field(query, { field: field.name, as: field.name });
68
+ query = orm2.add_query_order(query, field.name, field.desc);
88
69
  }
89
70
  if (cursor) {
90
- query = this._set_query_cursor(query, cursor);
71
+ const { expression, bindings } = this._parse_cursor(cursor);
72
+ query = orm2.where_raw(query, expression, bindings);
91
73
  }
92
- query = query.limit(take + 1);
93
- query = set_query_page_result(query, (nodes) => {
74
+ query = orm2.set_query_limit(query, take + 1);
75
+ query = orm2.set_query_page_result(query, (nodes) => {
94
76
  let cursor2;
95
77
  if (nodes.length > take) {
96
78
  cursor2 = this._create_cursor(nodes[take - 1]);
@@ -113,29 +95,29 @@ var CursorPaginator = class {
113
95
  })
114
96
  );
115
97
  }
116
- _set_query_cursor(query, cursor) {
98
+ _parse_cursor(cursor) {
117
99
  const values = JSON.parse(cursor);
118
100
  const left = [];
119
101
  const right = [];
120
- const expr_values = {};
102
+ const bindings = {};
121
103
  for (let i = 0; i < this.fields.length; ++i) {
122
104
  const field = this.fields[i];
123
105
  const expressions = field.desc ? right : left;
124
106
  const placeholders = field.desc ? left : right;
125
107
  expressions.push(`"${field.name}"`);
126
- const placeholder = `value${i}`;
127
- placeholders.push("$" + placeholder);
128
- expr_values[placeholder] = values[i];
108
+ placeholders.push("$" + field.name);
109
+ bindings[field.name] = values[i];
129
110
  }
130
- return query.where(
131
- query.raw(`(${left.join(",")}) > (${right.join(",")})`, expr_values)
132
- );
111
+ return {
112
+ expression: `(${left.join(",")}) > (${right.join(",")})`,
113
+ bindings
114
+ };
133
115
  }
134
116
  };
135
117
 
136
- // src/utils/run-after.ts
137
- function run_after_query(query, transform) {
138
- return query.afterQuery(async (_, result) => {
118
+ // ../graphql-orm/src/utils/run-after.ts
119
+ function run_after_query(orm2, query, transform) {
120
+ return orm2.run_after_query(query, async (result) => {
139
121
  if (Array.isArray(result)) {
140
122
  const result1 = await Promise.all(result.map(transform));
141
123
  return result1.filter(Boolean);
@@ -147,7 +129,7 @@ function run_after_query(query, transform) {
147
129
  });
148
130
  }
149
131
 
150
- // src/resolvers/field.ts
132
+ // ../graphql-orm/src/resolvers/field.ts
151
133
  function defineFieldResolver(options = {}) {
152
134
  const { tableField, modify, transform } = options;
153
135
  return function resolve(query, context) {
@@ -155,10 +137,13 @@ function defineFieldResolver(options = {}) {
155
137
  if (modify) {
156
138
  query = modify(query, context);
157
139
  } else {
158
- query = query.select({ [field]: tableField || field });
140
+ query = context.graph.orm.select_field(query, {
141
+ field: tableField || field,
142
+ as: field
143
+ });
159
144
  }
160
145
  if (transform) {
161
- query = run_after_query(query, async (instance) => {
146
+ query = run_after_query(context.graph.orm, query, async (instance) => {
162
147
  instance[field] = await transform(instance[field], instance, context);
163
148
  return instance;
164
149
  });
@@ -167,14 +152,12 @@ function defineFieldResolver(options = {}) {
167
152
  };
168
153
  }
169
154
 
170
- // src/resolvers/graph.ts
155
+ // ../graphql-orm/src/resolvers/graph.ts
171
156
  var import_graphql_parse_resolve_info = require("graphql-parse-resolve-info");
172
- function createGraphResolver(types, options) {
173
- return new GraphResolver(types, options);
174
- }
175
157
  var GraphResolver = class {
176
- constructor(tables, options = {}) {
177
- this.tables = tables;
158
+ constructor(orm2, type_resolvers, options = {}) {
159
+ this.orm = orm2;
160
+ this.type_resolvers = type_resolvers;
178
161
  this.options = options;
179
162
  }
180
163
  resolve(query, { info, context, filters }) {
@@ -191,87 +174,81 @@ var GraphResolver = class {
191
174
  _resolve_type(query, context) {
192
175
  const { tree } = context;
193
176
  const type = Object.keys(tree.fieldsByTypeName)[0];
194
- const table_resolver = this.tables[type];
195
- if (!table_resolver) {
196
- throw new Error(`Table resolver not found for type ${type}.`);
177
+ const type_resolver = this.type_resolvers[type];
178
+ if (!type_resolver) {
179
+ throw new Error(`Resolver not found for type ${type}.`);
197
180
  }
198
- return table_resolver(query, { ...context, graph: this, tree, type });
181
+ return type_resolver.resolve(query, {
182
+ ...context,
183
+ graph: this,
184
+ tree,
185
+ type
186
+ });
199
187
  }
200
188
  _resolve_page(query, paginator, context) {
201
189
  let { tree } = context;
202
- const { args } = tree;
203
190
  for (const field of paginator.path) {
204
191
  const type = Object.keys(tree.fieldsByTypeName)[0];
205
192
  tree = tree.fieldsByTypeName[type][field];
206
193
  }
207
194
  query = this._resolve_type(query, { ...context, tree });
208
- return paginator.paginate(query, args);
195
+ return paginator.paginate(query, {
196
+ ...context,
197
+ graph: this
198
+ });
209
199
  }
210
200
  };
211
201
 
212
- // src/resolvers/page.ts
213
- var import_orchid_core = require("orchid-core");
202
+ // ../graphql-orm/src/resolvers/page.ts
214
203
  function definePageResolver(paginator, options = {}) {
215
204
  const { tableField, modify } = options;
216
- return defineFieldResolver({
217
- modify(query, context) {
218
- const { field, graph } = context;
219
- let get_page;
220
- query = query.select({
221
- [field]: (q) => {
222
- q = graph._resolve_page(
223
- q[tableField || field],
224
- paginator,
225
- context
226
- );
227
- get_page = get_query_pagination_handler(q);
228
- return modify ? modify(q, context) : q;
229
- }
230
- });
231
- (0, import_orchid_core.overrideParserInQuery)(query.query, field, get_page);
232
- return query;
233
- }
234
- });
205
+ return function resolve(query, context) {
206
+ const { graph, field } = context;
207
+ const pagination_context = {};
208
+ query = graph.orm.select_relation(query, {
209
+ relation: tableField || field,
210
+ as: field,
211
+ modify(subquery) {
212
+ subquery = graph._resolve_page(subquery, paginator, context);
213
+ subquery = graph.orm.modify_subquery_pagination(
214
+ subquery,
215
+ pagination_context
216
+ );
217
+ return modify ? modify(subquery, context) : subquery;
218
+ }
219
+ });
220
+ query = graph.orm.finish_query_pagination(query, field, pagination_context);
221
+ return query;
222
+ };
235
223
  }
236
224
 
237
- // src/resolvers/relation.ts
225
+ // ../graphql-orm/src/resolvers/relation.ts
238
226
  function defineRelationResolver(options = {}) {
239
227
  const { tableField, filters, modify } = options;
240
- return defineFieldResolver({
241
- modify(query, context) {
242
- const { graph, field } = context;
243
- return query.select({
244
- [field]: (q) => {
245
- const relation_query = graph._resolve_type(
246
- q[tableField || field],
247
- { ...context, filters }
248
- );
249
- return modify ? modify(relation_query, context) : relation_query;
250
- }
251
- });
252
- }
253
- });
228
+ return function resolve(query, context) {
229
+ const { graph, field } = context;
230
+ return graph.orm.select_relation(query, {
231
+ relation: tableField || field,
232
+ as: field,
233
+ modify(subquery) {
234
+ subquery = graph._resolve_type(subquery, { ...context, filters });
235
+ return modify ? modify(subquery, context) : subquery;
236
+ }
237
+ });
238
+ };
254
239
  }
255
240
 
256
- // src/utils/is-plain-object.ts
241
+ // ../graphql-orm/src/utils/is-plain-object.ts
257
242
  function is_plain_object(obj) {
258
243
  return typeof obj === "object" && !Array.isArray(obj);
259
244
  }
260
245
 
261
- // src/filters/filters.ts
262
- function is_scalar(value) {
263
- return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
264
- }
265
- function apply_filters({
266
- query,
267
- filters,
268
- args,
269
- modifiers
270
- }) {
246
+ // ../graphql-orm/src/filters/filters.ts
247
+ function apply_filters(query, { filters, modifiers, context }) {
271
248
  if (!filters) {
272
249
  return query;
273
250
  }
274
- const filter_obj = args?.filter;
251
+ const filter_obj = context.tree.args?.filter;
275
252
  if (!filter_obj) {
276
253
  return query;
277
254
  }
@@ -283,90 +260,60 @@ function apply_filters({
283
260
  continue;
284
261
  }
285
262
  if (modifiers?.[field]) {
286
- query = modifiers[field](query, value);
263
+ query = modifiers[field](query, value, context);
287
264
  } else {
288
- const [dbfield, op0] = field.split("__");
289
- const op = op0?.toLowerCase() || "exact";
290
- if (
291
- // TODO: add the rest of orchid ops (or allow all of them?)
292
- [
293
- "exact",
294
- "lt",
295
- "lte",
296
- "gt",
297
- "gte",
298
- "like",
299
- "ilike",
300
- "contains"
301
- ].includes(op)
302
- ) {
303
- if (!is_scalar(value)) {
304
- throw new Error(
305
- `Unsupported filter value for ${query.table}.${field}: must be scalar.`
306
- );
307
- }
308
- if (op === "exact") {
309
- query = query.where({ [dbfield]: value });
310
- } else {
311
- const query_op = { like: "contains" }[op] || op;
312
- query = query.where({ [dbfield]: { [query_op]: value } });
313
- }
314
- } else if (op === "in") {
315
- if (!(Array.isArray(value) && value.every(is_scalar))) {
316
- throw new Error(
317
- `Invalid filter value for ${query.table}.${field}: must be an array of scalars.`
318
- );
319
- }
320
- query = query.whereIn(dbfield, value);
321
- } else {
322
- throw new Error(
323
- `Invalid filter ${query.table}.${field}: unsupported operator ${op}.`
324
- );
325
- }
265
+ const [table_field, op0] = field.split("__");
266
+ const op = op0?.toLowerCase();
267
+ query = context.graph.orm.where(query, table_field, op, value);
326
268
  }
327
269
  }
328
270
  return query;
329
271
  }
330
272
 
331
- // src/resolvers/table.ts
332
- function defineTableResolver(table, options = {}) {
333
- const get_default_field_resolver = (field, tableField) => {
334
- const table_field_lookup = tableField || field;
335
- if (options.modifiers?.[table_field_lookup]) {
336
- return (query) => query;
337
- } else if (table.relations?.[table_field_lookup]) {
338
- return defineRelationResolver({ tableField });
339
- } else {
340
- return defineFieldResolver({ tableField });
273
+ // ../graphql-orm/src/resolvers/table.ts
274
+ var TableResolver = class {
275
+ constructor(orm2, table, options = {}) {
276
+ this.orm = orm2;
277
+ this.table = table;
278
+ this.options = options;
279
+ this.relations = new Set(orm2.get_table_relations(table));
280
+ this.virtual_fields = new Set(orm2.get_table_virtual_fields(table));
281
+ this.modifiers = { ...orm2.get_table_modifiers(table), ...options.modifiers };
282
+ const { fields } = options;
283
+ if (fields) {
284
+ this.table_field_resolvers = Object.keys(fields).reduce((resolvers, field) => {
285
+ const r0 = fields[field];
286
+ const r = typeof r0 === "function" ? r0 : r0 === true ? this._get_default_field_resolver(field) : typeof r0 === "string" ? this._get_default_field_resolver(field, r0) : void 0;
287
+ if (r === void 0) {
288
+ throw new Error(
289
+ `Field resolver must be a function, string, or true; found ${typeof r0}.`
290
+ );
291
+ }
292
+ resolvers[field] = r;
293
+ return resolvers;
294
+ }, {});
341
295
  }
342
- };
343
- const { fields } = options;
344
- const table_field_resolvers = fields ? Object.keys(fields).reduce(
345
- (resolvers, field) => {
346
- const r0 = fields[field];
347
- const r = typeof r0 === "function" ? r0 : r0 === true ? get_default_field_resolver(field) : typeof r0 === "string" ? get_default_field_resolver(field, r0) : void 0;
348
- if (r === void 0) {
349
- throw new Error(
350
- `Field resolver must be a function, string, or true; found ${r0}`
351
- );
352
- }
353
- resolvers[field] = r;
354
- return resolvers;
355
- },
356
- {}
357
- ) : void 0;
358
- const { modify, transform } = options;
359
- return function resolve(query, context) {
296
+ }
297
+ // Introspection results
298
+ relations;
299
+ virtual_fields;
300
+ modifiers;
301
+ table_field_resolvers;
302
+ /** Modify query to select fields/relations and filter the result set. */
303
+ resolve(query, context) {
304
+ const { modify, transform } = this.options;
360
305
  const { graph, tree, type, filters } = context;
361
- if (query.table !== table.table) {
306
+ const query_table = this.orm.get_query_table(query);
307
+ const table_table = this.orm.get_table_table(this.table);
308
+ if (query_table !== table_table) {
362
309
  throw new Error(
363
- `Mismatching query type (expected ${table.table}, found ${query.table}).`
310
+ `Mismatching query table for type ${type}: expected ${table_table}, received ${query_table}.`
364
311
  );
365
312
  }
366
- const allow_all_fields = options.allowAllFields ?? graph.options.allowAllFields ?? table_field_resolvers === void 0;
367
- if (!allow_all_fields && !table_field_resolvers) {
313
+ const allow_all_fields = this.options.allowAllFields ?? graph.options.allowAllFields ?? !this.options.fields;
314
+ if (!allow_all_fields && !this.table_field_resolvers) {
368
315
  throw new Error(
369
- `Table resolver for ${type} must either allow all fields or specify options.fields`
316
+ `Resolver for type ${type} must either allow all fields or specify options.fields.`
370
317
  );
371
318
  }
372
319
  if (modify) {
@@ -374,31 +321,137 @@ function defineTableResolver(table, options = {}) {
374
321
  }
375
322
  for (const subtree of Object.values(tree.fieldsByTypeName[type])) {
376
323
  const field = subtree.name;
377
- const r = table_field_resolvers?.[field] || (allow_all_fields ? get_default_field_resolver(field) : void 0);
324
+ const r = this.table_field_resolvers?.[field] || (allow_all_fields ? this._get_default_field_resolver(field) : void 0);
378
325
  if (!r) {
379
- throw new Error(`No field resolver defined for field ${type}.${field}`);
326
+ throw new Error(`No field resolver defined for field ${type}.${field}.`);
380
327
  }
381
328
  if (r) {
382
329
  query = r(query, { ...context, tree: subtree, field });
383
330
  }
384
331
  }
385
- const allow_all_filters = options.allowAllFilters ?? graph.options.allowAllFilters;
332
+ const allow_all_filters = this.options.allowAllFilters ?? graph.options.allowAllFilters;
386
333
  const effective_filters = allow_all_filters ? true : filters;
387
334
  if (effective_filters) {
388
- query = apply_filters({
389
- query,
335
+ query = apply_filters(query, {
390
336
  filters: effective_filters,
391
- args: tree.args,
392
- modifiers: options.modifiers
337
+ modifiers: this.modifiers,
338
+ context
393
339
  });
394
340
  }
395
341
  if (transform) {
396
- query = run_after_query(query, (instance) => {
342
+ query = run_after_query(this.orm, query, (instance) => {
397
343
  return transform(instance, context);
398
344
  });
399
345
  }
346
+ query = this.orm.prevent_select_all(query);
400
347
  return query;
401
- };
348
+ }
349
+ /**
350
+ * Create field resolver which will modify query to resolve GraphQL field.
351
+ */
352
+ _get_default_field_resolver(field, tableField) {
353
+ const table_field_lookup = tableField || field;
354
+ if (this.options.modifiers?.[table_field_lookup] || this.virtual_fields.has(table_field_lookup)) {
355
+ return (query) => query;
356
+ } else if (this.relations.has(table_field_lookup)) {
357
+ return defineRelationResolver({ tableField });
358
+ } else {
359
+ return defineFieldResolver({ tableField });
360
+ }
361
+ }
362
+ };
363
+
364
+ // src/orm/orm.ts
365
+ var import_orchid_core = require("orchid-core");
366
+ var orm = {
367
+ Table: void 0,
368
+ Query: void 0,
369
+ // Reflection
370
+ get_table_table(table) {
371
+ return table.table;
372
+ },
373
+ get_table_relations(table) {
374
+ return Object.keys(table.relations);
375
+ },
376
+ get_table_virtual_fields() {
377
+ return [];
378
+ },
379
+ get_table_modifiers() {
380
+ return void 0;
381
+ },
382
+ get_query_table(query) {
383
+ if (!query.table) {
384
+ throw new Error("Query must have table.");
385
+ }
386
+ return query.table;
387
+ },
388
+ // Select
389
+ select_field(query, { field, as }) {
390
+ return query.select({ [as]: field });
391
+ },
392
+ select_relation(query, { relation, as, modify }) {
393
+ return query.select({
394
+ [as]: (q) => modify(q[relation])
395
+ });
396
+ },
397
+ // Find
398
+ where(query, field, op, value) {
399
+ return query.where({ [field]: op ? { [op]: value } : value });
400
+ },
401
+ where_raw(query, expression, bindings) {
402
+ return query.where(query.raw(expression, bindings));
403
+ },
404
+ // Order & Limit
405
+ reset_query_order(query) {
406
+ return query.clear("order");
407
+ },
408
+ add_query_order(query, field, desc) {
409
+ return query.order({ [field]: desc ? "DESC" : "ASC" });
410
+ },
411
+ set_query_limit(query, limit) {
412
+ return query.limit(limit);
413
+ },
414
+ // Pagination helpers
415
+ set_query_page_result(query, get_page) {
416
+ const _query = query.query;
417
+ const { handleResult } = _query;
418
+ _query.handleResult = (q, result, s2) => {
419
+ const data = handleResult(q, result, s2);
420
+ return s2 ? data : get_page(data);
421
+ };
422
+ _query[s] = get_page;
423
+ return query;
424
+ },
425
+ modify_subquery_pagination(subquery, context) {
426
+ context.get_page = subquery.query[s];
427
+ return subquery;
428
+ },
429
+ finish_query_pagination(query, field, context) {
430
+ const { get_page } = context;
431
+ if (!get_page) {
432
+ throw new Error(`Internal error: paginator didn't provide page callback.`);
433
+ }
434
+ (0, import_orchid_core.overrideParserInQuery)(query.query, field, get_page);
435
+ return query;
436
+ },
437
+ // Misc
438
+ run_after_query(query, fn) {
439
+ return query.afterQuery(async (_, result) => fn(result));
440
+ },
441
+ prevent_select_all(query) {
442
+ return query;
443
+ }
444
+ };
445
+ var s = Symbol("subquery_pagination_handler");
446
+
447
+ // src/resolvers/graph.ts
448
+ function createGraphResolver(types, options) {
449
+ return new GraphResolver(orm, types, options);
450
+ }
451
+
452
+ // src/resolvers/table.ts
453
+ function defineTableResolver(table, options = {}) {
454
+ return new TableResolver(orm, table, options);
402
455
  }
403
456
  // Annotate the CommonJS export names for ESM import in node:
404
457
  0 && (module.exports = {