@coderich/autograph 0.9.10 → 0.10.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/CHANGELOG.md +5 -0
- package/index.js +2 -0
- package/package.json +10 -7
- package/src/.DS_Store +0 -0
- package/src/core/Resolver.js +7 -1
- package/src/core/SchemaDecorator.js +46 -0
- package/src/core/ServerResolver.js +7 -93
- package/src/data/.DS_Store +0 -0
- package/src/data/DataLoader.js +28 -26
- package/src/data/Model.js +22 -10
- package/src/data/stream/DataHydrator.js +58 -0
- package/src/data/stream/ResultSet.js +34 -0
- package/src/data/stream/ResultSetItem.js +158 -0
- package/src/data/stream/ResultSetItemProxy.js +161 -0
- package/src/driver/MongoDriver.js +46 -18
- package/src/graphql/ast/.DS_Store +0 -0
- package/src/graphql/ast/Field.js +1 -2
- package/src/graphql/ast/Model.js +4 -0
- package/src/graphql/ast/Node.js +0 -7
- package/src/graphql/ast/Schema.js +2 -3
- package/src/graphql/ast/SchemaDecorator.js +141 -0
- package/src/graphql/ast/TypeDefApi.js +93 -0
- package/src/graphql/extension/api.js +1 -16
- package/src/graphql/extension/framework.js +0 -1
- package/src/query/Query.js +1 -19
- package/src/query/QueryBuilder.js +3 -2
- package/src/query/QueryResolver.js +6 -3
- package/src/service/app.service.js +6 -0
- package/src/service/decorator.service.js +21 -288
- package/src/service/event.service.js +1 -0
- package/src/service/graphql.service.js +1 -1
- package/src/data/ResultSet2.js +0 -210
- package/src/data/ResultSet3.js +0 -186
package/CHANGELOG.md
CHANGED
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const Schema = require('./src/core/Schema');
|
|
2
|
+
const SchemaDecorator = require('./src/core/SchemaDecorator');
|
|
2
3
|
const GraphQL = require('./src/core/GraphQL');
|
|
3
4
|
const Resolver = require('./src/core/Resolver');
|
|
4
5
|
const Rule = require('./src/core/Rule');
|
|
@@ -8,6 +9,7 @@ const { eventEmitter: Emitter } = require('./src/service/event.service');
|
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
Schema,
|
|
12
|
+
SchemaDecorator,
|
|
11
13
|
GraphQL,
|
|
12
14
|
Resolver,
|
|
13
15
|
Rule,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coderich/autograph",
|
|
3
3
|
"author": "Richard Livolsi (coderich)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"description": "AutoGraph",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"graphql",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "APP_ROOT_PATH=$(pwd) node ./test/server",
|
|
26
|
-
"test": "APP_ROOT_PATH=$(pwd) ratchet test",
|
|
27
|
-
"test:debug": "APP_ROOT_PATH=$(pwd) node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand",
|
|
26
|
+
"test": "APP_ROOT_PATH=$(pwd) ratchet test --forceExit",
|
|
27
|
+
"test:debug": "APP_ROOT_PATH=$(pwd) node --inspect-brk ./node_modules/jest/bin/jest.js --watch --runInBand --logHeapUsage",
|
|
28
28
|
"lint": "APP_ROOT_PATH=$(pwd) ratchet lint",
|
|
29
29
|
"inspect": "APP_ROOT_PATH=$(pwd) node --expose-gc --inspect=9222 ./src/server",
|
|
30
30
|
"ratchet": "ratchet"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"@graphql-tools/schema": "^8.3.14",
|
|
33
34
|
"@hapi/boom": "^9.1.0",
|
|
34
35
|
"axios": "^0.21.4",
|
|
35
36
|
"dataloader": "^2.0.0",
|
|
@@ -37,23 +38,25 @@
|
|
|
37
38
|
"fill-range": "^7.0.1",
|
|
38
39
|
"glob": "^7.1.6",
|
|
39
40
|
"graphql-fields": "^2.0.3",
|
|
40
|
-
"graphql-tools": "^7.0.5",
|
|
41
41
|
"lodash": "^4.17.21",
|
|
42
|
+
"mongodb": "^4.8.0",
|
|
42
43
|
"object-hash": "^2.0.1",
|
|
43
44
|
"picomatch": "^2.1.1",
|
|
44
45
|
"uuid": "^3.3.3",
|
|
45
46
|
"validator": "^12.2.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@coderich/ratchet": "^1.5.
|
|
49
|
+
"@coderich/ratchet": "^1.5.7",
|
|
49
50
|
"graphql": "^15.5.0",
|
|
50
|
-
"mongodb": "
|
|
51
|
-
"mongodb-memory-server": "^6.9.6",
|
|
51
|
+
"mongodb-memory-server": "^8.7.2",
|
|
52
52
|
"neo4j-driver": "^4.0.0",
|
|
53
53
|
"neodb": "^3.0.0",
|
|
54
54
|
"redis": "^2.8.0",
|
|
55
55
|
"redis-mock": "^0.47.0"
|
|
56
56
|
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"graphql": "*"
|
|
59
|
+
},
|
|
57
60
|
"repository": {
|
|
58
61
|
"type": "git",
|
|
59
62
|
"url": "git@github.com:coderich/autograph.git"
|
package/src/.DS_Store
CHANGED
|
Binary file
|
package/src/core/Resolver.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const Model = require('../data/Model');
|
|
2
2
|
const Query = require('../query/Query');
|
|
3
3
|
const ResultSet = require('../data/ResultSet');
|
|
4
|
+
const DataHydrator = require('../data/stream/DataHydrator');
|
|
4
5
|
const DataLoader = require('../data/DataLoader');
|
|
5
6
|
const DataTransaction = require('../data/DataTransaction');
|
|
6
7
|
const QueryBuilder = require('../query/QueryBuilder');
|
|
@@ -71,6 +72,10 @@ module.exports = class Resolver {
|
|
|
71
72
|
return new DataTransaction(this, parentTxn);
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
disconnect(model) {
|
|
76
|
+
return this.toModelEntity(model).getDriver().disconnect();
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
resolve(query) {
|
|
75
80
|
const { model, crud } = query.toObject();
|
|
76
81
|
|
|
@@ -78,7 +83,8 @@ module.exports = class Resolver {
|
|
|
78
83
|
case 'create': case 'update': case 'delete': {
|
|
79
84
|
return model.getDriver().resolve(query.toDriver()).then((data) => {
|
|
80
85
|
this.clear(model);
|
|
81
|
-
|
|
86
|
+
data = model.shape(data, 'deserialize');
|
|
87
|
+
return new DataHydrator(query, data);
|
|
82
88
|
});
|
|
83
89
|
}
|
|
84
90
|
default: {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const Model = require('../data/Model');
|
|
2
|
+
const SchemaDecorator = require('../graphql/ast/SchemaDecorator');
|
|
3
|
+
const { identifyOnDeletes } = require('../service/schema.service');
|
|
4
|
+
const { createSystemEvent } = require('../service/event.service');
|
|
5
|
+
|
|
6
|
+
// Export class
|
|
7
|
+
module.exports = class extends SchemaDecorator {
|
|
8
|
+
constructor(schema, stores) {
|
|
9
|
+
super(schema);
|
|
10
|
+
|
|
11
|
+
// Create drivers
|
|
12
|
+
this.drivers = Object.entries(stores).reduce((prev, [key, value]) => {
|
|
13
|
+
const { Driver } = value;
|
|
14
|
+
|
|
15
|
+
return Object.assign(prev, {
|
|
16
|
+
[key]: {
|
|
17
|
+
dao: new Driver(value, this),
|
|
18
|
+
idKey: Driver.idKey,
|
|
19
|
+
idValue: Driver.idValue,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}, {});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setup() {
|
|
26
|
+
return createSystemEvent('Setup', this, () => {
|
|
27
|
+
const entities = this.models.filter(m => m.isEntity());
|
|
28
|
+
|
|
29
|
+
// Create model indexes
|
|
30
|
+
return Promise.all(entities.map(async (model) => {
|
|
31
|
+
const key = model.getKey();
|
|
32
|
+
const indexes = model.getIndexes();
|
|
33
|
+
const driver = model.getDriver();
|
|
34
|
+
if (driver.createCollection) await driver.createCollection(key);
|
|
35
|
+
return driver.createIndexes(key, indexes);
|
|
36
|
+
}));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
initialize() {
|
|
41
|
+
super.initialize();
|
|
42
|
+
this.models = super.getModels().map(model => new Model(this, model, this.drivers[model.getDriverName()]));
|
|
43
|
+
this.models.forEach(model => model.referentialIntegrity(identifyOnDeletes(this.models, model)));
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -1,101 +1,15 @@
|
|
|
1
|
-
const
|
|
2
|
-
const Boom = require('./Boom');
|
|
3
|
-
const { unrollGuid, guidToId, ensureArray, promiseChain } = require('../service/app.service');
|
|
4
|
-
|
|
5
|
-
const normalizeQuery = (args = {}, info) => {
|
|
6
|
-
const query = { fields: GraphqlFields(info, {}, { processArguments: true }), ...args };
|
|
7
|
-
return query;
|
|
8
|
-
// const { fields = {} } = query;
|
|
9
|
-
// const { first, last, before, after } = args;
|
|
10
|
-
// return Object.assign(query, { pagination: { first, last, before, after }, fields: _.get(fields, 'edges.node') });
|
|
11
|
-
};
|
|
1
|
+
const { unrollGuid, guidToId } = require('../service/app.service');
|
|
12
2
|
|
|
13
3
|
module.exports = class ServerResolver {
|
|
14
4
|
constructor() {
|
|
15
|
-
//
|
|
16
|
-
this.get = ({ autograph }, model, { id: guid }, required = false,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return autograph.resolver.match(model).id(guidToId(autograph, guid)).merge(query).one().then((doc) => {
|
|
20
|
-
if (!doc && required) throw Boom.notFound(`${model} Not Found`);
|
|
21
|
-
return doc;
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Query
|
|
26
|
-
this.query = ({ autograph }, model, args, info) => autograph.resolver.match(model).merge(normalizeQuery(args, info)).many();
|
|
27
|
-
this.count = ({ autograph }, model, args, info) => autograph.resolver.match(model).merge(args).count();
|
|
5
|
+
// Queries
|
|
6
|
+
this.get = ({ autograph }, model, { id: guid }, required = false, query) => autograph.resolver.match(model).id(guidToId(autograph, guid)).select(query.fields).one({ required });
|
|
7
|
+
this.query = ({ autograph }, model, args, query) => autograph.resolver.match(model).select(query.fields).merge(args).many();
|
|
8
|
+
this.count = ({ autograph }, model, args, query) => autograph.resolver.match(model).merge(args).count();
|
|
28
9
|
|
|
29
10
|
// Mutations
|
|
30
|
-
this.create = ({ autograph }, model, { input, meta }, query) => autograph.resolver.match(model).meta(meta).save(unrollGuid(autograph, model, input));
|
|
11
|
+
this.create = ({ autograph }, model, { input, meta }, query) => autograph.resolver.match(model).select(query.fields).meta(meta).save(unrollGuid(autograph, model, input));
|
|
31
12
|
this.delete = ({ autograph }, model, { id: guid, meta }, query) => autograph.resolver.match(model).id(guidToId(autograph, guid)).select(query.fields).meta(meta).remove();
|
|
32
|
-
|
|
33
|
-
this.update = ({ autograph }, model, args, query) => {
|
|
34
|
-
const { resolver } = autograph;
|
|
35
|
-
const { id: guid, input, splice = {}, meta } = args;
|
|
36
|
-
|
|
37
|
-
return autograph.resolver.match(model).id(guidToId(autograph, guid)).select(query.fields).meta(meta).save(unrollGuid(autograph, model, input)).then((result) => {
|
|
38
|
-
if (!Object.keys(splice).length) return result;
|
|
39
|
-
|
|
40
|
-
const txn = resolver.transaction();
|
|
41
|
-
|
|
42
|
-
return promiseChain(Object.entries(splice).map(([key, { with: from, put: to, splice: subSplice }]) => {
|
|
43
|
-
to = to ? ensureArray(to) : to;
|
|
44
|
-
from = from ? ensureArray(from) : from;
|
|
45
|
-
const selectAll = Boolean(from && from.length === 0);
|
|
46
|
-
|
|
47
|
-
return async () => {
|
|
48
|
-
if (subSplice) {
|
|
49
|
-
console.log('we have to subSplice this');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (selectAll) {
|
|
53
|
-
// Empty
|
|
54
|
-
if (to === null || (to && to.length === 0)) {
|
|
55
|
-
txn.match(model).id(result.id).select(query.fields).meta(meta).save({ [key]: to });
|
|
56
|
-
return txn.exec();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Replace
|
|
60
|
-
if (to) {
|
|
61
|
-
const modelRef = model.getField(key).getModelRef();
|
|
62
|
-
const createTo = await resolver.match(modelRef).save(...to);
|
|
63
|
-
// const createTo = await Promise.all(to.map(el => modelRef.appendDefaultValues(el).then(r => modelRef.appendCreateFields(r, true))));
|
|
64
|
-
txn.match(model).id(result.id).select(query.fields).meta(meta).save({ [key]: createTo });
|
|
65
|
-
return txn.exec();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Nonsense
|
|
69
|
-
return Promise.resolve([result]);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Update
|
|
73
|
-
if (from && to) {
|
|
74
|
-
txn.match(model).id(result.id).meta(meta).splice(key, from, to);
|
|
75
|
-
return txn.exec();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Remove
|
|
79
|
-
if (from && to === null) {
|
|
80
|
-
txn.match(model).id(result.id).meta(meta).splice(key, from);
|
|
81
|
-
return txn.exec();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Add
|
|
85
|
-
if (!from && to) {
|
|
86
|
-
txn.match(model).id(result.id).meta(meta).splice(key, null, to);
|
|
87
|
-
return txn.exec();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Nonsense
|
|
91
|
-
return Promise.resolve([result]);
|
|
92
|
-
};
|
|
93
|
-
})).then((results) => {
|
|
94
|
-
return txn.commit().then(() => {
|
|
95
|
-
return results.pop().pop();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
};
|
|
13
|
+
this.update = ({ autograph }, model, { id: guid, meta, input }, query) => autograph.resolver.match(model).id(guidToId(autograph, guid)).select(query.fields).meta(meta).save(unrollGuid(autograph, model, input));
|
|
100
14
|
}
|
|
101
15
|
};
|
package/src/data/.DS_Store
CHANGED
|
Binary file
|
package/src/data/DataLoader.js
CHANGED
|
@@ -1,42 +1,44 @@
|
|
|
1
1
|
const FBDataLoader = require('dataloader');
|
|
2
|
-
const
|
|
3
|
-
const
|
|
2
|
+
const DataHydrator = require('./stream/DataHydrator');
|
|
3
|
+
// const ResultSet = require('./ResultSet');
|
|
4
|
+
// const Query = require('../query/Query');
|
|
5
|
+
|
|
4
6
|
const { hashObject } = require('../service/app.service');
|
|
5
7
|
|
|
6
8
|
// let counter = 0;
|
|
7
9
|
module.exports = class DataLoader extends FBDataLoader {
|
|
8
10
|
constructor(resolver, model) {
|
|
9
|
-
const idKey = model.idKey();
|
|
11
|
+
// const idKey = model.idKey();
|
|
10
12
|
const driver = model.getDriver();
|
|
11
13
|
|
|
12
14
|
return new FBDataLoader((queries) => {
|
|
13
|
-
// The idea is to group the "findOne by id" queries together to make 1 query instead
|
|
14
|
-
const { findOneByIdQueries, allOtherQueries } = queries.reduce((prev, query, i) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}, { findOneByIdQueries: [], allOtherQueries: [] });
|
|
15
|
+
// // The idea is to group the "findOne by id" queries together to make 1 query instead
|
|
16
|
+
// const { findOneByIdQueries, allOtherQueries } = queries.reduce((prev, query, i) => {
|
|
17
|
+
// const { id, method } = query.toObject();
|
|
18
|
+
// const key = method === 'findOne' && id ? 'findOneByIdQueries' : 'allOtherQueries';
|
|
19
|
+
// prev[key].push({ id, query, i });
|
|
20
|
+
// return prev;
|
|
21
|
+
// }, { findOneByIdQueries: [], allOtherQueries: [] });
|
|
20
22
|
|
|
21
|
-
// Aggregate ids
|
|
22
|
-
const ids = Array.from(new Set(findOneByIdQueries.map(el => `${el.id}`)));
|
|
23
|
-
const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
|
|
24
|
-
const batchWhere = model.transform(batchQuery, { id: ids }, 'serialize', true);
|
|
25
|
-
const promises = [Promise.all(allOtherQueries.map(({ query, i }) => driver.resolve(query.toDriver()).then(data => ({ data, query, i }))))];
|
|
26
|
-
if (ids.length) promises.push(driver.resolve(batchQuery.where(batchWhere).toDriver()).then(results => findOneByIdQueries.map(({ query, id, i }) => ({ i, query, data: results.find(r => `${r[idKey]}` === `${id}`) || null }))));
|
|
23
|
+
// // Aggregate ids
|
|
24
|
+
// const ids = Array.from(new Set(findOneByIdQueries.map(el => `${el.id}`)));
|
|
25
|
+
// const batchQuery = new Query({ resolver, model, method: 'findMany', crud: 'read' });
|
|
26
|
+
// const batchWhere = model.transform(batchQuery, { id: ids }, 'serialize', true);
|
|
27
|
+
// const promises = [Promise.all(allOtherQueries.map(({ query, i }) => driver.resolve(query.toDriver()).then(data => ({ data, query, i }))))];
|
|
28
|
+
// if (ids.length) promises.push(driver.resolve(batchQuery.where(batchWhere).toDriver()).then(results => findOneByIdQueries.map(({ query, id, i }) => ({ i, query, data: results.find(r => `${r[idKey]}` === `${id}`) || null }))));
|
|
27
29
|
|
|
28
|
-
return Promise.all(promises).then((results) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
30
|
+
// return Promise.all(promises).then((results) => {
|
|
31
|
+
// const sorted = results.flat().filter(Boolean).sort((a, b) => a.i - b.i);
|
|
32
|
+
// return sorted.map(({ query, data }) => (data != null && typeof data === 'object' ? new ResultSet(query, data) : data));
|
|
33
|
+
// });
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
return Promise.all(queries.map((query) => {
|
|
36
|
+
return driver.resolve(query.toDriver()).then((data) => {
|
|
37
|
+
return (data != null && typeof data === 'object' ? new DataHydrator(query, data) : data);
|
|
38
|
+
});
|
|
39
|
+
}));
|
|
38
40
|
}, {
|
|
39
|
-
cache:
|
|
41
|
+
cache: false,
|
|
40
42
|
cacheKeyFn: query => hashObject(query.getCacheKey()),
|
|
41
43
|
});
|
|
42
44
|
}
|
package/src/data/Model.js
CHANGED
|
@@ -126,6 +126,9 @@ module.exports = class extends Model {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Normalizes data by renaming keys and serdes on field values (unless keysOnly)
|
|
131
|
+
*/
|
|
129
132
|
normalize(query, data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), keysOnly = false) {
|
|
130
133
|
// Transform all the data
|
|
131
134
|
return map(data, (doc) => {
|
|
@@ -140,6 +143,25 @@ module.exports = class extends Model {
|
|
|
140
143
|
});
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
getShape(serdes = 'deserialize') {
|
|
147
|
+
return this.getSelectFields().map((field) => {
|
|
148
|
+
const [from, to] = serdes === 'serialize' ? [field.getName(), field.getKey()] : [field.getKey(), field.getName()];
|
|
149
|
+
return { from, to, type: field.getDataType(), isArray: field.isArray(), shape: field.isEmbedded() ? field.getModelRef().getShape(serdes) : null };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
shape(data, serdes = (() => { throw new Error('No Sir Sir SerDes!'); }), shape) {
|
|
154
|
+
shape = shape || this.getShape(serdes);
|
|
155
|
+
|
|
156
|
+
return map(data, (doc) => {
|
|
157
|
+
return shape.reduce((prev, { from, to, shape: subShape }) => {
|
|
158
|
+
const value = doc[from];
|
|
159
|
+
if (value === undefined) return prev;
|
|
160
|
+
return Object.assign(prev, { [to]: subShape ? this.shape(value, serdes, subShape) : value });
|
|
161
|
+
}, {});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
143
165
|
validate(query, data) {
|
|
144
166
|
const normalized = this.deserialize(query, data);
|
|
145
167
|
|
|
@@ -150,14 +172,4 @@ module.exports = class extends Model {
|
|
|
150
172
|
})));
|
|
151
173
|
}));
|
|
152
174
|
}
|
|
153
|
-
|
|
154
|
-
tform(query, data) {
|
|
155
|
-
return map(data, (doc) => {
|
|
156
|
-
return Object.keys(doc).map(k => this.getField(k)).filter(Boolean).reduce((prev, curr) => {
|
|
157
|
-
const key = curr.getName();
|
|
158
|
-
const value = doc[key];
|
|
159
|
-
return Object.assign(prev, { [key]: curr.tform(query, value) });
|
|
160
|
-
}, {});
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
175
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { get } = require('lodash');
|
|
2
|
+
const Stream = require('stream');
|
|
3
|
+
const ResultSet = require('./ResultSet');
|
|
4
|
+
const ResultSetItem = require('./ResultSetItemProxy');
|
|
5
|
+
const { promiseChain, mapPromise, toKeyObj } = require('../../service/app.service');
|
|
6
|
+
|
|
7
|
+
module.exports = class DataHydrator {
|
|
8
|
+
constructor(query, data) {
|
|
9
|
+
let { select = {} } = query.toObject();
|
|
10
|
+
select = toKeyObj(select);
|
|
11
|
+
return data instanceof Stream ? DataHydrator.stream(query, data, select) : DataHydrator.process(query, data, select);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static stream(query, stream, select) {
|
|
15
|
+
const promises = [];
|
|
16
|
+
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
stream.on('data', (data) => {
|
|
19
|
+
promises.push(DataHydrator.hydrate(query, data, select));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
stream.on('error', reject);
|
|
23
|
+
|
|
24
|
+
stream.on('end', () => {
|
|
25
|
+
Promise.all(promises).then(results => resolve(new ResultSet(query, results)));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static process(query, data, select) {
|
|
31
|
+
return mapPromise(data, d => DataHydrator.hydrate(query, d, select)).then(results => new ResultSet(query, results));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static hydrate(query, data, select) {
|
|
35
|
+
const loopArray = Array.from(new Array(Math.max(0, ...Object.keys(select).map(key => key.split('.').length))));
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const item = new ResultSetItem(query, data);
|
|
39
|
+
|
|
40
|
+
return Promise.all(Object.keys(select).map((path) => {
|
|
41
|
+
const arrPath = path.split('.');
|
|
42
|
+
|
|
43
|
+
return Promise.all(loopArray.map((el, depth) => {
|
|
44
|
+
const arr = arrPath.slice(0, depth);
|
|
45
|
+
const $arr = arr.map(ele => `$${ele}`);
|
|
46
|
+
const key = arr[depth];
|
|
47
|
+
|
|
48
|
+
// id has special handling
|
|
49
|
+
if (!key || key === 'id') return Promise.resolve();
|
|
50
|
+
|
|
51
|
+
// Resolve all other attributes
|
|
52
|
+
get(item, arr.join('.'));
|
|
53
|
+
return promiseChain($arr.map($prop => chain => (chain.pop() || item)[$prop]()));
|
|
54
|
+
}));
|
|
55
|
+
})).then(() => resolve(item)).catch(e => reject(e));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const { get } = require('lodash');
|
|
2
|
+
const DataService = require('../DataService');
|
|
3
|
+
const { ensureArray } = require('../../service/app.service');
|
|
4
|
+
|
|
5
|
+
module.exports = class ResultSet {
|
|
6
|
+
constructor(query, data, adjustForPagination = true) {
|
|
7
|
+
if (data == null) return data;
|
|
8
|
+
const { first, after, last, before } = query.toObject();
|
|
9
|
+
|
|
10
|
+
let hasNextPage = false;
|
|
11
|
+
let hasPreviousPage = false;
|
|
12
|
+
if (adjustForPagination && data.length) (({ hasPreviousPage, hasNextPage } = DataService.paginateResultSet(data, first, after, last, before)));
|
|
13
|
+
|
|
14
|
+
return Object.defineProperties(data, {
|
|
15
|
+
$$pageInfo: {
|
|
16
|
+
get() {
|
|
17
|
+
const edges = ensureArray(data);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
startCursor: get(edges, '0.$$cursor', ''),
|
|
21
|
+
endCursor: get(edges, `${edges.length - 1}.$$cursor`, ''),
|
|
22
|
+
hasPreviousPage,
|
|
23
|
+
hasNextPage,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
enumerable: false,
|
|
27
|
+
},
|
|
28
|
+
$$isResultSet: {
|
|
29
|
+
value: true,
|
|
30
|
+
enumerable: false,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const { get } = require('lodash');
|
|
2
|
+
const ResultSet = require('./ResultSet');
|
|
3
|
+
const { map, keyPaths, mapPromise, toGUID, hashObject } = require('../../service/app.service');
|
|
4
|
+
|
|
5
|
+
module.exports = class ResultSetItem {
|
|
6
|
+
constructor(query, doc) {
|
|
7
|
+
if (doc == null) return doc;
|
|
8
|
+
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
const { resolver, model, sort } = query.toObject();
|
|
11
|
+
const fields = model.getFields().filter(f => f.getName() !== 'id');
|
|
12
|
+
|
|
13
|
+
const definition = fields.reduce((prev, field) => {
|
|
14
|
+
const name = field.getName();
|
|
15
|
+
const $name = `$${name}`;
|
|
16
|
+
const value = doc[name];
|
|
17
|
+
|
|
18
|
+
// Field attributes
|
|
19
|
+
prev[name] = {
|
|
20
|
+
get() {
|
|
21
|
+
if (cache.has(name)) return cache.get(name);
|
|
22
|
+
let $value = field.deserialize(query, value);
|
|
23
|
+
|
|
24
|
+
if ($value != null && field.isEmbedded()) {
|
|
25
|
+
const newModel = field.getModelRef();
|
|
26
|
+
const newQuery = query.model(newModel);
|
|
27
|
+
const newFields = newModel.getFields().filter(f => f.getName() !== 'id');
|
|
28
|
+
$value = new ResultSet(newQuery, map($value, v => new ResultSetItem(newQuery, v, newFields)), false);
|
|
29
|
+
}
|
|
30
|
+
cache.set(name, $value);
|
|
31
|
+
return $value;
|
|
32
|
+
},
|
|
33
|
+
set($value) {
|
|
34
|
+
cache.set(name, $value);
|
|
35
|
+
},
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true, // Allows things like delete
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Hydrated field attributes
|
|
41
|
+
prev[`$${name}`] = {
|
|
42
|
+
get() {
|
|
43
|
+
return (args = {}) => {
|
|
44
|
+
// Ensure where clause
|
|
45
|
+
args.where = args.where || {};
|
|
46
|
+
|
|
47
|
+
// Cache
|
|
48
|
+
const cacheKey = `${$name}-${hashObject(args)}`;
|
|
49
|
+
if (cache.has(cacheKey)) return cache.get(cacheKey);
|
|
50
|
+
|
|
51
|
+
const promise = new Promise((resolve, reject) => {
|
|
52
|
+
(() => {
|
|
53
|
+
const $value = this[name];
|
|
54
|
+
|
|
55
|
+
if (field.isScalar() || field.isEmbedded()) return Promise.resolve($value);
|
|
56
|
+
|
|
57
|
+
const modelRef = field.getModelRef();
|
|
58
|
+
|
|
59
|
+
if (field.isArray()) {
|
|
60
|
+
if (field.isVirtual()) {
|
|
61
|
+
args.where[[field.getVirtualField()]] = this.id; // Is where[[field.getVirtualField()]] correct?
|
|
62
|
+
return resolver.match(modelRef).merge(args).many();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Not a "required" query + strip out nulls
|
|
66
|
+
args.where.id = $value;
|
|
67
|
+
return resolver.match(modelRef).merge(args).many();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (field.isVirtual()) {
|
|
71
|
+
args.where[[field.getVirtualField()]] = this.id;
|
|
72
|
+
return resolver.match(modelRef).merge(args).one();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolver.match(modelRef).id($value).one({ required: field.isRequired() });
|
|
76
|
+
})().then((results) => {
|
|
77
|
+
if (results == null) return field.resolve(query, results); // Allow field to determine
|
|
78
|
+
return mapPromise(results, result => field.resolve(query, result)).then(() => results); // Resolve the inside fields but still return "results"!!!!
|
|
79
|
+
}).then((resolved) => {
|
|
80
|
+
resolve(resolved);
|
|
81
|
+
}).catch((e) => {
|
|
82
|
+
reject(e);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
cache.set(cacheKey, promise);
|
|
87
|
+
return promise;
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
enumerable: false,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Field count (let's assume it's a Connection Type - meaning dont try with anything else)
|
|
94
|
+
prev[`$${name}:count`] = {
|
|
95
|
+
get() {
|
|
96
|
+
return (q = {}) => {
|
|
97
|
+
q.where = q.where || {};
|
|
98
|
+
if (field.isVirtual()) q.where[field.getVirtualField()] = this.id;
|
|
99
|
+
else q.where.id = this[name];
|
|
100
|
+
return resolver.match(field.getModelRef()).merge(q).count();
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
enumerable: false,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return prev;
|
|
107
|
+
}, {
|
|
108
|
+
id: {
|
|
109
|
+
get() { return doc.id || doc[model.idKey()]; },
|
|
110
|
+
set(id) { doc.id = id; }, // Embedded array of documents need to set id
|
|
111
|
+
enumerable: true,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
$id: {
|
|
115
|
+
get() { return toGUID(model.getName(), this.id); },
|
|
116
|
+
enumerable: false,
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
$$cursor: {
|
|
120
|
+
get() {
|
|
121
|
+
const sortPaths = keyPaths(sort);
|
|
122
|
+
const sortValues = sortPaths.reduce((prv, path) => Object.assign(prv, { [path]: get(this, path) }), {});
|
|
123
|
+
const sortJSON = JSON.stringify(sortValues);
|
|
124
|
+
return Buffer.from(sortJSON).toString('base64');
|
|
125
|
+
},
|
|
126
|
+
enumerable: false,
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
$$model: {
|
|
130
|
+
value: model,
|
|
131
|
+
enumerable: false,
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
$$isResultSetItem: {
|
|
135
|
+
value: true,
|
|
136
|
+
enumerable: false,
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
$$save: {
|
|
140
|
+
get() { return input => resolver.match(model).id(this.id).save({ ...this, ...input }); },
|
|
141
|
+
enumerable: false,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
$$remove: {
|
|
145
|
+
get() { return () => resolver.match(model).id(this.id).remove(); },
|
|
146
|
+
enumerable: false,
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
$$delete: {
|
|
150
|
+
get() { return () => resolver.match(model).id(this.id).delete(); },
|
|
151
|
+
enumerable: false,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Create and return ResultSetItem
|
|
156
|
+
return Object.defineProperties(this, definition);
|
|
157
|
+
}
|
|
158
|
+
};
|