@snowtop/ent 0.1.0-alpha81 → 0.1.0-alpha87

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.
@@ -1,14 +1,16 @@
1
- import { Ent, EntConstructor, Viewer, ID, Data, PrivacyPolicy, Context } from "../core/base";
1
+ import { Ent, EntConstructor, Viewer, ID, Data, PrivacyPolicy, Context, WriteOperation } from "../core/base";
2
2
  import { DataOperation, AssocEdgeInputOptions } from "../core/ent";
3
3
  import { Queryer } from "../core/db";
4
4
  import { TransformedUpdateOperation, UpdateOperation } from "../schema";
5
- export declare enum WriteOperation {
6
- Insert = "insert",
7
- Edit = "edit",
8
- Delete = "delete"
9
- }
5
+ import { FieldInfoMap } from "../schema/schema";
6
+ export { WriteOperation };
10
7
  declare type MaybeNull<T extends Ent> = T | null;
11
8
  declare type TMaybleNullableEnt<T extends Ent> = T | MaybeNull<T>;
9
+ interface BuilderOrchestrator {
10
+ __getOptions(): {
11
+ fieldInfo: FieldInfoMap;
12
+ };
13
+ }
12
14
  export interface Builder<TEnt extends Ent<TViewer>, TViewer extends Viewer = Viewer, TExistingEnt extends TMaybleNullableEnt<TEnt> = MaybeNull<TEnt>> {
13
15
  existingEnt: TExistingEnt;
14
16
  ent: EntConstructor<TEnt, TViewer>;
@@ -18,6 +20,8 @@ export interface Builder<TEnt extends Ent<TViewer>, TViewer extends Viewer = Vie
18
20
  operation: WriteOperation;
19
21
  editedEnt?(): Promise<TEnt | null>;
20
22
  nodeType: string;
23
+ getInput(): Data;
24
+ orchestrator: BuilderOrchestrator;
21
25
  }
