@snowtop/ent 0.1.12 → 0.1.13

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.
Files changed (44) hide show
  1. package/action/experimental_action.js +2 -2
  2. package/action/operations.js +4 -2
  3. package/core/base.d.ts +12 -10
  4. package/core/clause.d.ts +4 -3
  5. package/core/clause.js +98 -43
  6. package/core/config.d.ts +6 -0
  7. package/core/context.d.ts +6 -14
  8. package/core/context.js +9 -4
  9. package/core/db.js +1 -1
  10. package/core/ent.d.ts +1 -0
  11. package/core/ent.js +30 -11
  12. package/core/loaders/assoc_count_loader.js +1 -1
  13. package/core/loaders/assoc_edge_loader.d.ts +3 -0
  14. package/core/loaders/assoc_edge_loader.js +18 -0
  15. package/core/loaders/object_loader.js +2 -2
  16. package/core/loaders/query_loader.d.ts +3 -3
  17. package/core/loaders/query_loader.js +2 -1
  18. package/core/loaders/raw_count_loader.js +1 -1
  19. package/core/query/assoc_query.d.ts +22 -0
  20. package/core/query/assoc_query.js +101 -2
  21. package/core/query/custom_query.js +2 -9
  22. package/core/query/query.d.ts +1 -0
  23. package/core/query/query.js +72 -11
  24. package/core/query/shared_assoc_test.js +404 -7
  25. package/core/query/shared_test.js +9 -37
  26. package/core/query_impl.d.ts +2 -1
  27. package/core/query_impl.js +25 -7
  28. package/graphql/query/edge_connection.js +2 -2
  29. package/package.json +2 -2
  30. package/parse_schema/parse.d.ts +2 -2
  31. package/parse_schema/parse.js +3 -3
  32. package/schema/struct_field.d.ts +4 -2
  33. package/schema/struct_field.js +33 -4
  34. package/scripts/custom_graphql.js +1 -1
  35. package/testutils/builder.d.ts +1 -1
  36. package/testutils/builder.js +4 -4
  37. package/testutils/ent-graphql-tests/index.js +2 -2
  38. package/testutils/fake_data/fake_contact.js +1 -1
  39. package/testutils/fake_data/fake_event.js +1 -1
  40. package/testutils/fake_data/test_helpers.js +2 -2
  41. package/testutils/fake_data/user_query.js +1 -1
  42. package/testutils/query.d.ts +9 -0
  43. package/testutils/query.js +45 -0
  44. package/testutils/write.js +3 -3
@@ -65,7 +65,7 @@ class AssocEdgeCountLoader {
65
65
  });
66
66
  }
67
67
  const loader = await this.loaderFn();
68
- return await loader.load(id);
68
+ return loader.load(id);
69
69
  }
70
70
  clearAll() {
71
71
  this.loader && this.loader.clearAll();
@@ -2,6 +2,7 @@ import { Context, ID, EdgeQueryableDataOptions, Loader, LoaderFactory } from "..
2
2
  import { AssocEdge, AssocEdgeConstructor } from "../ent";
3
3
  export interface AssocLoader<T extends AssocEdge> extends Loader<ID, T[]> {
4
4
  loadEdgeForID2(id: ID, id2: ID): Promise<T | undefined>;
5
+ loadTwoWay(id: ID): Promise<T[]>;
5
6
  }
6
7
  export declare class AssocEdgeLoader<T extends AssocEdge> implements Loader<ID, T[]> {
7
8
  private edgeType;
@@ -13,6 +14,7 @@ export declare class AssocEdgeLoader<T extends AssocEdge> implements Loader<ID,
13
14
  constructor(edgeType: string, edgeCtr: AssocEdgeConstructor<T>, options: EdgeQueryableDataOptions, context: Context);
14
15
  private getLoader;
15
16
  load(id: ID): Promise<T[]>;
17
+ loadTwoWay(id: ID): Promise<T[]>;
16
18
  loadEdgeForID2(id: ID, id2: ID): Promise<T | undefined>;
17
19
  clearAll(): void;
18
20
  }
@@ -23,6 +25,7 @@ export declare class AssocDirectEdgeLoader<T extends AssocEdge> implements Loade
23
25
  context?: Context<import("../base").Viewer<import("../base").Ent<any> | null, ID | null>> | undefined;
24
26
  constructor(edgeType: string, edgeCtr: AssocEdgeConstructor<T>, options?: Partial<Pick<import("../base").QueryableDataOptions, "clause" | "limit" | "orderby" | "disableTransformations">> | undefined, context?: Context<import("../base").Viewer<import("../base").Ent<any> | null, ID | null>> | undefined);
25
27
  load(id: ID): Promise<T[]>;
28
+ loadTwoWay(id: ID): Promise<T[]>;
26
29
  loadEdgeForID2(id: ID, id2: ID): Promise<T | undefined>;
27
30
  clearAll(): void;
28
31
  }
