@memberjunction/metadata-sync 6.1.2 → 6.1.4

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 (49) hide show
  1. package/README.md +88 -25
  2. package/dist/config.d.ts +23 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/index.d.ts +6 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/file-backup-manager.d.ts +10 -0
  10. package/dist/lib/file-backup-manager.d.ts.map +1 -1
  11. package/dist/lib/file-backup-manager.js +17 -0
  12. package/dist/lib/file-backup-manager.js.map +1 -1
  13. package/dist/lib/file-write-batch.d.ts +0 -4
  14. package/dist/lib/file-write-batch.d.ts.map +1 -1
  15. package/dist/lib/file-write-batch.js +10 -8
  16. package/dist/lib/file-write-batch.js.map +1 -1
  17. package/dist/lib/graph-provider-pool.d.ts +62 -33
  18. package/dist/lib/graph-provider-pool.d.ts.map +1 -1
  19. package/dist/lib/graph-provider-pool.js +131 -79
  20. package/dist/lib/graph-provider-pool.js.map +1 -1
  21. package/dist/lib/push-outcome.d.ts +66 -0
  22. package/dist/lib/push-outcome.d.ts.map +1 -0
  23. package/dist/lib/push-outcome.js +81 -0
  24. package/dist/lib/push-outcome.js.map +1 -0
  25. package/dist/lib/push-write-mode.d.ts +47 -0
  26. package/dist/lib/push-write-mode.d.ts.map +1 -0
  27. package/dist/lib/push-write-mode.js +53 -0
  28. package/dist/lib/push-write-mode.js.map +1 -0
  29. package/dist/lib/record-primary-key.d.ts +35 -0
  30. package/dist/lib/record-primary-key.d.ts.map +1 -0
  31. package/dist/lib/record-primary-key.js +44 -0
  32. package/dist/lib/record-primary-key.js.map +1 -0
  33. package/dist/lib/transaction-manager.d.ts +11 -1
  34. package/dist/lib/transaction-manager.d.ts.map +1 -1
  35. package/dist/lib/transaction-manager.js +20 -4
  36. package/dist/lib/transaction-manager.js.map +1 -1
  37. package/dist/plugins/index.d.ts +1 -0
  38. package/dist/plugins/index.d.ts.map +1 -1
  39. package/dist/plugins/index.js +43 -17
  40. package/dist/plugins/index.js.map +1 -1
  41. package/dist/services/PullService.d.ts +0 -1
  42. package/dist/services/PullService.d.ts.map +1 -1
  43. package/dist/services/PullService.js +8 -19
  44. package/dist/services/PullService.js.map +1 -1
  45. package/dist/services/PushService.d.ts +109 -0
  46. package/dist/services/PushService.d.ts.map +1 -1
  47. package/dist/services/PushService.js +631 -319
  48. package/dist/services/PushService.js.map +1 -1
  49. package/package.json +14 -14
@@ -1,26 +1,61 @@
1
1
  /**
2
- * One independent provider per JSON-root graph for `mj sync push`.
2
+ * Provider assignment per JSON-root graph for `mj sync push`.
3
3
  *
4
- * Sibling graphs may run in parallel; a root and its nested relatedEntities share
5
- * one provider so every DB op in that graph uses the same connection. Mixing the
6
- * host connection with a graph instance in the same tree is a deadlock (child FK
7
- * waits on an uncommitted parent).
4
+ * Two modes, fixed when the pool is built. A pool never mixes them.
8
5
  *
9
- * Drain: a graph is released at the end of a batch when (a) it will not appear
10
- * at a later level, or (b) its TransactionDepth is 0 (Save already committed,
11
- * so a fresh instance at the next level is safe). Graphs with leftover depth
12
- * stay live until their last level. Peak live independent instances is therefore
13
- * bounded by `--parallel-batch-size`, plus any still-open leftover-depth graphs.
6
+ * - **`host`** (atomic push, the default): every graph uses the host provider, inside the
7
+ * push transaction. Nothing commits until the push commits. The host has ONE transaction
8
+ * stack, so graphs must run one at a time: `obtain` throws if a second graph asks for the
9
+ * host while another graph still holds it. The caller releases a graph with `drainBatch`.
10
+ *
11
+ * - **`independent`** (an entity directory using isolated transactions): each graph gets its own instance from
12
+ * `CreateIndependentInstance()` (shared pool, own transaction stack), so sibling graphs can
13
+ * run in parallel. A root and its nested relatedEntities share one instance, so every DB op
14
+ * in that graph uses the same connection. Each `Save()` commits on its own, so this mode is
15
+ * NOT atomic: `onGraphSettled` reports which graphs left committed rows behind.
16
+ * If `CreateIndependentInstance` fails, `obtain` throws. It never falls back to the host,
17
+ * because mixing the host connection with graph instances is a deadlock (child FK waits on
18
+ * an uncommitted parent). The caller decides the mode up front (see `probeIndependentInstances`).
19
+ *
20
+ * Drain (independent mode): a graph is released at the end of a batch when (a) it will not
21
+ * appear at a later level, or (b) its TransactionDepth is 0 (Save already committed, so a fresh
22
+ * instance at the next level is safe). Graphs with leftover depth stay live until their last
23
+ * level. Peak live independent instances is therefore bounded by the batch size, plus any
24
+ * still-open leftover-depth graphs.
14
25
  */
