@coderich/autograph 0.12.0 → 0.13.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/index.js +4 -6
- package/package.json +30 -44
- package/src/data/DataLoader.js +77 -70
- package/src/data/Emitter.js +89 -0
- package/src/data/Loader.js +33 -0
- package/src/data/Pipeline.js +84 -101
- package/src/data/Resolver.js +304 -0
- package/src/data/Transaction.js +49 -0
- package/src/query/Query.js +159 -335
- package/src/query/QueryBuilder.js +228 -114
- package/src/query/QueryResolver.js +110 -205
- package/src/query/QueryResolverTransaction.js +16 -0
- package/src/schema/Schema.js +602 -0
- package/src/service/AppService.js +38 -0
- package/src/service/ErrorService.js +7 -0
- package/CHANGELOG.md +0 -41
- package/LICENSE +0 -21
- package/README.md +0 -76
- package/src/.DS_Store +0 -0
- package/src/core/.DS_Store +0 -0
- package/src/core/Boom.js +0 -9
- package/src/core/EventEmitter.js +0 -95
- package/src/core/Resolver.js +0 -124
- package/src/core/Schema.js +0 -55
- package/src/core/ServerResolver.js +0 -15
- package/src/data/.DS_Store +0 -0
- package/src/data/DataService.js +0 -120
- package/src/data/DataTransaction.js +0 -161
- package/src/data/Field.js +0 -83
- package/src/data/Model.js +0 -214
- package/src/data/TreeMap.js +0 -78
- package/src/data/Type.js +0 -50
- package/src/driver/.DS_Store +0 -0
- package/src/driver/MongoDriver.js +0 -227
- package/src/driver/index.js +0 -11
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +0 -206
- package/src/graphql/ast/Model.js +0 -145
- package/src/graphql/ast/Node.js +0 -291
- package/src/graphql/ast/Schema.js +0 -133
- package/src/graphql/ast/Type.js +0 -26
- package/src/graphql/ast/TypeDefApi.js +0 -93
- package/src/graphql/extension/.DS_Store +0 -0
- package/src/graphql/extension/api.js +0 -193
- package/src/graphql/extension/framework.js +0 -71
- package/src/graphql/extension/type.js +0 -34
- package/src/query/.DS_Store +0 -0
- package/src/query/QueryBuilderTransaction.js +0 -26
- package/src/query/QueryService.js +0 -111
- package/src/service/.DS_Store +0 -0
- package/src/service/app.service.js +0 -319
- package/src/service/decorator.service.js +0 -114
- package/src/service/event.service.js +0 -66
- package/src/service/graphql.service.js +0 -92
- 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
|
+
};
|