@@ -113,6 +113,15 @@ class AssocEdgeLoader {
113
113
  const loader = await this.loaderFn();
114
114
  return loader.load(id);
115
115
  }
116
+ async loadTwoWay(id) {
117
+ return (0, ent_1.loadTwoWayEdges)({
118
+ ctr: this.edgeCtr,
119
+ id1: id,
120
+ edgeType: this.edgeType,
121
+ context: this.context,
122
+ queryOptions: this.options,
123
+ });
124
+ }
116
125
  // maybe eventually optimize this
117
126
  async loadEdgeForID2(id, id2) {
118
127
  return (0, ent_1.loadEdgeForID2)({
@@ -145,6 +154,15 @@ class AssocDirectEdgeLoader {
145
154
  ctr: this.edgeCtr,
146
155
  });
147
156
  }
157
+ async loadTwoWay(id) {
158
+ return (0, ent_1.loadTwoWayEdges)({
159
+ ctr: this.edgeCtr,
160
+ id1: id,
161
+ edgeType: this.edgeType,
162
+ context: this.context,
163
+ queryOptions: this.options,
164
+ });
165
+ }
148
166
  async loadEdgeForID2(id, id2) {
149
167
  return (0, ent_1.loadEdgeForID2)({
150
168
  id1: id,
@@ -67,7 +67,7 @@ async function loadRowsForClauseLoader(options, clause) {
67
67
  const rowOptions = {
68
68
  ...options,
69
69
  // @ts-expect-error clause in LoadRowOptions doesn't take templatized version of Clause
70
- clause: (0, clause_1.getCombinedClause)(options, clause),
70
+ clause: (0, clause_1.getCombinedClause)(options, clause, true),
71
71
  };
72
72
  return (await (0, ent_1.loadRows)(rowOptions));
73
73
  }
@@ -75,7 +75,7 @@ async function loadCountForClauseLoader(options, clause) {
75
75
  const rowOptions = {
76
76
  ...options,
77
77
  // @ts-expect-error clause in LoadRowOptions doesn't take templatized version of Clause
78
- clause: (0, clause_1.getCombinedClause)(options, clause),
78
+ clause: (0, clause_1.getCombinedClause)(options, clause, true),
79
79
  };
80
80
  const row = await (0, ent_1.loadRow)({
81
81
  ...rowOptions,
@@ -1,14 +1,14 @@
1
- import { Context, ID, EdgeQueryableDataOptions, Loader, LoaderFactory, Data } from "../base";
1
+ import { Context, EdgeQueryableDataOptions, Loader, LoaderFactory, Data } from "../base";
2
2
  import * as clause from "../clause";
3
3
  import { ObjectLoaderFactory } from "./object_loader";
4
4
  import { OrderBy } from "../query_impl";
5
5
  declare class QueryDirectLoader<K extends any> implements Loader<K, Data[]> {
6
6
  private options;
7
7
  private queryOptions?;
8
- context?: Context<import("../base").Viewer<import("../base").Ent<any> | null, ID | null>> | undefined;
8
+ context?: Context<import("../base").Viewer<import("../base").Ent<any> | null, import("../base").ID | null>> | undefined;
9
9
  private memoizedInitPrime;
10
10
  private primedLoaders;
11
- constructor(options: QueryOptions, queryOptions?: Partial<Pick<import("../base").QueryableDataOptions, "clause" | "limit" | "orderby" | "disableTransformations">> | undefined, context?: Context<import("../base").Viewer<import("../base").Ent<any> | null, ID | null>> | undefined);
11
+ constructor(options: QueryOptions, queryOptions?: Partial<Pick<import("../base").QueryableDataOptions, "clause" | "limit" | "orderby" | "disableTransformations">> | undefined, context?: Context<import("../base").Viewer<import("../base").Ent<any> | null, import("../base").ID | null>> | undefined);
12
12
  private initPrime;
13
13
  load(id: K): Promise<Data[]>;
14
14
  clearAll(): void;
@@ -57,9 +57,10 @@ async function simpleCase(options, id, queryOptions) {
57
57
  throw new Error(`need options.groupCol or options.clause`);
58
58
  }
59
59
  if (queryOptions?.clause) {
60
+ // TODO does this one need a getCombinedClause check??
60
61
  cls = clause.And(cls, queryOptions.clause);
61
62
  }
62
- return await (0, ent_1.loadRows)({
63
+ return (0, ent_1.loadRows)({
63
64
  ...options,
64
65
  clause: cls,
65
66
  orderby: getOrderByLocal(options, queryOptions),
@@ -113,7 +113,7 @@ class RawCountLoader {
113
113
  }
114
114
  async load(id) {
115
115
  if (this.loader) {
116
- return await this.loader.load(id);
116
+ return this.loader.load(id);
117
117
  }
118
118
  const rows = await simpleCase(this.options, id, this.context);
119
119
  return rows[0];
@@ -11,6 +11,7 @@ export declare abstract class AssocEdgeQueryBase<TSource extends Ent<TViewer>, T
11
11
  private countLoaderFactory;
12
12
  private dataLoaderFactory;
13
13
  private options;
14
+ private loadTwoWay;
14
15
  constructor(viewer: TViewer, src: EdgeQuerySource<TSource, TDest, TViewer>, countLoaderFactory: AssocEdgeCountLoaderFactory, dataLoaderFactory: AssocEdgeLoaderFactory<TEdge>, options: LoadEntOptions<TDest, TViewer> | loaderOptionsFunc<TViewer>);
15
16
  private isEdgeQuery;
16
17
  abstract sourceEnt(id: ID): Promise<Ent | null>;
@@ -27,6 +28,27 @@ export declare abstract class AssocEdgeQueryBase<TSource extends Ent<TViewer>, T
27
28
  __beforeBETA(time: Date): this;
28
29
  __afterBETA(time: Date): this;
29
30
  __withinBeta(start: Date, end: Date): this;
31
+ /**
32
+ * intersect multiple queries together with this one to get candidate edges
33
+ * the edges returned will always be from the originating edge query
34
+ *
35
+ * @param others list of other queries to intersect with the source edge
36
+ */
37
+ __intersect(...others: AssocEdgeQueryBase<any, TDest, TEdge, TViewer>[]): this;
38
+ /**
39
+ * union multiple queries together with this one to get candidate edges
40
+ * if the edge exists in the source query, that's the edge returned
41
+ * if the edge doesn't exist, the first edge in the list of queries that has the edge is returned
42
+ * @param others list of other queries to union with the source edge
43
+ */
44
+ __union<TSource2 extends Ent<TViewer>>(...others: AssocEdgeQueryBase<TSource2, TDest, TEdge, TViewer>[]): this;
45
+ /**
46
+ * this fetches edges where there's a two way connection between both sets of edges
47
+ * e.g. in a social networking system, where the source and dest are both following each other
48
+ *
49
+ * will not work in the future when there's sharding...
50
+ */
51
+ __twoWay(): this;
30
52
  }
31
53
  export interface EdgeQueryCtr<TSource extends Ent, TDest extends Ent, TEdge extends AssocEdge> {
32
54
  new (viewer: Viewer, src: EdgeQuerySource<TSource>): EdgeQuery<TSource, TDest, TEdge>;
@@ -39,6 +39,7 @@ class AssocEdgeQueryBase extends query_1.BaseEdgeQuery {
39
39
  this.countLoaderFactory = countLoaderFactory;
40
40
  this.dataLoaderFactory = dataLoaderFactory;
41
41
  this.options = options;
42
+ this.loadTwoWay = false;
42
43
  }
43
44
  isEdgeQuery(obj) {
44
45
  if (obj.queryIDs !== undefined) {
@@ -147,7 +148,9 @@ class AssocEdgeQueryBase extends query_1.BaseEdgeQuery {
147
148
  // doesn't make sense
148
149
  // so only makes sense if one of these...
149
150
  // Id2 needs to be an option
150
- const edges = await loader.load(info.id);
151
+ const edges = this.loadTwoWay
152
+ ? await loader.loadTwoWay(info.id)
153
+ : await loader.load(info.id);
151
154
  this.edges.set(info.id, edges);
152
155
  }));
153
156
  }
@@ -186,10 +189,47 @@ class AssocEdgeQueryBase extends query_1.BaseEdgeQuery {
186
189
  }
187
190
  // start is inclusive, end is exclusive
188
191
  __withinBeta(start, end) {
189
- this.__assertNoFiltersBETA("after");
192
+ this.__assertNoFiltersBETA("within");
190
193
  this.__addCustomFilterBETA(new WithinFilter(start, end));
191
194
  return this;
192
195
  }
196
+ /**
197
+ * intersect multiple queries together with this one to get candidate edges
198
+ * the edges returned will always be from the originating edge query
199
+ *
200
+ * @param others list of other queries to intersect with the source edge
201
+ */
202
+ __intersect(...others) {
203
+ // TODO I don't really see a reason why we can't chain first or something first before this
204
+ // but for now let's not support it
205
+ // when we do this correctly, we'll allow chaining
206
+ this.__assertNoFiltersBETA("intersect");
207
+ this.__addCustomFilterBETA(new IntersectFilter(others));
208
+ return this;
209
+ }
210
+ /**
211
+ * union multiple queries together with this one to get candidate edges
212
+ * if the edge exists in the source query, that's the edge returned
213
+ * if the edge doesn't exist, the first edge in the list of queries that has the edge is returned
214
+ * @param others list of other queries to union with the source edge
215
+ */
216
+ __union(...others) {
217
+ // same chain comment from intersect...
218
+ this.__assertNoFiltersBETA("union");
219
+ this.__addCustomFilterBETA(new UnionFilter(others));
220
+ return this;
221
+ }
222
+ /**
223
+ * this fetches edges where there's a two way connection between both sets of edges
224
+ * e.g. in a social networking system, where the source and dest are both following each other
225
+ *
226
+ * will not work in the future when there's sharding...
227
+ */
228
+ __twoWay() {
229
+ this.__assertNoFiltersBETA("twoWay");
230
+ this.loadTwoWay = true;
231
+ return this;
232
+ }
193
233
  }
194
234
  exports.AssocEdgeQueryBase = AssocEdgeQueryBase;
195
235
  class BeforeFilter {
@@ -223,3 +263,62 @@ class WithinFilter {
223
263
  return options;
224
264
  }
225
265
  }
266
+ class IntersectFilter {
267
+ constructor(queries) {
268
+ this.queries = queries;
269
+ this.edges = [];
270
+ }
271
+ async fetch() {
272
+ let i = 0;
273
+ // maybe future optimization. instead of this, use a SQL query if edge_types are the same
274
+ for await (const query of this.queries) {
275
+ const edges = await query.queryEdges();
276
+ const set = new Set();
277
+ edges.forEach((edge) => {
278
+ set.add(edge.id2);
279
+ });
280
+ this.edges[i] = set;
281
+ i++;
282
+ }
283
+ }
284
+ filter(_id, edges) {
285
+ return edges.filter((edge) => {
286
+ return this.edges.every((set) => {
287
+ return set.has(edge.id2);
288
+ });
289
+ });
290
+ }
291
+ }
292
+ class UnionFilter {
293
+ constructor(queries) {
294
+ this.queries = queries;
295
+ this.edges = [];
296
+ }
297
+ async fetch() {
298
+ let i = 0;
299
+ // maybe future optimization. instead of this, use a SQL query if edge_types are the same
300
+ for await (const query of this.queries) {
301
+ const edges = await query.queryEdges();
302
+ this.edges[i] = edges;
303
+ i++;
304
+ }
305
+ }
306
+ filter(_id, edges) {
307
+ const set = new Set();
308
+ const result = [];
309
+ for (const edge of edges) {
310
+ set.add(edge.id2);
311
+ result.push(edge);
312
+ }
313
+ for (const edges of this.edges) {
314
+ for (const edge of edges) {
315
+ if (set.has(edge.id2)) {
316
+ continue;
317
+ }
318
+ result.push(edge);
319
+ set.add(edge.id2);
320
+ }
321
+ }
322
+ return result;
323
+ }
324
+ }
@@ -7,17 +7,10 @@ const loaders_1 = require("../loaders");
7
7
  const query_1 = require("./query");
8
8
  function getClause(opts) {
9
9
  let cls = opts.clause;
10
- if (opts.disableTransformations) {
10
+ if (opts.disableTransformations || !cls) {
11
11
  return cls;
12
12
  }
13
- let optClause = opts.loadEntOptions.loaderFactory?.options?.clause;
14
- if (typeof optClause === "function") {
15
- optClause = optClause();
16
- }
17
- if (!optClause) {
18
- return cls;
19
- }
20
- return (0, clause_1.AndOptional)(cls, optClause);
13
+ return (0, clause_1.getCombinedClause)(opts.loadEntOptions.loaderFactory?.options, cls, true);
21
14
  }
22
15
  function getRawCountLoader(viewer, opts) {
23
16
  if (!viewer.context?.cache) {
@@ -20,6 +20,7 @@ export interface EdgeQuery<TSource extends Ent, TDest extends Ent, TEdge extends
20
20
  sourceEnt(id: ID): Promise<Ent | null>;
21
21
  }
22
22
  export interface EdgeQueryFilter<T extends Data> {
23
+ fetch?(): Promise<void>;
23
24
  filter?(id: ID, edges: T[]): T[];
24
25
  query?(options: EdgeQueryableDataOptions): EdgeQueryableDataOptions | Promise<EdgeQueryableDataOptions>;
25
26
  paginationInfo?(id: ID): PaginationInfo | undefined;
@@ -65,6 +65,7 @@ class FirstFilter {
65
65
  constructor(options) {
66
66
  this.options = options;
67
67
  this.pageMap = new Map();
68
+ this.usedQuery = false;
68
69
  assertPositive(options.limit);
69
70
  this.sortCol = options.sortCol;
70
71
  if (options.after) {
@@ -72,18 +73,58 @@ class FirstFilter {
72
73
  }
73
74
  this.edgeQuery = options.query;
74
75
  }
76
+ setPageMap(ret, id, hasNextPage) {
77
+ this.pageMap.set(id, {
78
+ hasNextPage,
79
+ // hasPreviousPage always false even if there's a previous page because
80
+ // we shouldn't be querying in both directions at the same
81
+ hasPreviousPage: false,
82
+ startCursor: this.edgeQuery.getCursor(ret[0]),
83
+ endCursor: this.edgeQuery.getCursor(ret[ret.length - 1]),
84
+ });
85
+ }
75
86
  filter(id, edges) {
76
87
  if (edges.length > this.options.limit) {
77
- const ret = edges.slice(0, this.options.limit);
78
- this.pageMap.set(id, {
79
- hasNextPage: true,
80
- // hasPreviousPage always false even if there's a previous page because
81
- // we shouldn't be querying in both directions at the same
82
- hasPreviousPage: false,
83
- startCursor: this.edgeQuery.getCursor(ret[0]),
84
- endCursor: this.edgeQuery.getCursor(ret[ret.length - 1]),
85
- });
86
- return ret;
88
+ // we need a way to know where the cursor is and if we used it and if not need to do this in TypeScript and not in SQL
89
+ // so can't filter from 0 but specific item
90
+ // if we used the query or we're querying the first N, slice from 0
91
+ if (this.usedQuery || !this.offset) {
92
+ const ret = edges.slice(0, this.options.limit);
93
+ this.setPageMap(ret, id, true);
94
+ return ret;
95
+ }
96
+ else if (this.offset) {
97
+ const ret = [];
98
+ let found = false;
99
+ let i = 0;
100
+ let hasNextPage = false;
101
+ for (const edge of edges) {
102
+ const id = edge[this.options.cursorCol];
103
+ if (id === this.offset) {
104
+ // found!
105
+ found = true;
106
+ hasNextPage = true;
107
+ continue;
108
+ }
109
+ if (found) {
110
+ ret.push(edge);
111
+ if (ret.length === this.options.limit) {
112
+ if (i === ret.length - 1) {
113
+ hasNextPage = false;
114
+ }
115
+ break;
116
+ }
117
+ }
118
+ i++;
119
+ }
120
+ if (hasNextPage && ret.length < this.options.limit) {
121
+ hasNextPage = false;
122
+ }
123
+ if (ret.length) {
124
+ this.setPageMap(ret, id, hasNextPage);
125
+ }
126
+ return ret;
127
+ }
87
128
  }
88
129
  // TODO: in the future, when we have caching for edges
89
130
  // we'll want to hit that cache instead of passing the limit down to the
@@ -93,6 +134,7 @@ class FirstFilter {
93
134
  return edges;
94
135
  }
95
136
  async query(options) {
137
+ this.usedQuery = true;
96
138
  // we fetch an extra one to see if we're at the end
97
139
  const limit = this.options.limit + 1;
98
140
  options.limit = limit;
@@ -212,7 +254,7 @@ class BaseEdgeQuery {
212
254
  this.limitAdded = false;
213
255
  // this is basically just raw rows
214
256
  this.queryEdges = async () => {
215
- return await this.querySingleEdge("queryEdges");
257
+ return this.querySingleEdge("queryEdges");
216
258
  };
217
259
  this.queryAllEdges = async () => {
218
260
  return this.memoizedloadEdges();
@@ -402,15 +444,34 @@ class BaseEdgeQuery {
402
444
  // may need to bring sql mode or something back
403
445
  for (const filter of this.filters) {
404
446
  if (filter.query) {
447
+ if (filter.fetch) {
448
+ throw new Error(`a filter that augments the query cannot currently fetch`);
449
+ }
405
450
  let res = filter.query(options);
406
451
  options = (0, types_1.isPromise)(res) ? await res : res;
407
452
  }
453
+ else {
454
+ // if we've seen a filter that doesn't have a query, we can't do anything in SQL
455
+ // TODO figure out filter interactions https://github.com/lolopinto/ent/issues/685
456
+ // this is a scenario where if we have the first N filters that can modify the query,
457
+ // we do that in SQL and then we do the rest in code
458
+ // but once we have something doing it in code (e.g. intersect()), we can't do anything else in SQL
459
+ // and then have to differentiate between filters that augment (limit or add to) the items returned
460
+ // and those that reorder...
461
+ break;
462
+ }
408
463
  }
409
464
  await this.loadRawData(idsInfo, options);
410
465
  // no filters. nothing to do here.
411
466
  if (!this.filters.length) {
412
467
  return this.edges;
413
468
  }
469
+ // fetch anything we need to filter the query
470
+ for await (const filter of this.filters) {
471
+ if (filter.fetch) {
472
+ await filter.fetch();
473
+ }
474
+ }
414
475
  // filter as needed
415
476
  for (let [id, edges] of this.edges) {
416
477
  this.filters.forEach((filter) => {