@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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.10.x
4
+ - Removed model.tform() method
5
+ - Removed embedApi directive and embedded API GQL
6
+ - Hydrating fields apriori
7
+
3
8
  ## v0.9.x
4
9
  - Subscriptions API
5
10
  - postMutation no longer mutates "doc" and adds "result"
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.9.10",
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.4",
49
+ "@coderich/ratchet": "^1.5.7",
49
50
  "graphql": "^15.5.0",
50
- "mongodb": "3.6.9",
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
@@ -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
- return new ResultSet(query, data);
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 GraphqlFields = require('graphql-fields');
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
- // Getter
16
- this.get = ({ autograph }, model, { id: guid }, required = false, info) => {
17
- const query = { fields: GraphqlFields(info, {}, { processArguments: true }) };
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
  };
Binary file
@@ -1,42 +1,44 @@
1
1
  const FBDataLoader = require('dataloader');
2
- const ResultSet = require('./ResultSet');
3
- const Query = require('../query/Query');
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
- const { id, method } = query.toObject();
16
- const key = method === 'findOne' && id ? 'findOneByIdQueries' : 'allOtherQueries';
17
- prev[key].push({ id, query, i });
18
- return prev;
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
- const sorted = results.flat().filter(Boolean).sort((a, b) => a.i - b.i);
30
- return sorted.map(({ query, data }) => (data != null && typeof data === 'object' ? new ResultSet(query, data) : data));
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
- // return Promise.all(queries.map((query) => {
34
- // return driver.resolve(query.toDriver()).then((data) => {
35
- // return (data != null && typeof data === 'object' ? new ResultSet(query, data) : data);
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: true,
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
+ };