@coderich/autograph 0.11.1 → 0.13.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.
Files changed (56) hide show
  1. package/index.js +4 -6
  2. package/package.json +30 -44
  3. package/src/data/DataLoader.js +77 -70
  4. package/src/data/Emitter.js +89 -0
  5. package/src/data/Loader.js +33 -0
  6. package/src/data/Pipeline.js +88 -96
  7. package/src/data/Resolver.js +304 -0
  8. package/src/data/Transaction.js +49 -0
  9. package/src/query/Query.js +159 -334
  10. package/src/query/QueryBuilder.js +228 -114
  11. package/src/query/QueryResolver.js +110 -216
  12. package/src/query/QueryResolverTransaction.js +16 -0
  13. package/src/schema/Schema.js +593 -0
  14. package/src/service/AppService.js +38 -0
  15. package/src/service/ErrorService.js +7 -0
  16. package/CHANGELOG.md +0 -41
  17. package/LICENSE +0 -21
  18. package/README.md +0 -76
  19. package/src/.DS_Store +0 -0
  20. package/src/core/.DS_Store +0 -0
  21. package/src/core/Boom.js +0 -9
  22. package/src/core/EventEmitter.js +0 -95
  23. package/src/core/Resolver.js +0 -124
  24. package/src/core/Schema.js +0 -55
  25. package/src/core/ServerResolver.js +0 -15
  26. package/src/data/.DS_Store +0 -0
  27. package/src/data/DataService.js +0 -120
  28. package/src/data/DataTransaction.js +0 -96
  29. package/src/data/Field.js +0 -83
  30. package/src/data/Model.js +0 -223
  31. package/src/data/TreeMap.js +0 -78
  32. package/src/data/Type.js +0 -50
  33. package/src/driver/.DS_Store +0 -0
  34. package/src/driver/MongoDriver.js +0 -227
  35. package/src/driver/index.js +0 -11
  36. package/src/graphql/.DS_Store +0 -0
  37. package/src/graphql/ast/.DS_Store +0 -0
  38. package/src/graphql/ast/Field.js +0 -206
  39. package/src/graphql/ast/Model.js +0 -145
  40. package/src/graphql/ast/Node.js +0 -291
  41. package/src/graphql/ast/Schema.js +0 -133
  42. package/src/graphql/ast/Type.js +0 -26
  43. package/src/graphql/ast/TypeDefApi.js +0 -93
  44. package/src/graphql/extension/.DS_Store +0 -0
  45. package/src/graphql/extension/api.js +0 -193
  46. package/src/graphql/extension/framework.js +0 -71
  47. package/src/graphql/extension/type.js +0 -34
  48. package/src/query/.DS_Store +0 -0
  49. package/src/query/QueryBuilderTransaction.js +0 -26
  50. package/src/query/QueryService.js +0 -111
  51. package/src/service/.DS_Store +0 -0
  52. package/src/service/app.service.js +0 -319
  53. package/src/service/decorator.service.js +0 -114
  54. package/src/service/event.service.js +0 -66
  55. package/src/service/graphql.service.js +0 -92
  56. package/src/service/schema.service.js +0 -95