26
+ /**
27
+ * Checks once whether the host can create independent instances. Used to pick the pool mode
28
+ * before any graph runs, so a push never discovers mid-file that it has to switch topology.
29
+ * @returns `undefined` when independent instances work, otherwise the reason they do not.
30
+ */
31
+ export async function probeIndependentInstances(host) {
32
+ let instance;
33
+ try {
34
+ instance = await host.CreateIndependentInstance();
35
+ }
36
+ catch (e) {
37
+ return e instanceof Error ? e.message : String(e);
38
+ }
39
+ try {
40
+ await instance.ReleaseIndependentInstance();
41
+ }
42
+ catch {
43
+ /* the probe instance did no work; a failed release changes nothing */
44
+ }
45
+ return undefined;
46
+ }
15
47
  export class GraphProviderPool {
16
- constructor(host, log = () => undefined) {
48
+ constructor(host, options) {
17
49
  this.host = host;
18
- this.log = log;
19
50
  this.providers = new Map();
20
51
  this.lastLevelByGraph = new Map();
21
- this.independentUnavailable = false;
22
- this.unavailableLogged = false;
23
52
  this.anyFailed = false;
53
+ this.mode = options.mode;
54
+ this.log = options.log ?? (() => undefined);
55
+ this.onGraphSettled = options.onGraphSettled;
56
+ }
57
+ get Mode() {
58
+ return this.mode;
24
59
  }
25
60
  /**
26
61
  * Record, per graphId, the highest dependency-level index it occupies so we
@@ -41,54 +76,44 @@ export class GraphProviderPool {
41
76
  get hasFailed() {
42
77
  return this.anyFailed;
43
78
  }
44
- /**
45
- * Provider for this graph. Never returns a mix: either every graph in the
46
- * file gets its own independent instance, or (if the *first*
47
- * CreateIndependentInstance fails) every graph uses the host.
48
- *
49
- * If independent instances already exist and a later CreateIndependentInstance
50
- * throws, this throws rather than handing the rest of the file to the host.
51
- * Mid-file topology change is the deadlock this pool exists to prevent.
52
- *
53
- * The host itself is never stored in the map, so release cannot
54
- * RollbackTransaction the global push TX.
55
- */
79
+ /** Provider for this graph. See the class comment for the per-mode rules. */
56
80
  async obtain(graphId) {
81
+ if (this.mode === 'host') {
82
+ return this.obtainHost(graphId);
83
+ }
57
84
  const existing = this.providers.get(graphId);
58
85
  if (existing)
59
86
  return existing;
60
- if (this.independentUnavailable)
61
- return this.host;
62
87
  try {
63
88
  const created = await this.host.CreateIndependentInstance();
64
89
  this.providers.set(graphId, created);
65
90
  return created;
66
91
  }
67
92
  catch (e) {
68
- const reason = e.message;
69
- if (this.providers.size > 0) {
70
- throw new Error(`CreateIndependentInstance failed after ${this.providers.size} graph(s) already had independent instances (${reason}). ` +
71
- `Refusing mixed host + independent topology in one file.`);
72
- }
73
- this.independentUnavailable = true;
74
- if (!this.unavailableLogged) {
75
- this.unavailableLogged = true;
76
- this.log(`⚠️ CreateIndependentInstance unavailable (${reason}); ` +
77
- `ALL graphs in this file use the host provider (inside the push transaction). ` +
78
- `Mixing host + independent instances in one file is a deadlock — we refuse that topology.`);
79
- }
80
- return this.host;
93
+ const reason = e instanceof Error ? e.message : String(e);
94
+ throw new Error(`CreateIndependentInstance failed for graph ${graphId} (${reason}). ` +
95
+ `Refusing to run this graph on the host provider: mixing host and independent instances is a deadlock.`);
81
96
  }
82
97
  }
98
+ obtainHost(graphId) {
99
+ if (this.hostHolder !== undefined && this.hostHolder !== graphId) {
100
+ throw new Error(`Graph ${graphId} asked for the host provider while graph ${this.hostHolder} still holds it. ` +
101
+ `In atomic mode graphs share one transaction stack and must run one at a time.`);
102
+ }
103
+ this.hostHolder = graphId;
104
+ return this.host;
105
+ }
83
106
  /**
84
- * Commit-or-rollback + release graphs in this batch that are done:
85
- * their last level is `levelIndex`, or TransactionDepth is already 0
86
- * (Save settled; a fresh instance at a later level is safe).
87
- * Leftover-depth graphs that still appear later stay live.
88
- * Returns the first settle error so the caller cannot report success
89
- * with uncommitted rows.
107
+ * Release graphs in this batch that are done. Host mode: frees the host for the next graph
108
+ * (the push transaction settles it later). Independent mode: commit-or-rollback + release the
109
+ * graphs whose last level is `levelIndex`, or whose TransactionDepth is already 0.
110
+ * Returns the first settle error so the caller cannot report success with uncommitted rows.
90
111
  */
91
112
  async drainBatch(graphIds, levelIndex) {
113
+ if (this.mode === 'host') {
114
+ this.releaseHost(graphIds);
115
+ return undefined;
116
+ }
92
117
  const ending = graphIds.filter((id) => {
93
118
  const last = this.lastLevelByGraph.get(id) ?? levelIndex;
94
119
  if (last === levelIndex)
@@ -98,52 +123,79 @@ export class GraphProviderPool {
98
123
  });
99
124
  return this.releaseGraphs(ending);
100
125
  }
101
- /** Release every remaining independent instance. Safe to call from `finally`. */
126
+ /** Release every remaining graph. Safe to call from `finally`. */
102
127
  async releaseAll() {
128
+ if (this.mode === 'host') {
129
+ this.hostHolder = undefined;
130
+ return undefined;
131
+ }
103
132
  return this.releaseGraphs([...this.providers.keys()]);
104
133
  }
134
+ releaseHost(graphIds) {
135
+ if (this.hostHolder !== undefined && graphIds.includes(this.hostHolder)) {
136
+ this.hostHolder = undefined;
137
+ }
138
+ }
105
139
  async releaseGraphs(ids) {
106
140
  let settleError;
107
141
  for (const id of ids) {
108
142
  const provider = this.providers.get(id);
109
143
  this.providers.delete(id);
110
- if (!provider || provider === this.host)
144
+ if (!provider)
111
145
  continue;
146
+ const error = await this.settleAndRelease(id, provider);
147
+ settleError ??= error;
148
+ }
149
+ return settleError;
150
+ }
151
+ /** Settle one independent instance, report the outcome, and always release it. */
152
+ async settleAndRelease(id, provider) {
153
+ try {
154
+ const outcome = await this.settle(provider);
155
+ this.onGraphSettled?.(id, outcome);
156
+ return undefined;
157
+ }
158
+ catch (e) {
159
+ const error = e instanceof Error ? e : new Error(String(e));
160
+ this.anyFailed = true;
161
+ this.log(`Failed to settle graph ${id} transaction: ${error.message}`);
162
+ await this.rollbackQuietly(provider);
163
+ this.onGraphSettled?.(id, 'rolledBack');
164
+ return error;
165
+ }
166
+ finally {
112
167
  try {
113
- if (provider.TransactionDepth > 0) {
114
- if (this.anyFailed) {
115
- // Explicit rollback at the call site — do not rely on
116
- // ReleaseIndependentInstance's implicit leftover-depth rollback.
117
- await provider.RollbackTransaction();
118
- }
119
- else {
120
- await provider.CommitTransaction();
121
- }
122
- }
168
+ await provider.ReleaseIndependentInstance();
123
169
  }
124
- catch (e) {
125
- this.anyFailed = true;
126
- settleError ??= e;
127
- this.log(`Failed to settle graph ${id} transaction: ${e.message}`);
128
- try {
129
- if (provider.TransactionDepth > 0) {
130
- await provider.RollbackTransaction();
131
- }
132
- }
133
- catch {
134
- /* still release */
135
- }
170
+ catch {
171
+ /* pool-safe: handle already dropped */
136
172
  }
137
- finally {
138
- try {
139
- await provider.ReleaseIndependentInstance();
140
- }
141
- catch {
142
- /* pool-safe: handle already dropped */
143
- }
173
+ }
174
+ }
175
+ /**
176
+ * Depth 0: every Save on this instance already committed. Leftover depth: commit it, or
177
+ * roll it back explicitly when the file failed (do not rely on ReleaseIndependentInstance).
178
+ */
179
+ async settle(provider) {
180
+ if (provider.TransactionDepth === 0) {
181
+ return 'committed';
182
+ }
183
+ if (this.anyFailed) {
184
+ await provider.RollbackTransaction();
185
+ return 'rolledBack';
186
+ }
187
+ await provider.CommitTransaction();
188
+ return 'committed';
189
+ }
190
+ async rollbackQuietly(provider) {
191
+ try {
192
+ if (provider.TransactionDepth > 0) {
193
+ await provider.RollbackTransaction();
144
194
  }
145
195
  }
146
- return settleError;
196
+ catch {
197
+ /* still release */
198
+ }
147
199
  }
148
200
  }
149
201
  //# sourceMappingURL=graph-provider-pool.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-provider-pool.js","sourceRoot":"","sources":["../../src/lib/graph-provider-pool.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"graph-provider-pool.js","sourceRoot":"","sources":["../../src/lib/graph-provider-pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @fileoverview What a failed `mj sync push` left behind, and how to say so.
3
+ * @module push-outcome
4
+ */
5
+ import type { PushWriteMode } from './push-write-mode.js';
6
+ /** A create or update committed outside the push transaction (an isolated directory only). */
7
+ export interface CommittedWrite {
8
+ /** Absolute path of the metadata file the record came from. */
9
+ filePath: string;
10
+ entityName: string;
11
+ /** Position of the record inside the file, e.g. `MJ: Actions[3]/MJ: Action Params[0]`. */
12
+ recordPath: string;
13
+ status: 'created' | 'updated';
14
+ }
15
+ /** What a push had done when it failed. Same shape as the successful result's counts. */
16
+ export interface PushPartialTotals {
17
+ created: number;
18
+ updated: number;
19
+ unchanged: number;
20
+ deleted: number;
21
+ skipped: number;
22
+ deferred: number;
23
+ errors: number;
24
+ }
25
+ export interface PushAbortedDetails {
26
+ /** The write modes the directories processed so far were using. */
27
+ modes: PushWriteMode[];
28
+ /** Whether the push transaction was rolled back (or was never opened, as in a dry run). */
29
+ rolledBack: boolean;
30
+ /** Rows that are still in the database even though the push failed. */
31
+ committedWrites: CommittedWrite[];
32
+ /** Counts as far as the push got. Reported so a failed run is still machine-readable. */
33
+ totals: PushPartialTotals;
34
+ /** The SQL log for this run, when one was being written. Most wanted on a failure. */
35
+ sqlLogPath?: string;
36
+ cause: unknown;
37
+ }
38
+ /**
39
+ * Thrown by `PushService.push()` whenever a push fails after it started writing.
40
+ * The message is the original failure's message, so existing callers keep working;
41
+ * the extra fields let a caller say exactly what is and is not in the database.
42
+ */
43
+ export declare class PushAbortedError extends Error {
44
+ /** The write modes in play when the push failed, e.g. `['shared']` or `['shared','isolated']`. */
45
+ readonly modes: PushWriteMode[];
46
+ readonly rolledBack: boolean;
47
+ readonly committedWrites: CommittedWrite[];
48
+ readonly totals: PushPartialTotals;
49
+ readonly sqlLogPath?: string;
50
+ constructor(details: PushAbortedDetails);
51
+ /** True only when nothing from this push is left in the database. */
52
+ get NothingCommitted(): boolean;
53
+ }
54
+ /**
55
+ * The lines that tell the user how a failed push ended. The first line is the summary;
56
+ * any further lines list the committed records, grouped by file.
57
+ */
58
+ export declare function describeRollbackOutcome(rolledBack: boolean, committedWrites: CommittedWrite[], cwd: string): string[];
59
+ /** One line per file, then one indented line per committed record. */
60
+ export declare function describeCommittedWrites(committedWrites: CommittedWrite[], cwd: string): string[];
61
+ /**
62
+ * Message for a failed COMMIT. On PostgreSQL, deferred foreign keys are checked at commit, so
63
+ * the failure is about the transaction as a whole and cannot be tied to one record.
64
+ */
65
+ export declare function describeCommitFailure(cause: unknown, platform: string | undefined): string;
66
+ //# sourceMappingURL=push-outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-outcome.d.ts","sourceRoot":"","sources":["../../src/lib/push-outcome.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,0FAA0F;IAC1F,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,SAAS,GAAG,SAAS,CAAC;CAC/B;AAED,yFAAyF;AACzF,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,2FAA2F;IAC3F,UAAU,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,eAAe,EAAE,cAAc,EAAE,CAAC;IAClC,yFAAyF;IACzF,MAAM,EAAE,iBAAiB,CAAC;IAC1B,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,kGAAkG;IAClG,QAAQ,CAAC,KAAK,EAAE,aAAa,EAAE,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;gBAEjB,OAAO,EAAE,kBAAkB;IAUvC,qEAAqE;IACrE,IAAI,gBAAgB,IAAI,OAAO,CAE9B;CACF;AAMD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBrH;AAED,sEAAsE;AACtE,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,cAAc,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAehG;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAS1F"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @fileoverview What a failed `mj sync push` left behind, and how to say so.
3
+ * @module push-outcome
4
+ */
5
+ import path from 'path';
6
+ /**
7
+ * Thrown by `PushService.push()` whenever a push fails after it started writing.
8
+ * The message is the original failure's message, so existing callers keep working;
9
+ * the extra fields let a caller say exactly what is and is not in the database.
10
+ */
11
+ export class PushAbortedError extends Error {
12
+ constructor(details) {
13
+ super(messageOf(details.cause), { cause: details.cause });
14
+ this.name = 'PushAbortedError';
15
+ this.modes = details.modes;
16
+ this.rolledBack = details.rolledBack;
17
+ this.committedWrites = details.committedWrites;
18
+ this.totals = details.totals;
19
+ this.sqlLogPath = details.sqlLogPath;
20
+ }
21
+ /** True only when nothing from this push is left in the database. */
22
+ get NothingCommitted() {
23
+ return this.rolledBack && this.committedWrites.length === 0;
24
+ }
25
+ }
26
+ function messageOf(cause) {
27
+ return cause instanceof Error ? cause.message : String(cause);
28
+ }
29
+ /**
30
+ * The lines that tell the user how a failed push ended. The first line is the summary;
31
+ * any further lines list the committed records, grouped by file.
32
+ */
33
+ export function describeRollbackOutcome(rolledBack, committedWrites, cwd) {
34
+ if (!rolledBack) {
35
+ return [
36
+ '❌ Database transaction rollback failed. Check the database before pushing again: ' +
37
+ 'rows written by this push may still be locked or present.',
38
+ ...describeCommittedWrites(committedWrites, cwd),
39
+ ];
40
+ }
41
+ if (committedWrites.length === 0) {
42
+ return ['✓ Database transaction rolled back successfully. Nothing from this push was saved.'];
43
+ }
44
+ return [
45
+ `⚠️ The push transaction was rolled back, but ${committedWrites.length} created or updated ` +
46
+ `record${committedWrites.length === 1 ? '' : 's'} in directories using isolated transactions ` +
47
+ `${committedWrites.length === 1 ? 'was' : 'were'} already committed and ` +
48
+ `${committedWrites.length === 1 ? 'is' : 'are'} still in the database. Their files keep the pushed contents.`,
49
+ ...describeCommittedWrites(committedWrites, cwd),
50
+ ];
51
+ }
52
+ /** One line per file, then one indented line per committed record. */
53
+ export function describeCommittedWrites(committedWrites, cwd) {
54
+ const byFile = new Map();
55
+ for (const write of committedWrites) {
56
+ const list = byFile.get(write.filePath) ?? [];
57
+ list.push(write);
58
+ byFile.set(write.filePath, list);
59
+ }
60
+ const lines = [];
61
+ for (const [filePath, writes] of byFile) {
62
+ lines.push(` ${path.relative(cwd, filePath) || filePath}: ${writes.length} committed`);
63
+ for (const write of writes) {
64
+ lines.push(` ${write.status} ${write.entityName} at ${write.recordPath}`);
65
+ }
66
+ }
67
+ return lines;
68
+ }
69
+ /**
70
+ * Message for a failed COMMIT. On PostgreSQL, deferred foreign keys are checked at commit, so
71
+ * the failure is about the transaction as a whole and cannot be tied to one record.
72
+ */
73
+ export function describeCommitFailure(cause, platform) {
74
+ const base = `The database rejected the commit of the push transaction, so the transaction was not saved: ${messageOf(cause)}`;
75
+ if (platform === 'postgresql') {
76
+ return (`${base}. PostgreSQL checks deferred foreign-key constraints when the transaction commits, ` +
77
+ `so this error is not tied to a single record in the metadata files.`);
78
+ }
79
+ return base;
80
+ }
81
+ //# sourceMappingURL=push-outcome.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-outcome.js","sourceRoot":"","sources":["../../src/lib/push-outcome.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview How `mj sync push` writes the creates and updates of one entity directory.
3
+ * @module push-write-mode
4
+ *
5
+ * - `shared` (default): every save runs on the host provider inside the push transaction, one
6
+ * JSON-root graph at a time. A failure anywhere rolls back everything that directory wrote,
7
+ * along with every other shared directory in the push.
8
+ * - `isolated` (`isolatedTransactions: true`, or `--isolated-transactions`): sibling graphs run in
9
+ * parallel, each on its own provider instance and therefore its own connection and transaction.
10
+ * Each save commits on its own, so a failure does NOT roll those creates and updates back.
11
+ *
12
+ * Isolation is chosen **per entity directory**, because entities differ: metadata is simple and
13
+ * wants atomicity, while an entity that manages its own transaction scopes (an order with journal
14
+ * entries and payments) may want the parallelism. It cannot be finer than a directory: mixing the
15
+ * host connection with per-graph connections inside one file is the deadlock the graph pool exists
16
+ * to prevent.
17
+ */
18
+ /** How one entity directory's creates and updates are written. */
19
+ export type PushWriteMode = 'shared' | 'isolated';
20
+ /** Default number of JSON-root graphs run at once in an isolated directory. */
21
+ export declare const DEFAULT_PARALLEL_BATCH_SIZE = 10;
22
+ export interface PushWriteModeInput {
23
+ /** CLI `--isolated-transactions` / `--no-isolated-transactions`. Undefined when not passed. */
24
+ isolatedFlag?: boolean;
25
+ /** `push.isolatedTransactions` from this directory's `.mj-sync.json`. */
26
+ entityIsolated?: boolean;
27
+ /** `push.isolatedTransactions` from the root `.mj-sync.json`. */
28
+ rootIsolated?: boolean;
29
+ }
30
+ /** Where a directory's mode came from, for the log line. */
31
+ export type PushWriteModeSource = 'flag' | 'entity' | 'root' | 'default';
32
+ export interface PushDirectoryMode {
33
+ mode: PushWriteMode;
34
+ source: PushWriteModeSource;
35
+ }
36
+ /**
37
+ * The mode for one entity directory. The CLI flag wins over every file, so a run can force either
38
+ * mode without editing metadata; then the directory's own config, then the root's, then shared.
39
+ */
40
+ export declare function resolveDirectoryMode(input: PushWriteModeInput): PushDirectoryMode;
41
+ /** Graphs to run at once in a directory of this mode. */
42
+ export declare function graphBatchSizeFor(mode: PushWriteMode, parallelBatchSize?: number): number;
43
+ /** The warning shown once per push that writes any isolated directory to the database. */
44
+ export declare function isolatedModeWarning(directories: string[], graphBatchSize: number): string;
45
+ /** Told once when `--parallel-batch-size` cannot apply, so the flag does not look effective. */
46
+ export declare function unusedBatchSizeWarning(size: number): string;
47
+ //# sourceMappingURL=push-write-mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-write-mode.d.ts","sourceRoot":"","sources":["../../src/lib/push-write-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,CAAC;AAElD,+EAA+E;AAC/E,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,MAAM,WAAW,kBAAkB;IACjC,+FAA+F;IAC/F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iEAAiE;IACjE,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,4DAA4D;AAC5D,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,kBAAkB,GAAG,iBAAiB,CAWjF;AAED,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,iBAAiB,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzF;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAOzF;AAED,gGAAgG;AAChG,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3D"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @fileoverview How `mj sync push` writes the creates and updates of one entity directory.
3
+ * @module push-write-mode
4
+ *
5
+ * - `shared` (default): every save runs on the host provider inside the push transaction, one
6
+ * JSON-root graph at a time. A failure anywhere rolls back everything that directory wrote,
7
+ * along with every other shared directory in the push.
8
+ * - `isolated` (`isolatedTransactions: true`, or `--isolated-transactions`): sibling graphs run in
9
+ * parallel, each on its own provider instance and therefore its own connection and transaction.
10
+ * Each save commits on its own, so a failure does NOT roll those creates and updates back.
11
+ *
12
+ * Isolation is chosen **per entity directory**, because entities differ: metadata is simple and
13
+ * wants atomicity, while an entity that manages its own transaction scopes (an order with journal
14
+ * entries and payments) may want the parallelism. It cannot be finer than a directory: mixing the
15
+ * host connection with per-graph connections inside one file is the deadlock the graph pool exists
16
+ * to prevent.
17
+ */
18
+ /** Default number of JSON-root graphs run at once in an isolated directory. */
19
+ export const DEFAULT_PARALLEL_BATCH_SIZE = 10;
20
+ /**
21
+ * The mode for one entity directory. The CLI flag wins over every file, so a run can force either
22
+ * mode without editing metadata; then the directory's own config, then the root's, then shared.
23
+ */
24
+ export function resolveDirectoryMode(input) {
25
+ if (input.isolatedFlag !== undefined) {
26
+ return { mode: input.isolatedFlag ? 'isolated' : 'shared', source: 'flag' };
27
+ }
28
+ if (input.entityIsolated !== undefined) {
29
+ return { mode: input.entityIsolated ? 'isolated' : 'shared', source: 'entity' };
30
+ }
31
+ if (input.rootIsolated !== undefined) {
32
+ return { mode: input.rootIsolated ? 'isolated' : 'shared', source: 'root' };
33
+ }
34
+ return { mode: 'shared', source: 'default' };
35
+ }
36
+ /** Graphs to run at once in a directory of this mode. */
37
+ export function graphBatchSizeFor(mode, parallelBatchSize) {
38
+ return mode === 'isolated' ? parallelBatchSize ?? DEFAULT_PARALLEL_BATCH_SIZE : 1;
39
+ }
40
+ /** The warning shown once per push that writes any isolated directory to the database. */
41
+ export function isolatedModeWarning(directories, graphBatchSize) {
42
+ const list = directories.join(', ');
43
+ return (`Isolated transactions (${graphBatchSize} graphs in parallel) for: ${list}. ` +
44
+ `Each create and update in those directories is committed as soon as it is saved. If the push fails, ` +
45
+ `those records stay in the database; only the shared directories, the deletes and the deferred records roll back.`);
46
+ }
47
+ /** Told once when `--parallel-batch-size` cannot apply, so the flag does not look effective. */
48
+ export function unusedBatchSizeWarning(size) {
49
+ return (`--parallel-batch-size=${size} is ignored: no entity directory in this push uses isolated transactions, ` +
50
+ `so records are saved one graph at a time in a single transaction. ` +
51
+ `Set push.isolatedTransactions on an entity's .mj-sync.json, or pass --isolated-transactions, to run graphs in parallel.`);
52
+ }
53
+ //# sourceMappingURL=push-write-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-write-mode.js","sourceRoot":"","sources":["../../src/lib/push-write-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAmB9C;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAyB;IAC5D,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC9E,CAAC;IACD,IAAI,KAAK,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACvC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAClF,CAAC;IACD,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC9E,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC/C,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,iBAAiB,CAAC,IAAmB,EAAE,iBAA0B;IAC/E,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,iBAAiB,IAAI,2BAA2B,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,mBAAmB,CAAC,WAAqB,EAAE,cAAsB;IAC/E,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,CACL,0BAA0B,cAAc,6BAA6B,IAAI,IAAI;QAC7E,sGAAsG;QACtG,kHAAkH,CACnH,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,OAAO,CACL,yBAAyB,IAAI,4EAA4E;QACzG,oEAAoE;QACpE,yHAAyH,CAC1H,CAAC;AACJ,CAAC"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Primary-key extraction for pulled records.
3
+ *
4
+ * Pull must key every database record by its real primary-key value: the key decides whether a
5
+ * record updates an existing file entry or is appended as a new one. Reading the key through the
6
+ * entity's typed property (`record.ID`) only works when the entity's generated subclass is
7
+ * registered in this process. When it is not — an Open App whose server package did not load, a
8
+ * host's generated entities package missing from the CLI — the ClassFactory hands back a bare
9
+ * `BaseEntity`, which has no typed properties, so every record read `undefined`. All records then
10
+ * shared one key, each overwrote the last in the write batch, and a pull of N new records wrote
11
+ * exactly one, with an empty `primaryKey` that duplicated on the next pull (#3415).
12
+ *
13
+ * `BaseEntity.Get()` is metadata-driven and works on a bare `BaseEntity` as well as on a subclass.
14
+ */
15
+ import type { BaseEntity, EntityInfo } from '@memberjunction/core';
16
+ import type { RecordData } from './sync-engine.js';
17
+ /** Primary-key values keyed by field name — the shape `RecordData.primaryKey` carries. */
18
+ export type PrimaryKeyValues = NonNullable<RecordData['primaryKey']>;
19
+ /**
20
+ * Reads every primary-key field of `record` through `BaseEntity.Get()`.
21
+ *
22
+ * @throws when a key field has no value. A record without its key cannot be matched to a file
23
+ * entry and would overwrite other records, so refusing is the only safe outcome.
24
+ */
25
+ export declare function extractPrimaryKeyValues(record: BaseEntity, entityInfo: EntityInfo): PrimaryKeyValues;
26
+ /** True when `primaryKey` has at least one field and every field holds a value. */
27
+ export declare function hasCompletePrimaryKey(primaryKey: RecordData['primaryKey']): boolean;
28
+ /**
29
+ * Builds the string pull uses to match a database record to a file entry: `field:value` segments,
30
+ * sorted by field name, joined by `|`. `\` and `|` inside a value are escaped, so a value that
31
+ * contains the separator can't make two different keys produce the same string. The string is only
32
+ * ever compared in memory — never parsed or persisted.
33
+ */
34
+ export declare function createPrimaryKeyLookup(primaryKey: RecordData['primaryKey']): string;
35
+ //# sourceMappingURL=record-primary-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-primary-key.d.ts","sourceRoot":"","sources":["../../src/lib/record-primary-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,0FAA0F;AAC1F,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;AAErE;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU,GAAG,gBAAgB,CAapG;AAUD,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,YAAY,CAAC,GAAG,OAAO,CAGnF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,UAAU,CAAC,YAAY,CAAC,GAAG,MAAM,CAMnF"}