@snowtop/ent 0.2.12 → 0.2.14

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.
@@ -17,7 +17,7 @@ export interface OrchestratorOptions<TEnt extends Ent<TViewer>, TInput extends D
17
17
  action?: Action<TEnt, Builder<TEnt, TViewer>, TViewer, TInput>;
18
18
  schema: SchemaInputType;
19
19
  editedFields(): Map<string, any> | Promise<Map<string, any>>;
20
- updateInput?: (data: TInput) => void;
20
+ updateInput?: (data: TInput, operation?: WriteOperation) => void;
21
21
  expressions?: Map<string, clause.Clause>;
22
22
  fieldInfo: FieldInfoMap;
23
23
  }
@@ -38,6 +38,8 @@ export declare class Orchestrator<TEnt extends Ent<TViewer>, TInput extends Data
38
38
  private options;
39
39
  private edgeSet;
40
40
  private edges;
41
+ private fieldEdgeSources;
42
+ private fieldEdgeInputs;
41
43
  private conditionalEdges;
42
44
  private validatedFields;
43
45
  private logValues;
@@ -59,6 +61,9 @@ export declare class Orchestrator<TEnt extends Ent<TViewer>, TInput extends Data
59
61
  setDisableTransformations(val: boolean): void;
60
62
  setOnConflictOptions(onConflict: CreateRowOptions["onConflict"]): void;
61
63
  addInboundEdge<T2 extends Ent>(id1: ID | Builder<T2, any>, edgeType: string, nodeType: string, options?: AssocEdgeInputOptions): void;
64
+ __setFieldEdges<T2 extends Ent>(fieldName: string, ids: readonly (ID | Builder<T2, any>)[] | undefined, edgeType: string, nodeType: string, stored: {
65
+ existingIDs?: readonly ID[];
66
+ }): void;
62
67
  addOutboundEdge<T2 extends Ent>(id2: ID | Builder<T2, any>, edgeType: string, nodeType: string, options?: AssocEdgeInputOptions): void;
63
68
  removeInboundEdge(id1: ID, edgeType: string, options?: AssocEdgeOptions): void;
64
69
  removeOutboundEdge(id2: ID, edgeType: string, options?: AssocEdgeOptions): void;
@@ -39,12 +39,25 @@ const schema_1 = require("../schema/schema");
39
39
  const operations_1 = require("./operations");
40
40
  const action_1 = require("../action");
41
41
  const privacy_1 = require("../core/privacy");
42
+ const privacy_2 = require("./privacy");
42
43
  const executor_1 = require("./executor");
43
44
  const memoize_1 = require("../core/memoize");
44
45
  const logger_1 = require("../core/logger");
45
46
  const clause = __importStar(require("../core/clause"));
46
47
  const types_1 = require("util/types");
47
48
  const operations_2 = require("./operations");
49
+ function invalidFieldError(fieldName, value) {
50
+ let description;
51
+ try {
52
+ description = `${value}`;
53
+ }
54
+ catch {
55
+ // Null-prototype records (and lists containing them) cannot be coerced
56
+ // to strings. Diagnostic formatting must not hide a validation failure.
57
+ description = "[unprintable value]";
58
+ }
59
+ return new Error(`invalid field ${fieldName} with value ${description}`);
60
+ }
48
61
  var edgeDirection;
49
62
  (function (edgeDirection) {
50
63
  edgeDirection[edgeDirection["inboundEdge"] = 0] = "inboundEdge";
@@ -99,6 +112,8 @@ class Orchestrator {
99
112
  this.options = options;
100
113
  this.edgeSet = new Set();
101
114
  this.edges = new Map();
115
+ this.fieldEdgeSources = new WeakMap();
116
+ this.fieldEdgeInputs = new Map();
102
117
  this.conditionalEdges = new Map();
103
118
  this.validatedFields = null;
104
119
  this.changesets = [];
@@ -155,6 +170,105 @@ class Orchestrator {
155
170
  direction: edgeDirection.inboundEdge,
156
171
  }), action_1.WriteOperation.Insert, options?.conditional);
157
172
  }