@@ -0,0 +1,304 @@
1
+ const { graphql } = require('graphql');
2
+ const Boom = require('@hapi/boom');
3
+ const Util = require('@coderich/util');
4
+ const Emitter = require('./Emitter');
5
+ const Loader = require('./Loader');
6
+ const DataLoader = require('./DataLoader');
7
+ const Transaction = require('./Transaction');
8
+ const QueryResolver = require('../query/QueryResolver');
9
+
10
+ const loaders = {};
11
+
12
+ module.exports = class Resolver {
13
+ #schema;
14
+ #xschema;
15
+ #context;
16
+ #dataLoaders;
17
+ #sessions = []; // Holds nested 2D array of transactions
18
+
19
+ constructor({ schema, xschema, context }) {
20
+ this.#schema = schema.parse?.() || schema;
21
+ this.#xschema = xschema;
22
+ this.#context = context;
23
+ this.#dataLoaders = this.#createDataLoaders();
24
+ this.driver = this.raw; // Alias
25
+ this.model = this.match; // Alias
26
+ Util.set(this.#context, 'autograph.resolver', this);
27
+ }
28
+
29
+ getContext() {
30
+ return this.#context;
31
+ }
32
+
33
+ clear(model) {
34
+ this.#dataLoaders[model].clearAll();
35
+ return this;
36
+ }
37
+
38
+ clearAll() {
39
+ Object.values(this.#dataLoaders).forEach(loader => loader.clearAll());
40
+ return this;
41
+ }
42
+
43
+ clone() {
44
+ return new Resolver({
45
+ schema: this.#schema,
46
+ context: this.#context,
47
+ });
48
+ }
49
+
50
+ raw(model) {
51
+ return this.toModel(model)?.source?.client?.driver(model);
52
+ }
53
+
54
+ graphql(args) {
55
+ args.schema ??= this.#xschema;
56
+ args.contextValue ??= this.#context;
57
+ return graphql(args);
58
+ // const { schema } = this;
59
+ // const variableValues = JSON.parse(JSON.stringify(variables));
60
+ // return graphql({ schema, source, variableValues, contextValue });
61
+ }
62
+
63
+ /**
64
+ * Create and execute a query for a provided model.
65
+ *
66
+ * @param {string|object} model - The name (string) or model (object) you wish to query
67
+ * @returns {QueryResolver|QueryResolverTransaction} - An API to build and execute a query
68
+ */
69
+ match(model) {
70
+ return this.#sessions.slice(-1).pop()?.slice(-1).pop()?.match(model) ?? new QueryResolver({
71
+ resolver: this,
72
+ schema: this.#schema,
73
+ context: this.#context,
74
+ query: { model: `${model}` },
75
+ });
76
+ }
77
+
78
+ loader(name) {
79
+ const context = this.#context;
80
+
81
+ return new Proxy(loaders[name], {
82
+ get(loader, fn, proxy) {
83
+ if (fn === 'load') return args => loader.load(args, context);
84
+ return Reflect.get(loader, fn, proxy);
85
+ },
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Start a new transaction.
91
+ *
92
+ * @param {boolean} isolated - Create the transaction in isolation (new resolver)
93
+ * @param {Resolver} parent - The parent resolver that created this transaction
94
+ * @returns {Resolver} - A Resolver instance to construct queries in a transaction
95
+ */
96
+ transaction(isolated = true, parent = this) {
97
+ if (isolated) return this.clone().transaction(false, parent);
98
+
99
+ const currSession = this.#sessions.slice(-1).pop();
100
+ const currTransaction = currSession?.slice(-1).pop();
101
+ const realTransaction = new Transaction({ resolver: this, schema: this.#schema, context: this.#context });
102
+ const thunks = currTransaction ? currSession.thunks : []; // If in a transaction, piggy back off session
103
+
104
+ // If we're already in a transaction; add the "real" transaction to the existing session
105
+ // We do this because a "session" holds a group of transactions all bound to the same resolver
106
+ // Therefore this transaction should resolve when THAT resolver is committed or rolled back
107
+ if (currTransaction) currSession.push(realTransaction);
108
+
109
+ // In the case where we are currently in a transaction we need to create a hybrid transaction
110
+ // This transaction is part "real" transaction and part "current" transaction...
111
+ // This transaction ultimately calls currSession.pop() to remove itself (all transactions do)
112
+ const hybridTransaction = {
113
+ match: (...args) => currTransaction?.match(...args), // Bound to current transaction
114
+ commit: () => Promise.resolve(currSession.pop()), // DO NOT COMMIT! It's fate to commit is in "currSession"!
115
+ rollback: () => realTransaction.rollback().then(() => currSession.pop()), // REALLY, we need to rollback()
116
+ };
117
+
118
+ // In ALL cases we MUST create a new session with either the real or hybrid transaction!
119
+ // It is THIS transaction API that is used when resolver.match() is called
120
+ // Additional attributes are defined for use in order to clear data loader cache during transactions
121
+ this.#sessions.push(Object.defineProperties([currTransaction ? hybridTransaction : realTransaction], {
122
+ parent: { value: parent }, // The parent resolver
123
+ thunks: { value: thunks }, // Cleanup functions to run after session is completed (references parent)
124
+ }));
125
+
126
+ return this;
127
+ }
128
+
129
+ /**
130
+ * Auto run (commit or rollback) the current transaction based on the outcome of a provided promise.
131
+ *
132
+ * @param {Promise} promise - A promise to resolve that determines the fate of the current transaction
133
+ * @returns {*} - The promise resolution
134
+ */
135
+ run(promise) {
136
+ return promise.then((results) => {
137
+ return this.commit().then(() => results);
138
+ }).catch((e) => {
139
+ return this.rollback().then(() => Promise.reject(e));
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Commit the current transaction.
145
+ */
146
+ commit() {
147
+ let op = 'commit';
148
+ const errors = [];
149
+ const session = this.#sessions.pop()?.reverse();
150
+
151
+ // All transactions bound to this resolver are to be committed
152
+ return Util.promiseChain(session.map(transaction => () => {
153
+ return transaction[op]().catch((e) => {
154
+ op = 'rollback';
155
+ errors.push(e);
156
+ return transaction[op]().catch(ee => errors.push(ee));
157
+ });
158
+ })).then(() => {
159
+ return errors.length ? Promise.reject(errors) : Promise.all(session.thunks.map(thunk => thunk()));
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Rollback the current transaction
165
+ */
166
+ rollback() {
167
+ const errors = [];
168
+ const session = this.#sessions.pop()?.reverse();
169
+
170
+ // All transactions bound to this resolver are to be rolled back
171
+ return Util.promiseChain(session.map(transaction => () => {
172
+ transaction.rollback().catch(e => errors.push(e));
173
+ })).then(() => {
174
+ return errors.length ? Promise.reject(errors) : Promise.all(session.thunks.map(thunk => thunk()));
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Resolve a query.
180
+ *
181
+ * This method ultimately delegates to a DataSource (for mutations) otherwise a DataLoader.
182
+ *
183
+ * @param {Query} query - The query to resolve
184
+ * @returns {*} - The resolved query result
185
+ */
186
+ async resolve(query) {
187
+ let thunk;
188
+ const tquery = await query.transform();
189
+ const oquery = Object.defineProperties(tquery.toObject(), {
190
+ changeset: {
191
+ get: function get() {
192
+ return oquery.crud === 'update' ? Util.changeset(this.doc, this.input) : undefined;
193
+ },
194
+ },
195
+ });
196
+ const model = this.#schema.models[oquery.model];
197
+ const currSession = this.#sessions.slice(-1).pop();
198
+
199
+ if (oquery.isMutation) {
200
+ thunk = () => model.source.client.resolve(tquery.toDriver().toObject()).then((results) => {
201
+ // We clear the cache immediately (regardless if we're in transaction or not)
202
+ this.clear(model);
203
+
204
+ // If we're in a transaction, we clear the cache of all sessions when this session resolves
205
+ currSession?.thunks.push(...this.#sessions.map(s => () => s.parent.clear(model)));
206
+
207
+ // Return results
208
+ return results;
209
+ });
210
+ } else {
211
+ thunk = () => this.#dataLoaders[model].resolve(tquery);
212
+ }
213
+
214
+ return this.#createSystemEvent(tquery, () => {
215
+ return thunk().then((result) => {
216
+ if (oquery.flags?.required && (result == null || result?.length === 0)) throw Boom.notFound();
217
+ return this.toResultSet(model, result, tquery.toObject());
218
+ });
219
+ });
220
+ }
221
+
222
+ toResultSet(model, result) {
223
+ const self = this;
224
+ if (result == null) return result;
225
+ if (typeof result !== 'object') return result;
226
+ return Object.defineProperties(Util.map(result, (doc) => {
227
+ const $doc = this.#schema.models[model].walk(doc, node => node.value !== undefined && Object.assign(node, { key: node.field.name }), { key: 'key' });
228
+ return Object.defineProperties($doc, {
229
+ $: {
230
+ get: () => {
231
+ return new Proxy(this.match(model).id($doc.id), {
232
+ get(queryResolver, cmd, proxy) {
233
+ return (...args) => {
234
+ switch (cmd) {
235
+ case 'save': {
236
+ return queryResolver.save({ ...$doc, ...args[0] });
237
+ }
238
+ case 'lookup': {
239
+ const field = self.toModel(model).fields[args[0]];
240
+ const where = { [field.linkBy]: $doc[field.linkField.name] };
241
+ return self.match(field.model).where(where);
242
+ }
243
+ default: {
244
+ queryResolver = queryResolver[cmd](...args);
245
+ return queryResolver instanceof Promise ? queryResolver : proxy;
246
+ }
247
+ }
248
+ };
249
+ },
250
+ });
251
+ },
252
+ },
253
+ $model: { value: model },
254
+ $cursor: { value: doc.$cursor },
255
+ });
256
+ }), {
257
+ $pageInfo: { value: result.$pageInfo },
258
+ });
259
+ }
260
+
261
+ toModel(model) {
262
+ return typeof model === 'string' ? this.#schema.models[model] : model;
263
+ }
264
+
265
+ #createDataLoaders() {
266
+ return Object.entries(this.#schema.models).filter(([key, value]) => {
267
+ return value.loader && value.isEntity;
268
+ }).reduce((prev, [key, value]) => {
269
+ return Object.assign(prev, { [key]: new DataLoader(value) });
270
+ }, {});
271
+ }
272
+
273
+ #createSystemEvent(tquery, thunk = () => {}) {
274
+ const query = tquery.toObject();
275
+ const type = query.isMutation ? 'Mutation' : 'Query';
276
+ const event = { schema: this.#schema, context: this.#context, resolver: this, query };
277
+
278
+ return Emitter.emit(`pre${type}`, event).then(async (resultEarly) => {
279
+ if (resultEarly !== undefined) return resultEarly;
280
+ if (Util.isEqual(query.changeset, { added: {}, updated: {}, deleted: {} })) return query.doc;
281
+ if (query.isMutation) query.input = await tquery.pipeline('input', query.input, ['$finalize']);
282
+ if (query.isMutation) await Emitter.emit('finalize', event);
283
+ return thunk().then((result) => {
284
+ query.result = result;
285
+ return Emitter.emit(`post${type}`, event);
286
+ });
287
+ }).then((result = query.result) => {
288
+ query.result = result;
289
+ return Emitter.emit('preResponse', event);
290
+ }).then((result = query.result) => {
291
+ query.result = result;
292
+ return Emitter.emit('postResponse', event);
293
+ }).then((result = query.result) => result).catch((e) => {
294
+ const { data = {} } = e;
295
+ throw Boom.boomify(e, { data: { ...event, ...data } });
296
+ });
297
+ }
298
+
299
+ static $loader(name, resolver, config) {
300
+ if (!name) return loaders;
301
+ if (!resolver) return loaders[name];
302
+ return (loaders[name] = new Loader(resolver, config));
303
+ }
304
+ };
@@ -0,0 +1,49 @@
1
+ const QueryResolverTransaction = require('../query/QueryResolverTransaction');
2
+
3
+ module.exports = class Transaction {
4
+ #schema;
5
+ #context;
6
+ #resolver;
7
+ #sourceMap;
8
+
9
+ constructor(config) {
10
+ this.#schema = config.schema;
11
+ this.#context = config.context;
12
+ this.#resolver = config.resolver;
13
+ this.#sourceMap = new Map();
14
+ }
15
+
16
+ match(model) {
17
+ const { source: { client, supports } } = this.#schema.models[model];
18
+
19
+ // Save client transaction
20
+ if (!this.#sourceMap.has(client)) {
21
+ this.#sourceMap.set(client, supports.includes('transactions') ? client.transaction() : Promise.resolve({
22
+ commit: () => null,
23
+ rollback: () => null,
24
+ }));
25
+ }
26
+
27
+ return new QueryResolverTransaction({
28
+ resolver: this.#resolver,
29
+ schema: this.#schema,
30
+ context: this.#context,
31
+ transaction: this.#sourceMap.get(client),
32
+ query: { model: `${model}` },
33
+ });
34
+ }
35
+
36
+ commit() {
37
+ return this.#close('commit');
38
+ }
39
+
40
+ rollback() {
41
+ return this.#close('rollback');
42
+ }
43
+
44
+ #close(op) {
45
+ return Promise.all(Array.from(this.#sourceMap.entries()).map(([client, promise]) => {
46
+ return promise.then(transaction => transaction[op]());
47
+ }));
48
+ }
49
+ };