@snowtop/ent 0.2.13 → 0.2.15

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, defaultKeys?: ReadonlySet<string>) => 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());
@@ -599,6 +708,8 @@ class Orchestrator {
599
708
  async getFieldsWithDefaultValues(builder, schemaFields, editedFields, action) {
600
709
  let data = {};
601
710
  let defaultData = {};
711
+ const defaultKeys = new Set();
712
+ const transformedFields = new Set();
602
713
  let input = action?.getInput() || {};
603
714
  let updateInput = false;
604
715
  // transformations
@@ -606,6 +717,8 @@ class Orchestrator {
606
717
  // if disable transformations set, don't do schema transform and just do the right thing
607
718
  // else apply schema tranformation if it exists
608
719
  let transformed = null;
720
+ const initialOperation = this.actualOperation;
721
+ const initialEnt = this.existingEnt;
609
722
  const sqlOp = this.getSQLStatementOperation();
610
723
  // why is transform write technically different from upsert?
611
724
  // it's create -> update just at the db level...
@@ -649,6 +762,7 @@ class Orchestrator {
649
762
  }
650
763
  data[this.getStorageKey(k)] = dbVal;
651
764
  if (!field.immutable) {
765
+ transformedFields.add(k);
652
766
  this.defaultFieldsByTSName[this.getInputKey(k)] = inputVal;
653
767
  }
654
768
  // hmm do we need this?
@@ -668,6 +782,13 @@ class Orchestrator {
668
782
  builder.existingEnt = transformed.existingEnt;
669
783
  }
670
784
  }
785
+ if (this.fieldEdgeInputs.size > 0 &&
786
+ (initialOperation !== this.actualOperation ||
787
+ initialEnt !== this.existingEnt)) {
788
+ // Refresh inverse edges for the transformed operation and row before
789
+ // applying defaults or running triggers.
790
+ editedFields = await this.options.editedFields();
791
+ }
671
792
  // transforming before doing default fields so that we don't create a new id
672
793
  // and anything that depends on the type of operations knows what it is
673
794
  const userDefinedKeys = new Set();
@@ -679,7 +800,7 @@ class Orchestrator {
679
800
  if (value !== undefined) {
680
801
  userDefinedKeys.add(dbKey);
681
802
  }
682
- if (value === undefined) {
803
+ if (value === undefined && !transformedFields.has(fieldName)) {
683
804
  if (this.actualOperation === action_1.WriteOperation.Insert) {
684
805
  if (field.defaultToViewerOnCreate && field.defaultValueOnCreate) {
685
806
  throw new Error(`cannot set both defaultToViewerOnCreate and defaultValueOnCreate`);
@@ -718,6 +839,9 @@ class Orchestrator {
718
839
  }
719
840
  this.defaultFieldsByFieldName[fieldName] = defaultValue;
720
841
  this.defaultFieldsByTSName[this.getInputKey(fieldName)] = defaultValue;
842
+ if (field.disableUserEditable || updateOnlyIfOther) {
843
+ defaultKeys.add(this.getInputKey(fieldName));
844
+ }
721
845
  }
722
846
  }
723
847
  // if there's data changing, add data
@@ -728,7 +852,7 @@ class Orchestrator {
728
852
  };
729
853
  if (updateInput && this.options.updateInput) {
730
854
  // this basically fixes #605. just needs to be exposed correctly
731
- this.options.updateInput(this.defaultFieldsByTSName);
855
+ this.options.updateInput(this.defaultFieldsByTSName, this.actualOperation, defaultKeys);
732
856
  }
733
857
  }
734
858
  return { data, userDefinedKeys };
@@ -764,7 +888,7 @@ class Orchestrator {
764
888
  valid = await valid;
765
889
  }
766
890
  if (!valid) {
767
- return new Error(`invalid field ${fieldName} with value ${value}`);
891
+ return invalidFieldError(fieldName, value);
768
892
  }
769
893
  }
770
894
  // keep track of dependencies to resolve
@@ -779,7 +903,7 @@ class Orchestrator {
779
903
  valid = await valid;
780
904
  }
781
905
  if (!valid) {
782
- return new Error(`invalid field ${fieldName} with value ${value}`);
906
+ return invalidFieldError(fieldName, value);
783
907
  }
784
908
  }
785
909
  if (field.format) {
@@ -823,6 +947,12 @@ class Orchestrator {
823
947
  for (const fieldName of needsFullDataChecks) {
824
948
  const field = schemaFields.get(fieldName);
825
949
  let value = editedFields.get(fieldName);
950
+ // Deferred defaults are absent from editedFields, but validators must
951
+ // still receive the value that will be saved when this operation writes.
952
+ if (value === undefined &&
953
+ (op === action_1.WriteOperation.Insert || this.hasData(data))) {
954
+ value = this.defaultFieldsByFieldName[fieldName];
955
+ }
826
956
  // @ts-ignore...
827
957
  // type hackery because it's hard
828
958
  const v = await field.validateWithFullData(value, this.options.builder);
@@ -855,6 +985,20 @@ class Orchestrator {
855
985
  }
856
986
  }
857
987
  }
988
+ // If a trigger clears an input, the SQL write can still use its computed
989
+ // default. Update inverse edges for defaults included in `data`; apply edit
990
+ // defaults only when the edit has data to save.
991
+ for (const [fieldName, field] of this.fieldEdgeInputs) {
992
+ if (field.ids !== undefined ||
993
+ data[this.getStorageKey(fieldName)] === undefined) {
994
+ continue;
995
+ }
996
+ const value = this.defaultFieldsByFieldName[fieldName];
997
+ if (value === undefined) {
998
+ continue;
999
+ }
1000
+ this.__setFieldEdges(fieldName, value === null ? [] : Array.isArray(value) ? value : [value], field.edgeType, field.nodeType, {});
1001
+ }
858
1002
  this.validatedFields = data;
859
1003
  this.logValues = logValues;
860
1004
  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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowtop/ent",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "snowtop ent framework",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -18,7 +18,7 @@
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",
@@ -3,26 +3,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BaseEntSchemaWithTZ = exports.BaseEntSchema = exports.EntSchemaWithTZ = exports.EntSchema = exports.Node = exports.Timestamps = void 0;
4
4
  const uuid_1 = require("uuid");
5
5
  const field_1 = require("./field");
6
- let tsFields = {
7
- createdAt: (0, field_1.TimestampType)({
8
- hideFromGraphQL: true,
9
- disableUserEditable: true,
10
- defaultValueOnCreate: () => {
11
- return new Date();
12
- },
13
- }),
14
- updatedAt: (0, field_1.TimestampType)({
15
- hideFromGraphQL: true,
16
- disableUserEditable: true,
17
- defaultValueOnCreate: () => {
18
- return new Date();
19
- },
20
- onlyUpdateIfOtherFieldsBeingSet_BETA: true,
21
- defaultValueOnEdit: () => {
22
- return new Date();
23
- },
24
- }),
25
- };
6
+ function timestampFields(withTimezone) {
7
+ return {
8
+ createdAt: (0, field_1.TimestampType)({
9
+ withTimezone,
10
+ hideFromGraphQL: true,
11
+ disableUserEditable: true,
12
+ defaultValueOnCreate: () => {
13
+ return new Date();
14
+ },
15
+ }),
16
+ updatedAt: (0, field_1.TimestampType)({
17
+ withTimezone,
18
+ hideFromGraphQL: true,
19
+ disableUserEditable: true,
20
+ defaultValueOnCreate: () => {
21
+ return new Date();
22
+ },
23
+ onlyUpdateIfOtherFieldsBeingSet_BETA: true,
24
+ defaultValueOnEdit: () => {
25
+ return new Date();
26
+ },
27
+ }),
28
+ };
29
+ }
30
+ let tsFields = timestampFields(false);
26
31
  // Timestamps is a Pattern that adds a createdAt and updatedAt timestamp fields to the ent
27
32
  exports.Timestamps = {
28
33
  name: "timestamps",
@@ -43,25 +48,7 @@ let nodeFields = {
43
48
  let nodeFieldsWithTZ = {
44
49
  // inconsistent naming :(
45
50
  id: nodeField,
46
- createdAt: (0, field_1.TimestampType)({
47
- hideFromGraphQL: true,
48
- disableUserEditable: true,
49
- defaultValueOnCreate: () => {
50
- return new Date();
51
- },
52
- withTimezone: true,
53
- }),
54
- updatedAt: (0, field_1.TimestampType)({
55
- hideFromGraphQL: true,
56
- disableUserEditable: true,
57
- defaultValueOnCreate: () => {
58
- return new Date();
59
- },
60
- defaultValueOnEdit: () => {
61
- return new Date();
62
- },
63
- withTimezone: true,
64
- }),
51
+ ...timestampFields(true),
65
52
  };
66
53
  // Node is a Pattern that adds 3 fields to the ent: (id, createdAt, and updatedAt timestamps)
67
54
  exports.Node = {
@@ -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);
@@ -81,6 +81,7 @@ export declare class SimpleBuilder<T extends Ent, TExistingEnt extends TMaybleNu
81
81
  placeholderID: ID;
82
82
  orchestrator: Orchestrator<T, Data, Viewer, TExistingEnt>;
83
83
  fields: Map<string, any>;
84
+ private defaultInput;
84
85
  nodeType: string;
85
86
  m: Map<string, any>;
86
87
  constructor(viewer: Viewer, schema: BuilderSchema<T>, fields: Map<string, any>, operation: WriteOperation | undefined, existingEnt: TExistingEnt, action?: Action<T, SimpleBuilder<T, TExistingEnt>, Viewer, Data, TExistingEnt> | undefined, expressions?: Map<string, Clause>);
@@ -191,6 +191,7 @@ class SimpleBuilder {
191
191
  this.schema = schema;
192
192
  this.operation = operation;
193
193
  this.existingEnt = existingEnt;
194
+ this.defaultInput = new Map();
194
195
  this.m = new Map();
195
196
  // create dynamic placeholder
196
197
  // TODO: do we need to use this as the node when there's an existingEnt
@@ -247,11 +248,18 @@ class SimpleBuilder {
247
248
  // to simulate what we do in generated builders where we return a new Map
248
249
  const m = new Map();
249
250
  for (const [k, v] of this.fields) {
250
- m.set(k, v);
251
+ if (!this.defaultInput.has(k) || this.defaultInput.get(k) !== v) {
252
+ m.set(k, v);
253
+ }
251
254
  }
252
255
  return m;
253
256
  },
254
- updateInput: this.updateInput.bind(this),
257
+ updateInput: (input, _operation, defaultKeys) => {
258
+ this.updateInput(input);
259
+ for (const key of defaultKeys ?? []) {
260
+ this.defaultInput.set(key, input[key]);
261
+ }
262
+ },
255
263
  });
256
264
  }
257
265
  getInput() {
@@ -264,6 +272,7 @@ class SimpleBuilder {
264
272
  updateInput(input) {
265
273
  const knownFields = (0, schema_1.getFields)(this.schema);
266
274
  for (const k in input) {
275
+ this.defaultInput.delete(k);
267
276
  if (knownFields.has(k)) {
268
277
  this.fields.set(k, input[k]);
269
278
  }
@@ -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
  }