@coderich/autograph 0.10.0 → 0.10.3
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/CHANGELOG.md +20 -3
- package/index.js +2 -8
- package/package.json +5 -7
- package/src/.DS_Store +0 -0
- package/src/core/EventEmitter.js +2 -4
- package/src/core/Resolver.js +32 -57
- package/src/core/Schema.js +5 -38
- package/src/data/.DS_Store +0 -0
- package/src/data/DataLoader.js +71 -32
- package/src/data/DataService.js +82 -59
- package/src/data/Field.js +59 -126
- package/src/data/Model.js +113 -105
- package/src/data/Pipeline.js +184 -0
- package/src/data/Type.js +38 -74
- package/src/driver/MongoDriver.js +27 -22
- package/src/graphql/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +46 -24
- package/src/graphql/ast/Model.js +5 -16
- package/src/graphql/ast/Node.js +0 -25
- package/src/graphql/ast/Schema.js +105 -112
- package/src/graphql/extension/api.js +20 -18
- package/src/graphql/extension/framework.js +27 -33
- package/src/graphql/extension/type.js +2 -2
- package/src/query/Query.js +82 -14
- package/src/query/QueryBuilder.js +38 -30
- package/src/query/QueryBuilderTransaction.js +3 -3
- package/src/query/QueryResolver.js +77 -41
- package/src/query/QueryService.js +24 -42
- package/src/service/app.service.js +70 -13
- package/src/service/event.service.js +30 -73
- package/src/service/graphql.service.js +0 -9
- package/src/service/schema.service.js +5 -3
- package/src/core/GraphQL.js +0 -21
- package/src/core/Rule.js +0 -107
- package/src/core/SchemaDecorator.js +0 -46
- package/src/core/Transformer.js +0 -68
- package/src/data/Memoizer.js +0 -39
- package/src/data/ResultSet.js +0 -205
- package/src/data/stream/DataHydrator.js +0 -58
- package/src/data/stream/ResultSet.js +0 -34
- package/src/data/stream/ResultSetItem.js +0 -158
- package/src/data/stream/ResultSetItemProxy.js +0 -161
- package/src/graphql/ast/SchemaDecorator.js +0 -141
- package/src/graphql/directive/authz.directive.js +0 -84
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
## v0.10.x
|
|
4
|
-
-
|
|
5
|
-
- Removed
|
|
6
|
-
-
|
|
4
|
+
- Replaced ResultSet -> POJOs
|
|
5
|
+
- Removed all $field methods (auto populated)
|
|
6
|
+
- Removed .toObject()
|
|
7
|
+
- $model $save remove $delete $lookup $cursor $pageInfo
|
|
8
|
+
- Removed embedded API completely
|
|
9
|
+
- Removed Directives
|
|
10
|
+
- embedApi -> no replacement
|
|
11
|
+
- enforce -> use pipeline methods
|
|
12
|
+
- resolve -> use graphql resolvers
|
|
13
|
+
- @value -> use @field.instruct directive
|
|
14
|
+
- Removed Model.tform() -> use Model.shapeObject(shape, data)
|
|
15
|
+
- Removed Transformer + Rule -> use Pipeline
|
|
16
|
+
- Removed many pre-defined rules + transformers
|
|
17
|
+
- Moved "validator" to dev dependency -> isEmail
|
|
18
|
+
- Added QueryBuilder.resolve() terminal command
|
|
19
|
+
- Exported SchemaDecorator -> Schema
|
|
20
|
+
- Removed embedded schema SystemEvents (internal emitter also removed)
|
|
21
|
+
- Removed spread of arguments in QueryBuilder terminal commands (must pass in array)
|
|
22
|
+
- Mutate "merged" instead of "input"
|
|
23
|
+
- Validate "payload"
|
|
7
24
|
|
|
8
25
|
## v0.9.x
|
|
9
26
|
- Subscriptions API
|
package/index.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
const Schema = require('./src/core/Schema');
|
|
2
|
-
const SchemaDecorator = require('./src/core/SchemaDecorator');
|
|
3
|
-
const GraphQL = require('./src/core/GraphQL');
|
|
4
2
|
const Resolver = require('./src/core/Resolver');
|
|
5
|
-
const
|
|
3
|
+
const Pipeline = require('./src/data/Pipeline');
|
|
6
4
|
const Driver = require('./src/driver');
|
|
7
|
-
const Transformer = require('./src/core/Transformer');
|
|
8
5
|
const { eventEmitter: Emitter } = require('./src/service/event.service');
|
|
9
6
|
|
|
10
7
|
module.exports = {
|
|
11
8
|
Schema,
|
|
12
|
-
SchemaDecorator,
|
|
13
|
-
GraphQL,
|
|
14
9
|
Resolver,
|
|
15
|
-
Rule,
|
|
16
10
|
Driver,
|
|
17
|
-
Transformer,
|
|
18
11
|
Emitter,
|
|
12
|
+
Pipeline,
|
|
19
13
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderich/autograph",
|
|
3
3
|
"author": "Richard Livolsi (coderich)",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.3",
|
|
5
5
|
"description": "AutoGraph",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"graphql",
|
|
@@ -30,9 +30,7 @@
|
|
|
30
30
|
"ratchet": "ratchet"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@graphql-tools/schema": "^8.3.14",
|
|
34
33
|
"@hapi/boom": "^9.1.0",
|
|
35
|
-
"axios": "^0.21.4",
|
|
36
34
|
"dataloader": "^2.0.0",
|
|
37
35
|
"deepmerge": "^4.2.2",
|
|
38
36
|
"fill-range": "^7.0.1",
|
|
@@ -41,18 +39,18 @@
|
|
|
41
39
|
"lodash": "^4.17.21",
|
|
42
40
|
"mongodb": "^4.8.0",
|
|
43
41
|
"object-hash": "^2.0.1",
|
|
44
|
-
"picomatch": "^2.1.1"
|
|
45
|
-
"uuid": "^3.3.3",
|
|
46
|
-
"validator": "^12.2.0"
|
|
42
|
+
"picomatch": "^2.1.1"
|
|
47
43
|
},
|
|
48
44
|
"devDependencies": {
|
|
49
45
|
"@coderich/ratchet": "^1.5.7",
|
|
46
|
+
"@graphql-tools/schema": "^9.0.1",
|
|
50
47
|
"graphql": "^15.5.0",
|
|
51
48
|
"mongodb-memory-server": "^8.7.2",
|
|
52
49
|
"neo4j-driver": "^4.0.0",
|
|
53
50
|
"neodb": "^3.0.0",
|
|
54
51
|
"redis": "^2.8.0",
|
|
55
|
-
"redis-mock": "^0.47.0"
|
|
52
|
+
"redis-mock": "^0.47.0",
|
|
53
|
+
"validator": "^13.7.0"
|
|
56
54
|
},
|
|
57
55
|
"peerDependencies": {
|
|
58
56
|
"graphql": "*"
|
package/src/.DS_Store
CHANGED
|
Binary file
|
package/src/core/EventEmitter.js
CHANGED
|
@@ -8,7 +8,7 @@ const { ensureArray } = require('../service/app.service');
|
|
|
8
8
|
* If it expects more than 1 we block and wait for it to finish.
|
|
9
9
|
*/
|
|
10
10
|
module.exports = class extends EventEmitter {
|
|
11
|
-
emit(event, data
|
|
11
|
+
emit(event, data) {
|
|
12
12
|
return Promise.all(this.rawListeners(event).map((wrapper) => {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
14
|
const next = result => resolve(result); // If a result is passed this will bypass middleware thunk()
|
|
@@ -17,9 +17,7 @@ module.exports = class extends EventEmitter {
|
|
|
17
17
|
if (numArgs < 2) next();
|
|
18
18
|
});
|
|
19
19
|
})).then((results) => {
|
|
20
|
-
|
|
21
|
-
const target = event === 'validate' ? prev : results;
|
|
22
|
-
return target.find(r => r !== undefined); // There can be only one (result)
|
|
20
|
+
return results.find(r => r !== undefined); // There can be only one (result)
|
|
23
21
|
});
|
|
24
22
|
}
|
|
25
23
|
|
package/src/core/Resolver.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
const Model = require('../data/Model');
|
|
2
|
-
const Query = require('../query/Query');
|
|
3
|
-
const ResultSet = require('../data/ResultSet');
|
|
4
|
-
const DataHydrator = require('../data/stream/DataHydrator');
|
|
5
2
|
const DataLoader = require('../data/DataLoader');
|
|
6
3
|
const DataTransaction = require('../data/DataTransaction');
|
|
4
|
+
const Query = require('../query/Query');
|
|
7
5
|
const QueryBuilder = require('../query/QueryBuilder');
|
|
6
|
+
const { finalizeResults } = require('../data/DataService');
|
|
8
7
|
const { createSystemEvent } = require('../service/event.service');
|
|
9
8
|
|
|
10
9
|
module.exports = class Resolver {
|
|
11
10
|
constructor(schema, context = {}) {
|
|
12
|
-
this.models = schema.getModels();
|
|
13
11
|
this.schema = schema;
|
|
14
12
|
this.context = context;
|
|
13
|
+
this.models = schema.getModels();
|
|
15
14
|
this.loaders = this.models.reduce((prev, model) => prev.set(`${model}`, new DataLoader(this, model)), new Map());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getSchema() {
|
|
18
|
+
return this.schema;
|
|
19
|
+
}
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.getContext = () => this.context;
|
|
21
|
+
getContext() {
|
|
22
|
+
return this.context;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -38,31 +41,6 @@ module.exports = class Resolver {
|
|
|
38
41
|
*/
|
|
39
42
|
raw(model) {
|
|
40
43
|
return this.toModelEntity(model).raw();
|
|
41
|
-
// const entity = this.toModelEntity(model);
|
|
42
|
-
// const driver = entity.raw();
|
|
43
|
-
// if (!method) return driver;
|
|
44
|
-
|
|
45
|
-
// const resolver = this;
|
|
46
|
-
// const crud = ['get', 'find', 'count'].indexOf(method) > -1 ? 'read' : method;
|
|
47
|
-
// const query = new Query({ model: entity, resolver, crud });
|
|
48
|
-
|
|
49
|
-
// return new Proxy(driver, {
|
|
50
|
-
// get(target, prop, rec) {
|
|
51
|
-
// const value = Reflect.get(target, prop, rec);
|
|
52
|
-
|
|
53
|
-
// if (typeof value === 'function') {
|
|
54
|
-
// return (...args) => {
|
|
55
|
-
// return value.bind(target)(...args).then((result) => {
|
|
56
|
-
// const doc = resolver.toResultSet(model, result);
|
|
57
|
-
// createSystemEvent('Response', { method, query: query.doc(doc) });
|
|
58
|
-
// return result;
|
|
59
|
-
// });
|
|
60
|
-
// };
|
|
61
|
-
// }
|
|
62
|
-
|
|
63
|
-
// return value;
|
|
64
|
-
// },
|
|
65
|
-
// });
|
|
66
44
|
}
|
|
67
45
|
|
|
68
46
|
/**
|
|
@@ -77,31 +55,38 @@ module.exports = class Resolver {
|
|
|
77
55
|
}
|
|
78
56
|
|
|
79
57
|
resolve(query) {
|
|
80
|
-
const { model, crud } = query.toObject();
|
|
58
|
+
const { model, crud, method } = query.toObject();
|
|
81
59
|
|
|
82
60
|
switch (crud) {
|
|
83
61
|
case 'create': case 'update': case 'delete': {
|
|
84
62
|
return model.getDriver().resolve(query.toDriver()).then((data) => {
|
|
85
63
|
this.clear(model);
|
|
86
|
-
|
|
87
|
-
return
|
|
64
|
+
const rs = model.shapeObject(model.getShape(), data, query);
|
|
65
|
+
return finalizeResults(rs, query);
|
|
88
66
|
});
|
|
89
67
|
}
|
|
90
68
|
default: {
|
|
91
|
-
// This is needed in SF tests...
|
|
92
69
|
const key = model.idKey();
|
|
93
|
-
const { where
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
70
|
+
const { where } = query.toDriver();
|
|
71
|
+
const lookupValue = where[key];
|
|
72
|
+
|
|
73
|
+
// This is a shortcut to prevent making unnecessary query
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(where, key) && (lookupValue == null || (Array.isArray(lookupValue) && lookupValue.length === 0))) {
|
|
75
|
+
switch (method) {
|
|
76
|
+
case 'count': return Promise.resolve(0);
|
|
77
|
+
case 'findMany': return Promise.resolve([]);
|
|
78
|
+
default: return Promise.resolve(null);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Go through DataLoader to cache results
|
|
97
83
|
return this.loaders.get(`${model}`).load(query);
|
|
98
84
|
}
|
|
99
85
|
}
|
|
100
86
|
}
|
|
101
87
|
|
|
102
88
|
toModel(model) {
|
|
103
|
-
|
|
104
|
-
return $model;
|
|
89
|
+
return model instanceof Model ? model : this.schema.getModel(model);
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
toModelMarked(model) {
|
|
@@ -119,21 +104,11 @@ module.exports = class Resolver {
|
|
|
119
104
|
}
|
|
120
105
|
|
|
121
106
|
toResultSet(model, data, method) {
|
|
122
|
-
|
|
123
|
-
const query = new Query({ model: this
|
|
124
|
-
const result =
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
crud,
|
|
128
|
-
method,
|
|
129
|
-
result,
|
|
130
|
-
doc: result,
|
|
131
|
-
merged: result,
|
|
132
|
-
resolver: this,
|
|
133
|
-
key: `${method}${model}`,
|
|
134
|
-
context: this.getContext(),
|
|
135
|
-
query: query.doc(result).merged(result),
|
|
136
|
-
}, () => result);
|
|
107
|
+
model = this.toModel(model);
|
|
108
|
+
const query = new Query({ model, resolver: this, context: this.context, method });
|
|
109
|
+
const result = model.deserialize(data, query);
|
|
110
|
+
const event = { result, query, ...query.doc(result).merged(result).toObject() };
|
|
111
|
+
return createSystemEvent('Response', event, () => result);
|
|
137
112
|
}
|
|
138
113
|
|
|
139
114
|
// DataLoader Proxy Methods
|
package/src/core/Schema.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
const Model = require('../data/Model');
|
|
2
2
|
const Schema = require('../graphql/ast/Schema');
|
|
3
|
-
const apiExt = require('../graphql/extension/api');
|
|
4
|
-
const typeExt = require('../graphql/extension/type');
|
|
5
|
-
const frameworkExt = require('../graphql/extension/framework');
|
|
6
3
|
const { identifyOnDeletes } = require('../service/schema.service');
|
|
7
|
-
const {
|
|
4
|
+
const { eventEmitter } = require('../service/event.service');
|
|
8
5
|
|
|
9
6
|
// Export class
|
|
10
7
|
module.exports = class extends Schema {
|
|
@@ -23,13 +20,10 @@ module.exports = class extends Schema {
|
|
|
23
20
|
},
|
|
24
21
|
});
|
|
25
22
|
}, {});
|
|
26
|
-
|
|
27
|
-
// Create models
|
|
28
|
-
this.createModels();
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
setup() {
|
|
32
|
-
return
|
|
26
|
+
return eventEmitter.emit('setup', this).then(() => {
|
|
33
27
|
const entities = this.models.filter(m => m.isEntity());
|
|
34
28
|
|
|
35
29
|
// Create model indexes
|
|
@@ -43,38 +37,11 @@ module.exports = class extends Schema {
|
|
|
43
37
|
});
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
initialize() {
|
|
41
|
+
super.initialize();
|
|
47
42
|
this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
|
|
43
|
+
this.models.forEach(model => model.initialize());
|
|
48
44
|
this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
|
|
49
|
-
this.modelsByName = this.models.reduce((prev, model) => Object.assign(prev, { [model.getName()]: model }), {});
|
|
50
|
-
this.modelsByKey = this.models.reduce((prev, model) => Object.assign(prev, { [model.getKey()]: model }), {});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
loadDir(dir, options) {
|
|
54
|
-
super.loadDir(dir, options);
|
|
55
|
-
this.createModels();
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
extend(...schemas) {
|
|
60
|
-
super.extend(...schemas);
|
|
61
|
-
this.createModels();
|
|
62
45
|
return this;
|
|
63
46
|
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Called a runtime to get the full server api schema. Done this way because the
|
|
67
|
-
* end-user needs a chance to call Transformer.factory() etc (thus cannot be moved to constructor)
|
|
68
|
-
*/
|
|
69
|
-
getServerApiSchema() {
|
|
70
|
-
this.extend(frameworkExt(this), typeExt(this));
|
|
71
|
-
this.sextend(apiExt(this));
|
|
72
|
-
return super.getSchema();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
makeServerApiSchema() {
|
|
76
|
-
this.extend(frameworkExt(this), typeExt(this));
|
|
77
|
-
this.sextend(apiExt(this));
|
|
78
|
-
return super.makeExecutableSchema();
|
|
79
|
-
}
|
|
80
47
|
};
|
package/src/data/.DS_Store
CHANGED
|
Binary file
|
package/src/data/DataLoader.js
CHANGED
|
@@ -1,44 +1,83 @@
|
|
|
1
1
|
const FBDataLoader = require('dataloader');
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
// const Query = require('../query/Query');
|
|
2
|
+
const { map, ensureArray, hashObject } = require('../service/app.service');
|
|
3
|
+
const Query = require('../query/Query');
|
|
5
4
|
|
|
6
|
-
const
|
|
5
|
+
const handleData = (data, model, query) => {
|
|
6
|
+
if (data == null || typeof data !== 'object') return data;
|
|
7
|
+
return model.deserialize(data, query);
|
|
8
|
+
};
|
|
7
9
|
|
|
8
|
-
// let counter = 0;
|
|
9
10
|
module.exports = class DataLoader extends FBDataLoader {
|
|
10
11
|
constructor(resolver, model) {
|
|
11
|
-
// const idKey = model.idKey();
|
|
12
12
|
const driver = model.getDriver();
|
|
13
13
|
|
|
14
14
|
return new FBDataLoader((queries) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
let performBatchQuery = false; // If we don't have to batch it's faster to resolve normal
|
|
16
|
+
const defaultBatchName = '__default__'; // Something that won't collide with an actual field name
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Batch queries can save resources and network round-trip latency. However, we have to be careful to
|
|
20
|
+
* preserve the order and adhere to the DataLoader API. This step simply creates a map of batch
|
|
21
|
+
* queries to run; saving the order ("i") along with useful meta information
|
|
22
|
+
*/
|
|
23
|
+
const batchQueries = queries.reduce((prev, query, i) => {
|
|
24
|
+
const { batch = defaultBatchName, where, cmd } = query.toObject();
|
|
25
|
+
const key = batch && (cmd === 'one' || cmd === 'many') ? batch : defaultBatchName;
|
|
26
|
+
if (key !== defaultBatchName) performBatchQuery = true;
|
|
27
|
+
prev[key] = prev[key] || [];
|
|
28
|
+
prev[key].push({ query, where, cmd, i });
|
|
29
|
+
return prev;
|
|
30
|
+
}, {});
|
|
31
|
+
|
|
32
|
+
// Don't batch unless it's worth it!
|
|
33
|
+
if (!performBatchQuery) {
|
|
34
|
+
return Promise.all(queries.map((query) => {
|
|
35
|
+
return driver.resolve(query.toDriver()).then(data => handleData(data, model, query));
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* We have reduced the number of queries down to a smaller set of batch queries to run. The dance
|
|
41
|
+
* performed below retreives the data and then expands the results back into the original queries
|
|
42
|
+
*/
|
|
43
|
+
const whereShape = model.getShape('create', 'where');
|
|
44
|
+
|
|
45
|
+
// console.log(Object.entries(batchQueries).map(([key, value]) => ({ [key]: value.length })));
|
|
46
|
+
|
|
47
|
+
return Promise.all(Object.entries(batchQueries).map(([key, values]) => {
|
|
48
|
+
switch (key) {
|
|
49
|
+
case defaultBatchName: {
|
|
50
|
+
return values.map(({ query, i }) => driver.resolve(query.toDriver()).then(data => handleData(data, model, query)).then(data => ({ data, i })));
|
|
51
|
+
}
|
|
52
|
+
default: {
|
|
53
|
+
const keys = Array.from(new Set(values.map(({ where }) => map(where[key], el => `${el}`)).flat()));
|
|
54
|
+
const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
|
|
55
|
+
const batchWhere = model.shapeObject(whereShape, { [key]: keys }, batchQuery); // This will add back instructs etc
|
|
56
|
+
|
|
57
|
+
return driver.resolve(batchQuery.where(batchWhere).toDriver()).then(data => handleData(data, model, batchQuery)).then((results) => {
|
|
58
|
+
// One-time data transformation on results to make matching back faster (below)
|
|
59
|
+
const resultsByKey = results.reduce((prev, row) => {
|
|
60
|
+
ensureArray(row[key]).forEach((id) => {
|
|
61
|
+
prev[id] = prev[id] || [];
|
|
62
|
+
prev[id].push(row);
|
|
63
|
+
});
|
|
64
|
+
return prev;
|
|
65
|
+
}, {});
|
|
66
|
+
|
|
67
|
+
// Match back
|
|
68
|
+
return values.map(({ where, cmd, i }) => {
|
|
69
|
+
const targets = ensureArray(where[key]).map(t => `${t}`);
|
|
70
|
+
const data = targets.map(t => resultsByKey[t] || null).flat();
|
|
71
|
+
return { i, data: cmd === 'many' ? data.filter(d => d != null) : data[0] };
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}).flat()).then((results) => {
|
|
77
|
+
return results.flat().sort((a, b) => a.i - b.i).map(({ data }) => data);
|
|
78
|
+
});
|
|
40
79
|
}, {
|
|
41
|
-
cache:
|
|
80
|
+
cache: true,
|
|
42
81
|
cacheKeyFn: query => hashObject(query.getCacheKey()),
|
|
43
82
|
});
|
|
44
83
|
}
|
package/src/data/DataService.js
CHANGED
|
@@ -1,19 +1,50 @@
|
|
|
1
|
-
const { remove } = require('lodash');
|
|
2
|
-
const
|
|
3
|
-
|
|
1
|
+
const { get, remove } = require('lodash');
|
|
2
|
+
const { map, isPlainObject, objectContaining, mergeDeep, ensureArray, keyPaths } = require('../service/app.service');
|
|
3
|
+
|
|
4
|
+
exports.finalizeResults = (rs, query) => {
|
|
5
|
+
const { model, resolver } = query.toObject();
|
|
6
|
+
|
|
7
|
+
return map(exports.paginateResults(rs, query), (doc) => {
|
|
8
|
+
return Object.defineProperties(doc, {
|
|
9
|
+
$model: { value: model },
|
|
10
|
+
$save: { value: input => resolver.match(model).id(doc.id).save({ ...doc, ...input }) },
|
|
11
|
+
$remove: { value: (...args) => resolver.match(model).id(doc.id).remove(...args) },
|
|
12
|
+
$delete: { value: (...args) => resolver.match(model).id(doc.id).delete(...args) },
|
|
13
|
+
$lookup: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args) },
|
|
14
|
+
// $resolve: { value: (fieldName, args) => model.getFieldByName(fieldName).resolve(resolver, doc, args, true) },
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* This is cursor-style pagination only
|
|
21
|
+
* You add 2 extra records to the result in order to determine previous/next
|
|
22
|
+
*/
|
|
23
|
+
exports.paginateResults = (rs, query) => {
|
|
24
|
+
const { first, after, last, before, sort } = query.toObject();
|
|
25
|
+
const isPaginating = Boolean(first || last || after || before);
|
|
26
|
+
|
|
27
|
+
// Return right away if not paginating
|
|
28
|
+
if (!isPaginating) return rs;
|
|
4
29
|
|
|
5
|
-
exports.paginateResultSet = (rs, first, after, last, before) => {
|
|
6
30
|
let hasNextPage = false;
|
|
7
31
|
let hasPreviousPage = false;
|
|
8
32
|
const limiter = first || last;
|
|
33
|
+
const sortPaths = keyPaths(sort);
|
|
34
|
+
|
|
35
|
+
// Add $cursor data
|
|
36
|
+
map(rs, (doc) => {
|
|
37
|
+
const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(doc, path) }), {});
|
|
38
|
+
Object.defineProperty(doc, '$cursor', { get() { return Buffer.from(JSON.stringify(sortValues)).toString('base64'); } });
|
|
39
|
+
});
|
|
9
40
|
|
|
10
41
|
// First try to take off the "bookends" ($gte | $lte)
|
|
11
|
-
if (rs.length && rs[0]
|
|
42
|
+
if (rs.length && rs[0].$cursor === after) {
|
|
12
43
|
rs.shift();
|
|
13
44
|
hasPreviousPage = true;
|
|
14
45
|
}
|
|
15
46
|
|
|
16
|
-
if (rs.length && rs[rs.length - 1]
|
|
47
|
+
if (rs.length && rs[rs.length - 1].$cursor === before) {
|
|
17
48
|
rs.pop();
|
|
18
49
|
hasNextPage = true;
|
|
19
50
|
}
|
|
@@ -34,64 +65,56 @@ exports.paginateResultSet = (rs, first, after, last, before) => {
|
|
|
34
65
|
}
|
|
35
66
|
}
|
|
36
67
|
|
|
37
|
-
|
|
68
|
+
// Add $pageInfo
|
|
69
|
+
return Object.defineProperties(rs, {
|
|
70
|
+
$pageInfo: {
|
|
71
|
+
get() {
|
|
72
|
+
return {
|
|
73
|
+
startCursor: get(rs, '0.$cursor', ''),
|
|
74
|
+
endCursor: get(rs, `${rs.length - 1}.$cursor`, ''),
|
|
75
|
+
hasPreviousPage,
|
|
76
|
+
hasNextPage,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
38
81
|
};
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
* @param from <Array>
|
|
42
|
-
* @param to <Array>
|
|
43
|
-
*/
|
|
44
|
-
exports.spliceEmbeddedArray = (query, doc, key, from, to) => {
|
|
45
|
-
const { model } = query.toObject();
|
|
46
|
-
const field = model.getField(key);
|
|
47
|
-
const modelRef = field.getModelRef();
|
|
83
|
+
exports.spliceEmbeddedArray = (array, from, to) => {
|
|
48
84
|
const op = from && to ? 'edit' : (from ? 'pull' : 'push'); // eslint-disable-line no-nested-ternary
|
|
49
|
-
const promises = [];
|
|
50
|
-
|
|
51
|
-
// Can only splice arrays
|
|
52
|
-
if (!field || !field.isArray()) return Promise.reject(Boom.badRequest(`Cannot splice field '${key}'`));
|
|
53
|
-
|
|
54
|
-
// We have to deserialize because this normalizes the data (casting etc)
|
|
55
|
-
let $to = model.deserialize(query, { [key]: to })[key] || to;
|
|
56
|
-
const $from = model.deserialize(query, { [key]: from })[key] || from;
|
|
57
|
-
|
|
58
|
-
// If it's embedded we need to append default/create fields for insertion
|
|
59
|
-
if ($to && field.isEmbedded()) $to = $to.map(el => modelRef.appendDefaultFields(query, modelRef.appendCreateFields(el, true)));
|
|
60
85
|
|
|
61
86
|
// Convenience so the user does not have to explicity type out the same value over and over to replace
|
|
62
|
-
if (
|
|
87
|
+
if (from && from.length > 1 && to && to.length === 1) to = Array.from(from).fill(to[0]);
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
switch (op) {
|
|
90
|
+
case 'edit': {
|
|
91
|
+
array.forEach((el, j) => {
|
|
92
|
+
ensureArray(from).forEach((val, k) => {
|
|
93
|
+
if (objectContaining(el, val)) array[j] = isPlainObject(el) ? mergeDeep(el, ensureArray(to)[k]) : ensureArray(to)[k];
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
// case 'edit': {
|
|
99
|
+
// ensureArray(from).forEach((f, i) => {
|
|
100
|
+
// const t = ensureArray(to)[i];
|
|
101
|
+
// const indexes = array.map((el, j) => (el === f ? j : -1)).filter(index => index !== -1);
|
|
102
|
+
// indexes.forEach(index => (array[index] = t));
|
|
103
|
+
// });
|
|
104
|
+
// break;
|
|
105
|
+
// }
|
|
106
|
+
case 'push': {
|
|
107
|
+
array.push(...to);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case 'pull': {
|
|
111
|
+
remove(array, el => from.find(val => objectContaining(el, val)));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
default: {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
});
|
|
96
|
-
}, doc);
|
|
119
|
+
return array;
|
|
97
120
|
};
|