22
26
  export interface Executor extends Iterable<DataOperation>, Iterator<DataOperation> {
23
27
  placeholderID: ID;
@@ -66,4 +70,3 @@ interface Orchestrator {
66
70
  viewer: Viewer;
67
71
  }
68
72
  export declare function setEdgeTypeInGroup<T extends string>(orchestrator: Orchestrator, inputEnumValue: string, id1: ID, id2: ID, nodeType: string, m: Map<T, string>): Promise<void>;
69
- export {};
package/action/action.js CHANGED
@@ -1,14 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setEdgeTypeInGroup = exports.saveBuilderX = exports.saveBuilder = exports.WriteOperation = void 0;
4
+ const base_1 = require("../core/base");
5
+ Object.defineProperty(exports, "WriteOperation", { enumerable: true, get: function () { return base_1.WriteOperation; } });
4
6
  const ent_1 = require("../core/ent");
5
7
  const logger_1 = require("../core/logger");
6
- var WriteOperation;
7
- (function (WriteOperation) {
8
- WriteOperation["Insert"] = "insert";
9
- WriteOperation["Edit"] = "edit";
10
- WriteOperation["Delete"] = "delete";
11
- })(WriteOperation = exports.WriteOperation || (exports.WriteOperation = {}));
12
8
  async function saveBuilder(builder) {
13
9
  await saveBuilderImpl(builder, false);
14
10
  }
@@ -49,6 +49,7 @@ export declare class Orchestrator<TEnt extends Ent<TViewer>, TInput extends Data
49
49
  private disableTransformations;
50
50
  private memoizedGetFields;
51
51
  constructor(options: OrchestratorOptions<TEnt, TInput, TViewer, TExistingEnt>);
52
+ __getOptions(): OrchestratorOptions<any, any, any, any>;
52
53
  private addEdge;
53
54
  setDisableTransformations(val: boolean): void;
54
55
  addInboundEdge<T2 extends Ent>(id1: ID | Builder<T2, any>, edgeType: string, nodeType: string, options?: AssocEdgeInputOptions): void;
@@ -88,6 +88,10 @@ class Orchestrator {
88
88
  this.existingEnt = this.options.builder.existingEnt;
89
89
  this.memoizedGetFields = (0, memoizee_1.default)(this.getFieldsInfo.bind(this));
90
90
  }
91
+ // don't type this because we don't care
92
+ __getOptions() {
93
+ return this.options;
94
+ }
91
95
  addEdge(edge, op) {
92
96
  this.edgeSet.add(edge.edgeType);
93
97
  let m1 = this.edges.get(edge.edgeType) || new Map();
@@ -607,8 +611,12 @@ class Orchestrator {
607
611
  // build up data to be saved...
608
612
  let data = {};
609
613
  let logValues = {};
614
+ let needsFullDataChecks = [];
610
615
  for (const [fieldName, field] of schemaFields) {
611
616
  let value = editedFields.get(fieldName);
617
+ if (field.validateWithFullData) {
618
+ needsFullDataChecks.push(fieldName);
619
+ }
612
620
  if (value === undefined && op === action_1.WriteOperation.Insert) {
613
621
  // null allowed
614
622
  value = this.defaultFieldsByFieldName[fieldName];
@@ -626,6 +634,21 @@ class Orchestrator {
626
634
  logValues[dbKey] = field.logValue(value);
627
635
  }
628
636
  }
637
+ for (const fieldName of needsFullDataChecks) {
638
+ const field = schemaFields.get(fieldName);
639
+ let value = editedFields.get(fieldName);
640
+ // @ts-ignore...
641
+ // type hackery because it's hard
642
+ const v = await field.validateWithFullData(value, this.options.builder);
643
+ if (!v) {
644
+ if (value === undefined) {
645
+ errors.push(new Error(`field ${fieldName} set to undefined when it can't be nullable`));
646
+ }
647
+ else {
648
+ errors.push(new Error(`field ${fieldName} set to null when it can't be nullable`));
649
+ }
650
+ }
651
+ }
629
652
  // we ignored default values while editing.
630
653
  // if we're editing and there's data, add default values
631
654
  if (op === action_1.WriteOperation.Edit && this.hasData(data)) {
package/core/base.d.ts CHANGED
@@ -5,12 +5,12 @@ export interface Loader<T, V> {
5
5
  loadMany?(keys: T[]): Promise<(V | null)[]>;
6
6
  clearAll(): any;
7
7
  }
8
- interface LoaderWithLoadMany<T, V> extends Loader<T, V> {
9
- loadMany(keys: T[]): Promise<(V | null)[]>;
8
+ export interface LoaderWithLoadMany<T, V> extends Loader<T, V> {
9
+ loadMany(keys: T[]): Promise<V[]>;
10
10
  }
11
- export interface LoaderFactory<T, V> {
11
+ export interface LoaderFactory<K, V> {
12
12
  name: string;
13
- createLoader(context?: Context): Loader<T, V>;
13
+ createLoader(context?: Context): Loader<K, V>;
14
14
  }
15
15
  interface LoaderFactoryWithLoaderMany<T, V> extends LoaderFactory<T, V> {
16
16
  createLoader(context?: Context): LoaderWithLoadMany<T, V>;
@@ -25,12 +25,12 @@ export interface PrimableLoader<T, V> extends Loader<T, V> {
25
25
  }
26
26
  interface cache {
27
27
  getLoader<T, V>(name: string, create: () => Loader<T, V>): Loader<T, V>;
28
+ getLoaderWithLoadMany<T, V>(name: string, create: () => LoaderWithLoadMany<T, V>): LoaderWithLoadMany<T, V>;
28
29
  getCachedRows(options: queryOptions): Data[] | null;
29
30
  getCachedRow(options: queryOptions): Data | null;
30
31
  primeCache(options: queryOptions, rows: Data[]): void;
31
32
  primeCache(options: queryOptions, rows: Data): void;
32
33
  clearCache(): void;
33
- getEntCache(): Map<string, Ent | Error | null>;
34
34
  }
35
35
  interface queryOptions {
36
36
  fields: string[];
@@ -143,4 +143,9 @@ export interface PrivacyPolicyRule<TEnt extends Ent = Ent, TViewer = Viewer> {
143
143
  export interface PrivacyPolicy<TEnt extends Ent = Ent, TViewer = Viewer> {
144
144
  rules: PrivacyPolicyRule<TEnt, TViewer>[];
145
145
  }
146
+ export declare enum WriteOperation {
147
+ Insert = "insert",
148
+ Edit = "edit",
149
+ Delete = "delete"
150
+ }
146
151
  export {};
package/core/base.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DenyWithReason = exports.Deny = exports.Skip = exports.Allow = void 0;
3
+ exports.WriteOperation = exports.DenyWithReason = exports.Deny = exports.Skip = exports.Allow = void 0;
4
4
  // Privacy
5
5
  var privacyResult;
6
6
  (function (privacyResult) {
@@ -53,3 +53,9 @@ function DenyWithReason(e) {
53
53
  };
54
54
  }
55
55
  exports.DenyWithReason = DenyWithReason;
56
+ var WriteOperation;
57
+ (function (WriteOperation) {
58
+ WriteOperation["Insert"] = "insert";
59
+ WriteOperation["Edit"] = "edit";
60
+ WriteOperation["Delete"] = "delete";
61
+ })(WriteOperation = exports.WriteOperation || (exports.WriteOperation = {}));
package/core/context.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { Viewer, Data, Loader, Ent } from "./base";
2
+ import { Viewer, Data, Loader, LoaderWithLoadMany } from "./base";
3
3
  import { IncomingMessage, ServerResponse } from "http";
4
4
  import * as clause from "./clause";
5
5
  import { Context } from "./base";
@@ -11,17 +11,17 @@ export interface RequestContext<TViewer extends Viewer = Viewer> extends Context
11
11
  }
12
12
  export declare class ContextCache {
13
13
  loaders: Map<string, Loader<any, any>>;
14
+ loaderWithLoadMany: Map<string, LoaderWithLoadMany<any, any>>;
14
15
  getLoader<T, V>(name: string, create: () => Loader<T, V>): Loader<T, V>;
16
+ getLoaderWithLoadMany<T, V>(name: string, create: () => LoaderWithLoadMany<T, V>): LoaderWithLoadMany<T, V>;
15
17
  private itemMap;
16
18
  private listMap;
17
- private entCache;
18
19
  private getkey;
19
20
  getCachedRows(options: queryOptions): Data[] | null;
20
21
  getCachedRow(options: queryOptions): Data | null;
21
22
  primeCache(options: queryOptions, rows: Data[]): void;
22
23
  primeCache(options: queryOptions, rows: Data): void;
23
24
  clearCache(): void;
24
- getEntCache(): Map<string, Error | Ent<any> | null>;
25
25
  }
26
26
  interface queryOptions {
27
27
  fields: string[];
package/core/context.js CHANGED
@@ -5,10 +5,11 @@ const logger_1 = require("./logger");
5
5
  class ContextCache {
6
6
  constructor() {
7
7
  this.loaders = new Map();
8
+ // we should eventually combine the two but better for typing to be separate for now
9
+ this.loaderWithLoadMany = new Map();
8
10
  // we have a per-table map to make it easier to purge and have less things to compare with
9
11
  this.itemMap = new Map();
10
12
  this.listMap = new Map();
11
- this.entCache = new Map();
12
13
  }
13
14
  getLoader(name, create) {
14
15
  let l = this.loaders.get(name);
@@ -20,6 +21,16 @@ class ContextCache {
20
21
  this.loaders.set(name, l);
21
22
  return l;
22
23
  }
24
+ getLoaderWithLoadMany(name, create) {
25
+ let l = this.loaderWithLoadMany.get(name);
26
+ if (l) {
27
+ return l;
28
+ }
29
+ (0, logger_1.log)("debug", `new context-aware loader created for ${name}`);
30
+ l = create();
31
+ this.loaderWithLoadMany.set(name, l);
32
+ return l;
33
+ }
23
34
  // tableName is ignored bcos already indexed on that
24
35
  // maybe we just want to store sql queries???
25
36
  getkey(options) {
@@ -80,13 +91,15 @@ class ContextCache {
80
91
  // but may have some benefits by explicitily doing so?
81
92
  loader.clearAll();
82
93
  }
94
+ for (const [_key, loader] of this.loaderWithLoadMany) {
95
+ // may not need this since we're clearing the loaders themselves...
96
+ // but may have some benefits by explicitily doing so?
97
+ loader.clearAll();
98
+ }
83
99
  this.loaders.clear();
100
+ this.loaderWithLoadMany.clear();
84
101
  this.itemMap.clear();
85
102
  this.listMap.clear();
86
- this.entCache.clear();
87
- }
88
- getEntCache() {
89
- return this.entCache;
90
103
  }
91
104
  }
92
105
  exports.ContextCache = ContextCache;
package/core/ent.d.ts CHANGED
@@ -225,7 +225,7 @@ interface loadEdgeForIDOptions<T extends AssocEdge> extends loadCustomEdgesOptio
225
225
  }
226
226
  export declare function loadEdgeForID2<T extends AssocEdge>(options: loadEdgeForIDOptions<T>): Promise<T | undefined>;
227
227
  export declare function loadNodesByEdge<T extends Ent>(viewer: Viewer, id1: ID, edgeType: string, options: LoadEntOptions<T>): Promise<T[]>;
228
- export declare function applyPrivacyPolicyForRow<TEnt extends Ent<TViewer>, TViewer extends Viewer>(viewer: TViewer, options: LoadEntOptions<TEnt, TViewer>, row: Data | null): Promise<TEnt | null>;
228
+ export declare function applyPrivacyPolicyForRow<TEnt extends Ent<TViewer>, TViewer extends Viewer>(viewer: TViewer, options: LoadEntOptions<TEnt, TViewer>, row: Data): Promise<TEnt | null>;
229
229
  export declare function applyPrivacyPolicyForRows<TEnt extends Ent<TViewer>, TViewer extends Viewer>(viewer: TViewer, rows: Data[], options: LoadEntOptions<TEnt, TViewer>): Promise<Map<ID, TEnt>>;
230
230
  export declare function getEdgeTypeInGroup<T extends string>(viewer: Viewer, id1: ID, id2: ID, m: Map<T, string>): Promise<[T, AssocEdge] | undefined>;
231
231
  export {};
package/core/ent.js CHANGED
@@ -57,6 +57,34 @@ class cacheMap {
57
57
  return this.m.clear();
58
58
  }
59
59
  }
60
+ class entCacheMap {
61
+ constructor(viewer, options) {
62
+ this.viewer = viewer;
63
+ this.options = options;
64
+ this.m = new Map();
65
+ this.logEnabled = false;
66
+ this.logEnabled = (0, logger_1.logEnabled)("cache");
67
+ }
68
+ get(id) {
69
+ const ret = this.m.get(id);
70
+ if (this.logEnabled && ret) {
71
+ const key = getEntKey(this.viewer, id, this.options);
72
+ (0, logger_1.log)("cache", {
73
+ "ent-cache-hit": key,
74
+ });
75
+ }
76
+ return ret;
77
+ }
78
+ set(key, value) {
79
+ return this.m.set(key, value);
80
+ }
81
+ delete(key) {
82
+ return this.m.delete(key);
83
+ }
84
+ clear() {
85
+ return this.m.clear();
86
+ }
87
+ }
60
88
  function createDataLoader(options) {
61
89
  const loaderOptions = {};
62
90
  // if query logging is enabled, we should log what's happening with loader
@@ -87,60 +115,116 @@ function createDataLoader(options) {
87
115
  return result;
88
116
  }, loaderOptions);
89
117
  }
90
- function getEntKey(viewer, id, options) {
91
- return `${viewer.instanceKey()}:${options.loaderFactory.name}:${id}`;
92
- }
93
- exports.getEntKey = getEntKey;
94
- // fetches the ent from cache if cache exists.
95
- function entFromCacheMaybe(viewer, id, options) {
96
- const cache = viewer.context?.cache?.getEntCache();
97
- if (!cache) {
98
- return {};
99
- }
100
- const key = getEntKey(viewer, id, options);
101
- const r = cache.get(key);
102
- if (r !== undefined) {
103
- (0, logger_1.log)("cache", {
104
- "ent-cache-hit": key,
105
- });
118
+ // used to wrap errors that would eventually be thrown in ents
119
+ // not an Error because DataLoader automatically rejects that
120
+ class ErrorWrapper {
121
+ constructor(error) {
122
+ this.error = error;
106
123
  }
107
- return {
108
- key,
109
- ent: r instanceof Error ? undefined : r,
110
- error: r instanceof Error ? r : undefined,
111
- cache,
112
- };
113
124
  }
114
- // Ent accessors
115
- async function applyPrivacyPolicyForRowAndStoreInCache(viewer, options, row, info) {
116
- const ent = await applyPrivacyPolicyForRow(viewer, options, row);
117
- if (info.cache && info.key) {
118
- info.cache.set(info.key, ent);
119
- }
120
- return ent instanceof Error ? null : ent;
125
+ function createEntLoader(viewer, options, map) {
126
+ // share the cache across loaders even if we create a new instance
127
+ const loaderOptions = {};
128
+ loaderOptions.cacheMap = map;
129
+ return new dataloader_1.default(async (ids) => {
130
+ if (!ids.length) {
131
+ return [];
132
+ }
133
+ let result = [];
134
+ const loader = options.loaderFactory.createLoader(viewer.context);
135
+ const rows = await loader.loadMany(ids);
136
+ // this is a loader which should return the same order based on passed-in ids
137
+ // so let's depend on that...
138
+ for (let idx = 0; idx < rows.length; idx++) {
139
+ const row = rows[idx];
140
+ // db error
141
+ if (row instanceof Error) {
142
+ result[idx] = row;
143
+ continue;
144
+ }
145
+ else if (!row) {
146
+ result[idx] = new ErrorWrapper(new Error(`couldn't find row for value ${ids[idx]}`));
147
+ }
148
+ else {
149
+ const r = await applyPrivacyPolicyForRowImpl(viewer, options, row);
150
+ if (r instanceof Error) {
151
+ result[idx] = new ErrorWrapper(r);
152
+ }
153
+ else {
154
+ result[idx] = r;
155
+ }
156
+ }
157
+ }
158
+ return result;
159
+ }, loaderOptions);
121
160
  }
122
- async function applyPrivacyPolicyForRowAndStoreInCacheX(viewer, options, row, info) {
123
- const ent = await applyPrivacyPolicyForRowImpl(viewer, options, row);
124
- if (info.cache && info.key) {
125
- info.cache.set(info.key, ent);
161
+ class EntLoader {
162
+ constructor(viewer, options) {
163
+ this.viewer = viewer;
164
+ this.options = options;
165
+ this.map = new entCacheMap(viewer, options);
166
+ this.loader = createEntLoader(this.viewer, this.options, this.map);
126
167
  }
127
- if (ent instanceof Error) {
128
- throw ent;
168
+ getMap() {
169
+ return this.map;
129
170
  }
130
- if (ent === null) {
131
- throw new Error(`applyPrivacyPolicyForRowImpl returned null when it shouldn't. ent error`);
171
+ async load(id) {
172
+ return this.loader.load(id);
173
+ }
174
+ async loadMany(ids) {
175
+ return this.loader.loadMany(ids);
176
+ }
177
+ prime(id, ent) {
178
+ this.loader.prime(id, ent);
179
+ }
180
+ clear(id) {
181
+ this.loader.clear(id);
182
+ }
183
+ clearAll() {
184
+ this.loader.clearAll();
132
185
  }
133
- return ent;
134
186
  }
135
- async function loadEnt(viewer, id, options) {
136
- const info = entFromCacheMaybe(viewer, id, options);
137
- if (info.ent !== undefined) {
138
- return info.ent;
187
+ function getEntLoader(viewer, options) {
188
+ if (!viewer.context?.cache) {
189
+ return new EntLoader(viewer, options);
139
190
  }
140
- const row = await options.loaderFactory.createLoader(viewer.context).load(id);
141
- return applyPrivacyPolicyForRowAndStoreInCache(viewer, options, row, info);
191
+ const name = `ent-loader:${viewer.instanceKey()}:${options.loaderFactory.name}`;
192
+ return viewer.context.cache.getLoaderWithLoadMany(name, () => new EntLoader(viewer, options));
193
+ }
194
+ function getEntKey(viewer, id, options) {
195
+ return `${viewer.instanceKey()}:${options.loaderFactory.name}:${id}`;
196
+ }
197
+ exports.getEntKey = getEntKey;
198
+ async function loadEnt(viewer, id, options) {
199
+ const r = await getEntLoader(viewer, options).load(id);
200
+ return r instanceof ErrorWrapper ? null : r;
142
201
  }
143
202
  exports.loadEnt = loadEnt;
203
+ async function applyPrivacyPolicyForRowAndStoreInEntLoader(viewer, row, options,
204
+ // can pass in loader when calling this for multi-id cases...
205
+ loader) {
206
+ if (!loader) {
207
+ loader = getEntLoader(viewer, options);
208
+ }
209
+ // TODO every row.id needs to be audited...
210
+ // https://github.com/lolopinto/ent/issues/1064
211
+ const id = row.id;
212
+ // we should check the ent loader cache to see if this is already there
213
+ // TODO hmm... we eventually need a custom data-loader for this too so that it's all done correctly if there's a complicated fetch deep down in graphql
214
+ const result = loader.getMap().get(id);
215
+ if (result !== undefined) {
216
+ return result;
217
+ }
218
+ const r = await applyPrivacyPolicyForRowImpl(viewer, options, row);
219
+ if (r instanceof Error) {
220
+ loader.prime(id, new ErrorWrapper(r));
221
+ return new ErrorWrapper(r);
222
+ }
223
+ else {
224
+ loader.prime(id, r);
225
+ return r;
226
+ }
227
+ }
144
228
  // this is the same implementation-wise (right now) as loadEnt. it's just clearer that it's not loaded via ID.
145
229
  // used for load via email address etc
146
230
  async function loadEntViaKey(viewer, key, options) {
@@ -150,29 +234,16 @@ async function loadEntViaKey(viewer, key, options) {
150
234
  if (!row) {
151
235
  return null;
152
236
  }
153
- // TODO every row.id needs to be audited...
154
- // https://github.com/lolopinto/ent/issues/1064
155
- const info = entFromCacheMaybe(viewer, row.id, options);
156
- if (info.ent !== undefined) {
157
- return info.ent;
158
- }
159
- return applyPrivacyPolicyForRowAndStoreInCache(viewer, options, row, info);
237
+ const r = await applyPrivacyPolicyForRowAndStoreInEntLoader(viewer, row, options);
238
+ return r instanceof ErrorWrapper ? null : r;
160
239
  }
161
240
  exports.loadEntViaKey = loadEntViaKey;
162
241
  async function loadEntX(viewer, id, options) {
163
- const info = entFromCacheMaybe(viewer, id, options);
164
- if (info.error !== undefined) {
165
- throw info.error;
166
- }
167
- if (info.ent !== undefined && info.ent !== null) {
168
- return info.ent;
169
- }
170
- const row = await options.loaderFactory.createLoader(viewer.context).load(id);
171
- if (!row) {
172
- // todo make this better
173
- throw new Error(`${options.loaderFactory.name}: couldn't find row for value ${id}`);
242
+ const r = await getEntLoader(viewer, options).load(id);
243
+ if (r instanceof ErrorWrapper) {
244
+ throw r.error;
174
245
  }
175
- return applyPrivacyPolicyForRowAndStoreInCacheX(viewer, options, row, info);
246
+ return r;
176
247
  }
177
248
  exports.loadEntX = loadEntX;
178
249
  async function loadEntXViaKey(viewer, key, options) {
@@ -183,14 +254,11 @@ async function loadEntXViaKey(viewer, key, options) {
183
254
  // todo make this better
184
255
  throw new Error(`${options.loaderFactory.name}: couldn't find row for value ${key}`);
185
256
  }
186
- const info = entFromCacheMaybe(viewer, row.id, options);
187
- if (info.error !== undefined) {
188
- throw info.error;
189
- }
190
- if (info.ent !== undefined && info.ent !== null) {
191
- return info.ent;
257
+ const r = await applyPrivacyPolicyForRowAndStoreInEntLoader(viewer, row, options);
258
+ if (r instanceof ErrorWrapper) {
259
+ throw r.error;
192
260
  }
193
- return applyPrivacyPolicyForRowAndStoreInCacheX(viewer, options, row, info);
261
+ return r;
194
262
  }
195
263
  exports.loadEntXViaKey = loadEntXViaKey;
196
264
  /**
@@ -203,6 +271,9 @@ async function loadEntFromClause(viewer, options, clause) {
203
271
  context: viewer.context,
204
272
  };
205
273
  const row = await loadRow(rowOptions);
274
+ if (row === null) {
275
+ return null;
276
+ }
206
277
  return applyPrivacyPolicyForRow(viewer, options, row);
207
278
  }
208
279
  exports.loadEntFromClause = loadEntFromClause;
@@ -228,62 +299,15 @@ async function loadEnts(viewer, options, ...ids) {
228
299
  }
229
300
  // result
230
301
  let m = new Map();
231
- const cache = viewer.context?.cache?.getEntCache();
232
- let toFetch = [];
233
- if (cache) {
234
- for (const id of ids) {
235
- const key = getEntKey(viewer, id, options);
236
- const ent = cache.get(key);
237
- if (ent !== undefined) {
238
- (0, logger_1.log)("cache", {
239
- "ent-cache-hit": key,
240
- });
241
- if (ent === null) {
242
- // TODO this should return null if not loadable...
243
- // https://github.com/lolopinto/ent/issues/1070
244
- continue;
245
- }
246
- // @ts-ignore
247
- m.set(id, ent);
248
- }
249
- else {
250
- toFetch.push(id);
251
- }
302
+ const ret = await getEntLoader(viewer, options).loadMany(ids);
303
+ for (const r of ret) {
304
+ if (r instanceof Error) {
305
+ throw r;
252
306
  }
253
- }
254
- else {
255
- toFetch = ids;
256
- }
257
- // all in ent cache!
258
- if (!toFetch.length) {
259
- return m;
260
- }
261
- const l = options.loaderFactory.createLoader(viewer.context);
262
- const rows = await l.loadMany(toFetch);
263
- let rows2 = [];
264
- for (const row of rows) {
265
- if (!row) {
307
+ if (r instanceof ErrorWrapper) {
266
308
  continue;
267
309
  }
268
- if (row instanceof Error) {
269
- throw row;
270
- }
271
- rows2.push(row);
272
- }
273
- const m2 = await applyPrivacyPolicyForRows(viewer, rows2, options);
274
- for (const row of rows2) {
275
- const id = row[options.loaderFactory.options?.key || "id"];
276
- const ent = m2.get(id);
277
- if (cache) {
278
- // put back in cache...
279
- // store null for rows that can't be seen
280
- cache.set(getEntKey(viewer, id, options), ent ?? null);
281
- }
282
- if (ent !== undefined) {
283
- // TODO this should return null if not loadable...?
284
- // TODO https://github.com/lolopinto/ent/issues/1070
285
- m.set(id, ent);
286
- }
310
+ m.set(r.id, r);
287
311
  }
288
312
  return m;
289
313
  }
@@ -320,23 +344,16 @@ exports.loadEntsFromClause = loadEntsFromClause;
320
344
  async function loadCustomEnts(viewer, options, query) {
321
345
  const rows = await loadCustomData(options, query, viewer.context);
322
346
  const result = new Array(rows.length);
347
+ if (!rows.length) {
348
+ return [];
349
+ }
350
+ const entLoader = getEntLoader(viewer, options);
323
351
  await Promise.all(rows.map(async (row, idx) => {
324
- // TODO what if key is different
325
- // TODO https://github.com/lolopinto/ent/issues/1064
326
- const info = entFromCacheMaybe(viewer, row.id, options);
327
- if (info.ent !== undefined) {
328
- if (info.ent === null) {
329
- // we're done here
330
- return;
331
- }
332
- // @ts-ignore
333
- result[idx] = info.ent;
352
+ const r = await applyPrivacyPolicyForRowAndStoreInEntLoader(viewer, row, options, entLoader);
353
+ if (r instanceof ErrorWrapper) {
334
354
  return;
335
355
  }
336
- const privacyEnt = await applyPrivacyPolicyForRowAndStoreInCache(viewer, options, row, info);
337
- if (privacyEnt) {
338
- result[idx] = privacyEnt;
339
- }
356
+ result[idx] = r;
340
357
  }));
341
358
  // filter ents that aren't visible because of privacy
342
359
  return result.filter((r) => r !== undefined);
@@ -457,14 +474,11 @@ exports.loadDerivedEntX = loadDerivedEntX;
457
474
  // everything calls into this two so should be fine
458
475
  // TODO is there a smarter way to not instantiate two objects here?
459
476
  async function applyPrivacyPolicyForEnt(viewer, ent, data, fieldPrivacyOptions) {
460
- if (ent) {
461
- const error = await (0, privacy_1.applyPrivacyPolicyImpl)(viewer, ent.getPrivacyPolicy(), ent);
462
- if (error === null) {
463
- return doFieldPrivacy(viewer, ent, data, fieldPrivacyOptions);
464
- }
465
- return error;
477
+ const error = await (0, privacy_1.applyPrivacyPolicyImpl)(viewer, ent.getPrivacyPolicy(), ent);
478
+ if (error === null) {
479
+ return doFieldPrivacy(viewer, ent, data, fieldPrivacyOptions);
466
480
  }
467
- return null;
481
+ return error;
468
482
  }
469
483
  async function applyPrivacyPolicyForEntX(viewer, ent, data, options) {
470
484
  const r = await applyPrivacyPolicyForEnt(viewer, ent, data, options);
@@ -1505,9 +1519,6 @@ async function applyPrivacyPolicyForRow(viewer, options, row) {
1505
1519
  }
1506
1520
  exports.applyPrivacyPolicyForRow = applyPrivacyPolicyForRow;
1507
1521
  async function applyPrivacyPolicyForRowImpl(viewer, options, row) {
1508
- if (!row) {
1509
- return null;
1510
- }
1511
1522
  const ent = new options.ent(viewer, row);
1512
1523
  return applyPrivacyPolicyForEnt(viewer, ent, row, options);
1513
1524
  }
@@ -11,7 +11,7 @@ export declare class ObjectLoader<T> implements Loader<T, Data | null> {
11
11
  private initPrime;
12
12
  load(key: T): Promise<Data | null>;
13
13
  clearAll(): void;
14
- loadMany(keys: T[]): Promise<Data[]>;
14
+ loadMany(keys: T[]): Promise<Array<Data | null>>;
15
15
  prime(data: Data): void;
16
16
  primeAll(data: Data): void;
17
17
  }
@@ -24,6 +24,6 @@ export declare class ObjectLoaderFactory<T> implements LoaderFactory<T, Data | n
24
24
  private toPrime;
25
25
  constructor(options: ObjectLoaderOptions);
26
26
  createLoader(context?: Context): ObjectLoader<T>;
27
- addToPrime(factory: ObjectLoaderFactory<T>): void;
27
+ addToPrime(factory: ObjectLoaderFactory<T>): this;
28
28
  }
29
29
  export {};
@@ -29,6 +29,47 @@ const clause = __importStar(require("../clause"));
29
29
  const logger_1 = require("../logger");
30
30
  const loader_1 = require("./loader");
31
31
  const memoizee_1 = __importDefault(require("memoizee"));
32
+ async function loadRowsForLoader(options, ids, context) {
33
+ let col = options.key;
34
+ let cls = clause.In(col, ...ids);
35
+ if (options.clause) {
36
+ let optionClause;
37
+ if (typeof options.clause === "function") {
38
+ optionClause = options.clause();
39
+ }
40
+ else {
41
+ optionClause = options.clause;
42
+ }
43
+ if (optionClause) {
44
+ cls = clause.And(cls, optionClause);
45
+ }
46
+ }
47
+ const rowOptions = {
48
+ ...options,
49
+ clause: cls,
50
+ context,
51
+ };
52
+ let m = new Map();
53
+ let result = [];
54
+ for (let i = 0; i < ids.length; i++) {
55
+ result.push(null);
56
+ // store the index....
57
+ m.set(ids[i], i);
58
+ }
59
+ const rows = await (0, ent_1.loadRows)(rowOptions);
60
+ for (const row of rows) {
61
+ const id = row[col];
62
+ if (id === undefined) {
63
+ throw new Error(`need to query for column ${col} when using an object loader because the query may not be sorted and we need the id to maintain sort order`);
64
+ }
65
+ const idx = m.get(id);
66
+ if (idx === undefined) {
67
+ throw new Error(`malformed query. got ${id} back but didn't query for it`);
68
+ }
69
+ result[idx] = row;
70
+ }
71
+ return result;
72
+ }
32
73
  // optional clause...
33
74
  // so ObjectLoaderFactory and createDataLoader need to take a new optional field which is a clause that's always added here
34
75
  // and we need a disableTransform which skips loader completely and uses loadRow...
@@ -42,45 +83,8 @@ function createDataLoader(options) {
42
83
  if (!ids.length) {
43
84
  return [];
44
85
  }
45
- let col = options.key;
46
- let cls = clause.In(col, ...ids);
47
- if (options.clause) {
48
- let optionClause;
49
- if (typeof options.clause === "function") {
50
- optionClause = options.clause();
51
- }
52
- else {
53
- optionClause = options.clause;
54
- }
55
- if (optionClause) {
56
- cls = clause.And(cls, optionClause);
57
- }
58
- }
59
- const rowOptions = {
60
- ...options,
61
- clause: cls,
62
- };
63
- let m = new Map();
64
- let result = [];
65
- for (let i = 0; i < ids.length; i++) {
66
- result.push(null);
67
- // store the index....
68
- m.set(ids[i], i);
69
- }
70
86
  // context not needed because we're creating a loader which has its own cache which is being used here
71
- const rows = await (0, ent_1.loadRows)(rowOptions);
72
- for (const row of rows) {
73
- const id = row[col];
74
- if (id === undefined) {
75
- throw new Error(`need to query for column ${col} when using an object loader because the query may not be sorted and we need the id to maintain sort order`);
76
- }
77
- const idx = m.get(id);
78
- if (idx === undefined) {
79
- throw new Error(`malformed query. got ${id} back but didn't query for it`);
80
- }
81
- result[idx] = row;
82
- }
83
- return result;
87
+ return loadRowsForLoader(options, ids);
84
88
  }, loaderOptions);
85
89
  }
86
90
  class ObjectLoader {
@@ -147,7 +151,7 @@ class ObjectLoader {
147
151
  clause: cls,
148
152
  context: this.context,
149
153
  };
150
- return await (0, ent_1.loadRow)(rowOptions);
154
+ return (0, ent_1.loadRow)(rowOptions);
151
155
  }
152
156
  clearAll() {
153
157
  this.loader && this.loader.clearAll();
@@ -156,25 +160,7 @@ class ObjectLoader {
156
160
  if (this.loader) {
157
161
  return await this.loader.loadMany(keys);
158
162
  }
159
- let cls = clause.In(this.options.key, ...keys);
160
- if (this.options.clause) {
161
- let optionClause;
162
- if (typeof this.options.clause === "function") {
163
- optionClause = this.options.clause();
164
- }
165
- else {
166
- optionClause = this.options.clause;
167
- }
168
- if (optionClause) {
169
- cls = clause.And(cls, optionClause);
170
- }
171
- }
172
- const rowOptions = {
173
- ...this.options,
174
- clause: cls,
175
- context: this.context,
176
- };
177
- return await (0, ent_1.loadRows)(rowOptions);
163
+ return loadRowsForLoader(this.options, keys, this.context);
178
164
  }
179
165
  prime(data) {
180
166
  // we have this data from somewhere else, prime it in the c
@@ -225,6 +211,7 @@ class ObjectLoaderFactory {
225
211
  // because there's usually self references here
226
212
  addToPrime(factory) {
227
213
  this.toPrime.push(factory);
214
+ return this;
228
215
  }
229
216
  }
230
217
  exports.ObjectLoaderFactory = ObjectLoaderFactory;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowtop/ent",
3
- "version": "0.1.0-alpha81",
3
+ "version": "0.1.0-alpha87",
4
4
  "description": "snowtop ent framework",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
package/schema/field.d.ts CHANGED
@@ -131,8 +131,6 @@ export interface EnumOptions extends FieldOptions {
131
131
  graphQLType?: string;
132
132
  createEnumType?: boolean;
133
133
  }
134
- export interface StringEnumOptions extends EnumOptions {
135
- }
136
134
  /**
137
135
  * @deprecated Use StringEnumField
138
136
  */
@@ -147,6 +145,11 @@ export declare class EnumField extends BaseField implements Field {
147
145
  }
148
146
  export declare class StringEnumField extends EnumField {
149
147
  }
148
+ export interface PolymorphicStringEnumOptions extends EnumOptions {
149
+ parentFieldToValidate: string;
150
+ }
151
+ export interface StringEnumOptions extends EnumOptions {
152
+ }
150
153
  export declare function EnumType(options: StringEnumOptions): EnumField;
151
154
  declare type IntEnumMap = {
152
155
  [key: string]: number;
package/schema/field.js CHANGED
@@ -24,6 +24,7 @@ const luxon_1 = require("luxon");
24
24
  const snake_case_1 = require("snake-case");
25
25
  const util_1 = require("util");
26
26
  const uuid_1 = require("uuid");
27
+ const base_1 = require("../core/base");
27
28
  const db_1 = __importStar(require("../core/db"));
28
29
  const schema_1 = require("./schema");
29
30
  class BaseField {
@@ -68,21 +69,22 @@ class UUIDField extends BaseField {
68
69
  if (typeof polymorphic === "object" && polymorphic.types) {
69
70
  // an enum with types validated here
70
71
  return {
71
- [name]: EnumType({
72
+ [name]: PolymorphicStringEnumType({
72
73
  values: polymorphic.types,
73
74
  hideFromGraphQL: true,
74
75
  derivedWhenEmbedded: true,
75
76
  nullable: this.options?.nullable,
77
+ parentFieldToValidate: fieldName,
76
78
  }),
77
79
  };
78
80
  }
79
81
  else {
80
- // just a string field...
81
82
  return {
82
- [name]: StringType({
83
+ [name]: PolymorphicStringType({
83
84
  hideFromGraphQL: true,
84
85
  derivedWhenEmbedded: true,
85
86
  nullable: this.options?.nullable,
87
+ parentFieldToValidate: fieldName,
86
88
  }),
87
89
  };
88
90
  }
@@ -311,6 +313,33 @@ class StringField extends BaseField {
311
313
  }
312
314
  }
313
315
  exports.StringField = StringField;
316
+ function validatePolymorphicTypeWithFullData(val, b, field) {
317
+ const input = b.getInput();
318
+ const inputKey = b.orchestrator.__getOptions().fieldInfo[field].inputKey;
319
+ const v = input[inputKey];
320
+ if (val === null) {
321
+ // if this is being set to null, ok if v is also null
322
+ return v === null;
323
+ }
324
+ // if this is not being set, ok if v is not being set
325
+ if (val === undefined && b.operation === base_1.WriteOperation.Insert) {
326
+ return v === undefined;
327
+ }
328
+ return true;
329
+ }
330
+ class PolymorphicStringField extends StringField {
331
+ constructor(opts) {
332
+ super(opts);
333
+ this.opts = opts;
334
+ }
335
+ validateWithFullData(val, b) {
336
+ return validatePolymorphicTypeWithFullData(val, b, this.opts.parentFieldToValidate);
337
+ }
338
+ }
339
+ function PolymorphicStringType(opts) {
340
+ let result = new PolymorphicStringField(opts);
341
+ return Object.assign(result, opts);
342
+ }
314
343
  function StringType(options) {
315
344
  let result = new StringField(options);
316
345
  const options2 = { ...options };
@@ -556,6 +585,19 @@ exports.EnumField = EnumField;
556
585
  class StringEnumField extends EnumField {
557
586
  }
558
587
  exports.StringEnumField = StringEnumField;
588
+ class PolymorphicStringEnumField extends StringEnumField {
589
+ constructor(opts) {
590
+ super(opts);
591
+ this.opts = opts;
592
+ }
593
+ validateWithFullData(val, b) {
594
+ return validatePolymorphicTypeWithFullData(val, b, this.opts.parentFieldToValidate);
595
+ }
596
+ }
597
+ function PolymorphicStringEnumType(options) {
598
+ let result = new PolymorphicStringEnumField(options);
599
+ return Object.assign(result, options);
600
+ }
559
601
  function EnumType(options) {
560
602
  let result = new StringEnumField(options);
561
603
  return Object.assign(result, options);
@@ -217,6 +217,7 @@ export interface PolymorphicOptions {
217
217
  export interface Field extends FieldOptions {
218
218
  type: Type;
219
219
  valid?(val: any): Promise<boolean> | boolean;
220
+ validateWithFullData?(val: any, builder: Builder<any>): boolean | Promise<boolean>;
220
221
  format?(val: any, nested?: boolean): any;
221
222
  logValue(val: any): any;
222
223
  }
@@ -137,7 +137,8 @@ function getFieldInfo(value) {
137
137
  for (const [k, f] of fields) {
138
138
  ret[k] = {
139
139
  dbCol: (0, schema_2.getStorageKey)(f, k),
140
- inputKey: (0, camel_case_1.camelCase)(k),
140
+ // in tests (anything using SimpleBuilder), make it be the same as the fieldName
141
+ inputKey: k,
141
142
  };
142
143
  }
143
144
  return ret;