173
+ // Update inverse edges for generated fields while preserving explicit edge
174
+ // operations. This method is internal. For edits and deletions, use stored IDs.
175
+ // If `stored` omits `existingIDs`, reuse the IDs captured before synchronous
176
+ // default updates.
177
+ __setFieldEdges(fieldName, ids, edgeType, nodeType, stored) {
178
+ // Existing builders and literal IDs can refer to the same database row.
179
+ // Use placeholder IDs as queue keys so unsaved builders remain dependencies.
180
+ const endpointID = (id) => (0, privacy_2.isBuilder)(id) ? (id.existingEnt?.id ?? id.placeholderID) : id;
181
+ this.fieldEdgeInputs.set(fieldName, {
182
+ edgeType,
183
+ nodeType,
184
+ ids,
185
+ existingIDs: stored.existingIDs ??
186
+ this.fieldEdgeInputs.get(fieldName)?.existingIDs ??
187
+ [],
188
+ });
189
+ const inserts = new Map();
190
+ const removals = new Map();
191
+ const retained = new Set();
192
+ const contribute = (map, id, source) => {
193
+ const key = (0, privacy_2.isBuilder)(id) ? id.placeholderID : id;
194
+ let entry = map.get(key);
195
+ if (!entry) {
196
+ map.set(key, (entry = { id, sources: new Set() }));
197
+ }
198
+ entry.sources.add(source);
199
+ };
200
+ for (const [source, field] of this.fieldEdgeInputs) {
201
+ if (field.edgeType !== edgeType) {
202
+ continue;
203
+ }
204
+ const existing = this.actualOperation === action_1.WriteOperation.Insert ? [] : field.existingIDs;
205
+ const current = this.actualOperation === action_1.WriteOperation.Delete ? [] : field.ids;
206
+ for (const id of current ?? existing) {
207
+ retained.add(endpointID(id));
208
+ }
209
+ if (current !== undefined) {
210
+ for (const id of current) {
211
+ contribute(inserts, id, source);
212
+ }
213
+ for (const id of existing) {
214
+ contribute(removals, id, source);
215
+ }
216
+ }
217
+ }
218
+ const manualEndpoints = (op) => {
219
+ const endpoints = new Set();
220
+ for (const edge of this.edges.get(edgeType)?.get(op)?.values() ?? []) {
221
+ if (!this.fieldEdgeSources.has(edge)) {
222
+ endpoints.add(endpointID(edge.id));
223
+ }
224
+ }
225
+ return endpoints;
226
+ };
227
+ const manualInserts = manualEndpoints(action_1.WriteOperation.Insert);
228
+ const manualRemovals = manualEndpoints(action_1.WriteOperation.Delete);
229
+ for (const id of removals.keys()) {
230
+ if (retained.has(id) || manualInserts.has(id)) {
231
+ removals.delete(id);
232
+ }
233
+ }
234
+ for (const [key, contribution] of inserts) {
235
+ const id = endpointID(contribution.id);
236
+ if (manualInserts.has(id) || manualRemovals.has(id)) {
237
+ inserts.delete(key);
238
+ }
239
+ }
240
+ for (const [op, desired] of [
241
+ [action_1.WriteOperation.Insert, inserts],
242
+ [action_1.WriteOperation.Delete, removals],
243
+ ]) {
244
+ const queued = this.edges.get(edgeType)?.get(op);
245
+ for (const [id, edge] of queued ?? []) {
246
+ if (!this.fieldEdgeSources.has(edge)) {
247
+ continue;
248
+ }
249
+ const contribution = desired.get(id);
250
+ if (contribution) {
251
+ this.fieldEdgeSources.set(edge, contribution.sources);
252
+ }
253
+ else {
254
+ queued.delete(id);
255
+ }
256
+ }
257
+ for (const [key, contribution] of desired) {
258
+ if (this.edges.get(edgeType)?.get(op)?.has(key)) {
259
+ continue;
260
+ }
261
+ const edge = new edgeInputData({
262
+ id: contribution.id,
263
+ edgeType,
264
+ nodeType,
265
+ direction: edgeDirection.inboundEdge,
266
+ });
267
+ this.fieldEdgeSources.set(edge, contribution.sources);
268
+ this.addEdge(edge, op);
269
+ }
270
+ }
271
+ }
158
272
  addOutboundEdge(id2, edgeType, nodeType, options) {
159
273
  this.addEdge(new edgeInputData({
160
274
  id: id2,
@@ -532,27 +646,22 @@ class Orchestrator {
532
646
  }
533
647
  async triggers(action, builder, triggers) {
534
648
  const groups = [];
535
- let lastArray = 0;
536
- let prevWasArray = false;
537
- for (let i = 0; i < triggers.length; i++) {
538
- let t = triggers[i];
539
- if (Array.isArray(t)) {
540
- if (!prevWasArray) {
541
- // @ts-ignore
542
- groups.push(triggers.slice(lastArray, i));
649
+ let group = [];
650
+ for (const trigger of triggers) {
651
+ if (Array.isArray(trigger)) {
652
+ if (group.length) {
653
+ groups.push(group);
654
+ group = [];
543
655
  }
544
- groups.push(t);
545
- prevWasArray = true;
546
- lastArray++;
656
+ groups.push(trigger);
547
657
  }
548
658
  else {
549
- if (i === triggers.length - 1) {
550
- // @ts-ignore
551
- groups.push(triggers.slice(lastArray, i + 1));
552
- }
553
- prevWasArray = false;
659
+ group.push(trigger);
554
660
  }
555
661
  }
662
+ if (group.length) {
663
+ groups.push(group);
664
+ }
556
665
  for (const triggers of groups) {
557
666
  await Promise.all(triggers.map(async (trigger) => {
558
667
  let ret = await trigger.changeset(builder, action.getInput());
@@ -606,6 +715,8 @@ class Orchestrator {
606
715
  // if disable transformations set, don't do schema transform and just do the right thing
607
716
  // else apply schema tranformation if it exists
608
717
  let transformed = null;
718
+ const initialOperation = this.actualOperation;
719
+ const initialEnt = this.existingEnt;
609
720
  const sqlOp = this.getSQLStatementOperation();
610
721
  // why is transform write technically different from upsert?
611
722
  // it's create -> update just at the db level...
@@ -668,6 +779,13 @@ class Orchestrator {
668
779
  builder.existingEnt = transformed.existingEnt;
669
780
  }
670
781
  }
782
+ if (this.fieldEdgeInputs.size > 0 &&
783
+ (initialOperation !== this.actualOperation ||
784
+ initialEnt !== this.existingEnt)) {
785
+ // Refresh inverse edges for the transformed operation and row before
786
+ // applying defaults or running triggers.
787
+ editedFields = await this.options.editedFields();
788
+ }
671
789
  // transforming before doing default fields so that we don't create a new id
672
790
  // and anything that depends on the type of operations knows what it is
673
791
  const userDefinedKeys = new Set();
@@ -728,7 +846,7 @@ class Orchestrator {
728
846
  };
729
847
  if (updateInput && this.options.updateInput) {
730
848
  // this basically fixes #605. just needs to be exposed correctly
731
- this.options.updateInput(this.defaultFieldsByTSName);
849
+ this.options.updateInput(this.defaultFieldsByTSName, this.actualOperation);
732
850
  }
733
851
  }
734
852
  return { data, userDefinedKeys };
@@ -764,7 +882,7 @@ class Orchestrator {
764
882
  valid = await valid;
765
883
  }
766
884
  if (!valid) {
767
- return new Error(`invalid field ${fieldName} with value ${value}`);
885
+ return invalidFieldError(fieldName, value);
768
886
  }
769
887
  }
770
888
  // keep track of dependencies to resolve
@@ -779,7 +897,7 @@ class Orchestrator {
779
897
  valid = await valid;
780
898
  }
781
899
  if (!valid) {
782
- return new Error(`invalid field ${fieldName} with value ${value}`);
900
+ return invalidFieldError(fieldName, value);
783
901
  }
784
902
  }
785
903
  if (field.format) {
@@ -855,6 +973,20 @@ class Orchestrator {
855
973
  }
856
974
  }
857
975
  }
976
+ // If a trigger clears an input, the SQL write can still use its computed
977
+ // default. Update inverse edges for defaults included in `data`; apply edit
978
+ // defaults only when the edit has data to save.
979
+ for (const [fieldName, field] of this.fieldEdgeInputs) {
980
+ if (field.ids !== undefined ||
981
+ data[this.getStorageKey(fieldName)] === undefined) {
982
+ continue;
983
+ }
984
+ const value = this.defaultFieldsByFieldName[fieldName];
985
+ if (value === undefined) {
986
+ continue;
987
+ }
988
+ this.__setFieldEdges(fieldName, value === null ? [] : Array.isArray(value) ? value : [value], field.edgeType, field.nodeType, {});
989
+ }
858
990
  this.validatedFields = data;
859
991
  this.logValues = logValues;
860
992
  return errors;
@@ -1,5 +1,6 @@
1
1
  import { Builder } from "./action";
2
2
  import { Viewer, ID, Ent, PrivacyResult, PrivacyPolicyRule } from "../core/base";
3
+ export declare function isBuilder(node: ID | Builder<Ent, any>): node is Builder<Ent, any>;
3
4
  export declare class DenyIfBuilder implements PrivacyPolicyRule {
4
5
  private id?;
5
6
  constructor(id?: (ID | Builder<Ent, any>) | undefined);
package/action/privacy.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AllowIfBuilder = exports.DenyIfBuilder = void 0;
4
+ exports.isBuilder = isBuilder;
4
5
  const base_1 = require("../core/base");
5
6
  function isBuilder(node) {
6
7
  return node.placeholderID !== undefined;
package/core/clause.js CHANGED
@@ -185,7 +185,7 @@ class queryClause {
185
185
  this.prefix = prefix;
186
186
  }
187
187
  clause(idx, alias) {
188
- const q = (0, query_impl_1.buildQuery)(this.dependentQueryOptions);
188
+ const q = (0, query_impl_1.buildQuery)(this.dependentQueryOptions, idx);
189
189
  return `${this.prefix} (${q})`;
190
190
  }
191
191
  columns() {
@@ -28,6 +28,6 @@ export declare function reverseOrderBy(orderby: OrderBy): OrderBy;
28
28
  interface JoinInfo extends QueryFragmentInfo {
29
29
  }
30
30
  export declare function getJoinInfo(join: NonNullable<QueryableDataOptions["join"]>, clauseIdx?: number): JoinInfo;
31
- export declare function buildQueryData(options: QueryableDataOptions): BuiltQueryData;
32
- export declare function buildQuery(options: QueryableDataOptions): string;
31
+ export declare function buildQueryData(options: QueryableDataOptions, startIdx?: number): BuiltQueryData;
32
+ export declare function buildQuery(options: QueryableDataOptions, startIdx?: number): string;
33
33
  export {};
@@ -171,12 +171,14 @@ function getJoinInfo(join, clauseIdx = 1) {
171
171
  logValues,
172
172
  };
173
173
  }
174
- function buildQueryData(options) {
174
+ // startIdx is the first placeholder number when embedding this query in another.
175
+ // The returned values contain only this query's parameters.
176
+ function buildQueryData(options, startIdx = 1) {
175
177
  const fieldsAlias = options.fieldsAlias ?? options.alias;
176
- const fieldInfo = getFieldsInfo(options.fields, fieldsAlias, options.disableFieldsAlias, 1);
178
+ const fieldInfo = getFieldsInfo(options.fields, fieldsAlias, options.disableFieldsAlias, startIdx);
177
179
  const values = [...fieldInfo.values];
178
180
  const logValues = [...fieldInfo.logValues];
179
- let clauseIdx = 1 + fieldInfo.valuesUsed;
181
+ let clauseIdx = startIdx + fieldInfo.valuesUsed;
180
182
  const parts = [];
181
183
  const tableName = options.alias
182
184
  ? `${options.tableName} AS ${options.alias}`
@@ -220,6 +222,6 @@ function buildQueryData(options) {
220
222
  logValues,
221
223
  };
222
224
  }
223
- function buildQuery(options) {
224
- return buildQueryData(options).query;
225
+ function buildQuery(options, startIdx = 1) {
226
+ return buildQueryData(options, startIdx).query;
225
227
  }
@@ -631,11 +631,13 @@ exports.gqlQuery = GQLCapture.gqlQuery;
631
631
  exports.gqlMutation = GQLCapture.gqlMutation;
632
632
  exports.gqlContextType = GQLCapture.gqlContextType;
633
633
  exports.gqlConnection = GQLCapture.gqlConnection;
634
- // this requires the developer to npm-install "graphql-upload on their own"
634
+ // Apps that use uploads must install graphql-upload on their own. Keep the
635
+ // runtime import behind @snowtop/ent/graphql/upload so normal GraphQL users do
636
+ // not load the optional peer dependency.
635
637
  const gqlFileUpload = {
636
638
  type: "GraphQLUpload",
637
- importPath: "graphql-upload",
639
+ importPath: "@snowtop/ent/graphql/upload",
638
640
  tsType: "FileUpload",
639
- tsImportPath: "graphql-upload",
641
+ tsImportPath: "@snowtop/ent/graphql/upload",
640
642
  };
641
643
  exports.gqlFileUpload = gqlFileUpload;
@@ -0,0 +1,16 @@
1
+ import type { Readable } from "stream";
2
+ import type { GraphQLScalarType } from "graphql";
3
+ export interface FileUpload {
4
+ filename: string;
5
+ mimetype: string;
6
+ encoding: string;
7
+ createReadStream(): Readable;
8
+ }
9
+ export interface GraphQLUploadExpressOptions {
10
+ maxFieldSize?: number;
11
+ maxFileSize?: number;
12
+ maxFiles?: number;
13
+ [key: string]: unknown;
14
+ }
15
+ export declare const GraphQLUpload: GraphQLScalarType;
16
+ export declare const graphqlUploadExpress: (options?: GraphQLUploadExpressOptions) => any;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.graphqlUploadExpress = exports.GraphQLUpload = void 0;
4
+ const GraphQLUploadImport = require("graphql-upload/GraphQLUpload.js");
5
+ const graphqlUploadExpressImport = require("graphql-upload/graphqlUploadExpress.js");
6
+ exports.GraphQLUpload = GraphQLUploadImport;
7
+ exports.graphqlUploadExpress = graphqlUploadExpressImport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowtop/ent",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "snowtop ent framework",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -18,14 +18,18 @@
18
18
  "tsconfig-paths": "4.2.0",
19
19
  "tslib": "2.8.1",
20
20
  "typescript": "5.9.3",
21
- "uuid": "9.0.1"
21
+ "uuid": "11.1.1"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "@swc-node/register": "1.6.8",
25
25
  "better-sqlite3": "12.5.0",
26
- "graphql": "16.12.0"
26
+ "graphql": "16.12.0",
27
+ "graphql-upload": "15.0.2"
27
28
  },
28
29
  "peerDependenciesMeta": {
30
+ "graphql-upload": {
31
+ "optional": true
32
+ },
29
33
  "better-sqlite3": {
30
34
  "optional": true
31
35
  },
@@ -35,7 +35,7 @@ class StructField extends field_1.BaseField {
35
35
  }
36
36
  }
37
37
  formatImpl(obj, nested) {
38
- if (!(obj instanceof Object)) {
38
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
39
39
  throw new Error("valid was not called");
40
40
  }
41
41
  let ret = {};
@@ -122,7 +122,12 @@ class StructField extends field_1.BaseField {
122
122
  return this.formatImpl(obj, nested);
123
123
  }
124
124
  async validImpl(obj) {
125
- if (!(obj instanceof Object)) {
125
+ // GraphQL.js valueFromAST turns inline objects such as prefs: {enabled: true}
126
+ // into null-prototype records, which fail instanceof Object. Objects supplied
127
+ // in the JSON variables payload use ordinary prototypes instead. A struct
128
+ // can still be inline when other values use variables; object-valued variable
129
+ // defaults also go through valueFromAST when the variable is omitted.
130
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
126
131
  return false;
127
132
  }
128
133
  let promises = [];
@@ -241,9 +246,6 @@ class StructField extends field_1.BaseField {
241
246
  }));
242
247
  return valid.every((b) => b);
243
248
  }
244
- if (!(obj instanceof Object)) {
245
- return false;
246
- }
247
249
  return this.validImpl(obj);
248
250
  }
249
251
  }
@@ -25,7 +25,7 @@ class UnionField extends field_1.BaseField {
25
25
  }
26
26
  }
27
27
  format(obj) {
28
- if (!(obj instanceof Object)) {
28
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
29
29
  throw new Error("valid was not called");
30
30
  }
31
31
  const k = obj[KEY];
@@ -46,7 +46,8 @@ class UnionField extends field_1.BaseField {
46
46
  };
47
47
  }
48
48
  async valid(obj) {
49
- if (!(obj instanceof Object)) {
49
+ // Accept the same record shapes as the member struct fields.
50
+ if (typeof obj !== "object" || obj === null || Array.isArray(obj)) {
50
51
  return false;
51
52
  }
52
53
  let promises = [];
@@ -533,7 +533,10 @@ async function main() {
533
533
  });
534
534
  }
535
535
  main()
536
- .then()
536
+ // Imported application modules can leave database pools or other handles open.
537
+ // The awaited stdout write above is the script's final required work, so exit
538
+ // explicitly instead of waiting for the event loop to drain.
539
+ .then(() => (0, process_1.exit)(0))
537
540
  .catch((err) => {
538
541
  console.error(err);
539
542
  (0, process_1.exit)(1);
@@ -627,13 +627,14 @@ function setupSqlite(connString, tables, opts) {
627
627
  const client = await db_1.default.getInstance().getNewClient();
628
628
  for (const [key, _] of tdb.__getTables()) {
629
629
  const query = `delete from ${key}`;
630
- if (isSyncClient(client))
630
+ if (isSyncClient(client)) {
631
631
  if (client.execSync) {
632
632
  client.execSync(query);
633
633
  }
634
634
  else {
635
635
  await client.exec(query);
636
636
  }
637
+ }
637
638
  }
638
639
  });
639
640
  }