@memberjunction/metadata-sync 6.1.2 → 6.2.0-edge.0

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 (68) hide show
  1. package/README.md +88 -25
  2. package/dist/config.d.ts +42 -2
  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/EntityPropertyExtractor.d.ts +16 -0
  10. package/dist/lib/EntityPropertyExtractor.d.ts.map +1 -1
  11. package/dist/lib/EntityPropertyExtractor.js +34 -0
  12. package/dist/lib/EntityPropertyExtractor.js.map +1 -1
  13. package/dist/lib/FieldExternalizer.d.ts +1 -2
  14. package/dist/lib/FieldExternalizer.d.ts.map +1 -1
  15. package/dist/lib/FieldExternalizer.js.map +1 -1
  16. package/dist/lib/RecordProcessor.d.ts +7 -0
  17. package/dist/lib/RecordProcessor.d.ts.map +1 -1
  18. package/dist/lib/RecordProcessor.js +27 -1
  19. package/dist/lib/RecordProcessor.js.map +1 -1
  20. package/dist/lib/existing-record-files.d.ts +12 -0
  21. package/dist/lib/existing-record-files.d.ts.map +1 -0
  22. package/dist/lib/existing-record-files.js +85 -0
  23. package/dist/lib/existing-record-files.js.map +1 -0
  24. package/dist/lib/file-backup-manager.d.ts +10 -0
  25. package/dist/lib/file-backup-manager.d.ts.map +1 -1
  26. package/dist/lib/file-backup-manager.js +17 -0
  27. package/dist/lib/file-backup-manager.js.map +1 -1
  28. package/dist/lib/file-write-batch.d.ts +0 -4
  29. package/dist/lib/file-write-batch.d.ts.map +1 -1
  30. package/dist/lib/file-write-batch.js +10 -8
  31. package/dist/lib/file-write-batch.js.map +1 -1
  32. package/dist/lib/graph-provider-pool.d.ts +62 -33
  33. package/dist/lib/graph-provider-pool.d.ts.map +1 -1
  34. package/dist/lib/graph-provider-pool.js +131 -79
  35. package/dist/lib/graph-provider-pool.js.map +1 -1
  36. package/dist/lib/json-subproperty-externalization.d.ts +77 -0
  37. package/dist/lib/json-subproperty-externalization.d.ts.map +1 -0
  38. package/dist/lib/json-subproperty-externalization.js +139 -0
  39. package/dist/lib/json-subproperty-externalization.js.map +1 -0
  40. package/dist/lib/push-outcome.d.ts +66 -0
  41. package/dist/lib/push-outcome.d.ts.map +1 -0
  42. package/dist/lib/push-outcome.js +81 -0
  43. package/dist/lib/push-outcome.js.map +1 -0
  44. package/dist/lib/push-write-mode.d.ts +47 -0
  45. package/dist/lib/push-write-mode.d.ts.map +1 -0
  46. package/dist/lib/push-write-mode.js +53 -0
  47. package/dist/lib/push-write-mode.js.map +1 -0
  48. package/dist/lib/record-primary-key.d.ts +35 -0
  49. package/dist/lib/record-primary-key.d.ts.map +1 -0
  50. package/dist/lib/record-primary-key.js +44 -0
  51. package/dist/lib/record-primary-key.js.map +1 -0
  52. package/dist/lib/transaction-manager.d.ts +11 -1
  53. package/dist/lib/transaction-manager.d.ts.map +1 -1
  54. package/dist/lib/transaction-manager.js +20 -4
  55. package/dist/lib/transaction-manager.js.map +1 -1
  56. package/dist/plugins/index.d.ts +1 -0
  57. package/dist/plugins/index.d.ts.map +1 -1
  58. package/dist/plugins/index.js +43 -17
  59. package/dist/plugins/index.js.map +1 -1
  60. package/dist/services/PullService.d.ts +1 -3
  61. package/dist/services/PullService.d.ts.map +1 -1
  62. package/dist/services/PullService.js +28 -68
  63. package/dist/services/PullService.js.map +1 -1
  64. package/dist/services/PushService.d.ts +109 -0
  65. package/dist/services/PushService.d.ts.map +1 -1
  66. package/dist/services/PushService.js +631 -319
  67. package/dist/services/PushService.js.map +1 -1
  68. package/package.json +14 -14
@@ -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"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Reads every primary-key field of `record` through `BaseEntity.Get()`.
3
+ *
4
+ * @throws when a key field has no value. A record without its key cannot be matched to a file
5
+ * entry and would overwrite other records, so refusing is the only safe outcome.
6
+ */
7
+ export function extractPrimaryKeyValues(record, entityInfo) {
8
+ const values = {};
9
+ for (const pk of entityInfo.PrimaryKeys) {
10
+ const value = record.Get(pk.Name);
11
+ if (!isPresentKeyValue(value)) {
12
+ throw new Error(`Cannot read primary key field '${pk.Name}' on a '${entityInfo.Name}' record. ` +
13
+ `Refusing to write it: a record without its key would overwrite other records in the same file.`);
14
+ }
15
+ values[pk.Name] = value;
16
+ }
17
+ return values;
18
+ }
19
+ /**
20
+ * True when a key field holds a value. Only a missing value counts as absent: an empty string, or
21
+ * the text `null`, is a real key value.
22
+ */
23
+ function isPresentKeyValue(value) {
24
+ return value !== undefined && value !== null;
25
+ }
26
+ /** True when `primaryKey` has at least one field and every field holds a value. */
27
+ export function hasCompletePrimaryKey(primaryKey) {
28
+ const values = Object.values(primaryKey ?? {});
29
+ return values.length > 0 && values.every(isPresentKeyValue);
30
+ }
31
+ /**
32
+ * Builds the string pull uses to match a database record to a file entry: `field:value` segments,
33
+ * sorted by field name, joined by `|`. `\` and `|` inside a value are escaped, so a value that
34
+ * contains the separator can't make two different keys produce the same string. The string is only
35
+ * ever compared in memory — never parsed or persisted.
36
+ */
37
+ export function createPrimaryKeyLookup(primaryKey) {
38
+ const values = primaryKey ?? {};
39
+ return Object.keys(values)
40
+ .sort()
41
+ .map((field) => `${field}:${String(values[field]).replace(/[\\|]/g, '\\$&')}`)
42
+ .join('|');
43
+ }
44
+ //# sourceMappingURL=record-primary-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-primary-key.js","sourceRoot":"","sources":["../../src/lib/record-primary-key.ts"],"names":[],"mappings":"AAoBA;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAkB,EAAE,UAAsB;IAChF,MAAM,MAAM,GAAqB,EAAE,CAAC;IACpC,KAAK,MAAM,EAAE,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,kCAAkC,EAAE,CAAC,IAAI,WAAW,UAAU,CAAC,IAAI,YAAY;gBAC7E,gGAAgG,CACnG,CAAC;QACJ,CAAC;QACD,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,KAA+B;IACxD,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC;AAC/C,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,qBAAqB,CAAC,UAAoC;IACxE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC;IAC/C,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAoC;IACzE,MAAM,MAAM,GAAG,UAAU,IAAI,EAAE,CAAC;IAChC,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACvB,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;SAC7E,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
@@ -5,13 +5,23 @@
5
5
  * This module provides transaction support for push operations to ensure
6
6
  * all related entities are saved atomically with rollback capability.
7
7
  */
8
+ import type { DatabaseProviderBase } from '@memberjunction/core';
8
9
  import { SQLLogger } from './sql-logger.js';
9
10
  export interface TransactionOptions {
10
11
  }
11
12
  export declare class TransactionManager {
12
13
  private inTransaction;
13
14
  private sqlLogger?;
14
- constructor(sqlLogger?: SQLLogger);
15
+ private readonly explicitProvider?;
16
+ /**
17
+ * @param sqlLogger - optional SQL logger
18
+ * @param provider - the provider that owns the transaction. Pass it whenever the caller runs
19
+ * work on a specific provider (an atomic `mj sync push` runs every save on it), so the
20
+ * transaction and the work cannot end up on different instances. Defaults to the provider
21
+ * `initializeProvider()` set up.
22
+ */
23
+ constructor(sqlLogger?: SQLLogger, provider?: DatabaseProviderBase);
24
+ private resolveProvider;
15
25
  /**
16
26
  * Begin a new transaction.
17
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-manager.d.ts","sourceRoot":"","sources":["../../src/lib/transaction-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,WAAW,kBAAkB;CAClC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,SAAS,CAAC,CAAY;gBAElB,SAAS,CAAC,EAAE,SAAS;IAIjC;;;;;;;;;;;;;;;;OAgBG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBxC;;;OAGG;IACG,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC;IAqB7C;;OAEG;IACG,oBAAoB,CAAC,CAAC,EAC1B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,CAAC,CAAC;IAab;;OAEG;IACH,IAAI,eAAe,IAAI,OAAO,CAE7B;CACF"}
1
+ {"version":3,"file":"transaction-manager.d.ts","sourceRoot":"","sources":["../../src/lib/transaction-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,MAAM,WAAW,kBAAkB;CAClC;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,SAAS,CAAC,CAAY;IAC9B,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAuB;IAEzD;;;;;;OAMG;gBACS,SAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,oBAAoB;IAKlE,OAAO,CAAC,eAAe;IAIvB;;;;;;;;;;;;;;;;OAgBG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBnE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBxC;;;OAGG;IACG,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC;IA0B7C;;OAEG;IACG,oBAAoB,CAAC,CAAC,EAC1B,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,CAAC,CAAC;IAab;;OAEG;IACH,IAAI,eAAe,IAAI,OAAO,CAE7B;CACF"}
@@ -7,9 +7,20 @@
7
7
  */
8
8
  import { getDataProvider } from './provider-utils.js';
9
9
  export class TransactionManager {
10
- constructor(sqlLogger) {
10
+ /**
11
+ * @param sqlLogger - optional SQL logger
12
+ * @param provider - the provider that owns the transaction. Pass it whenever the caller runs
13
+ * work on a specific provider (an atomic `mj sync push` runs every save on it), so the
14
+ * transaction and the work cannot end up on different instances. Defaults to the provider
15
+ * `initializeProvider()` set up.
16
+ */
17
+ constructor(sqlLogger, provider) {
11
18
  this.inTransaction = false;
12
19
  this.sqlLogger = sqlLogger;
20
+ this.explicitProvider = provider;
21
+ }
22
+ resolveProvider() {
23
+ return this.explicitProvider ?? getDataProvider();
13
24
  }
14
25
  /**
15
26
  * Begin a new transaction.
@@ -32,7 +43,7 @@ export class TransactionManager {
32
43
  if (this.inTransaction) {
33
44
  throw new Error('Transaction already in progress');
34
45
  }
35
- const provider = getDataProvider();
46
+ const provider = this.resolveProvider();
36
47
  if (!provider) {
37
48
  throw new Error('No data provider available');
38
49
  }
@@ -51,7 +62,7 @@ export class TransactionManager {
51
62
  if (!this.inTransaction) {
52
63
  return; // No transaction to commit
53
64
  }
54
- const provider = getDataProvider();
65
+ const provider = this.resolveProvider();
55
66
  if (!provider) {
56
67
  throw new Error('No data provider available');
57
68
  }
@@ -71,7 +82,7 @@ export class TransactionManager {
71
82
  if (!this.inTransaction) {
72
83
  return true; // No transaction to rollback
73
84
  }
74
- const provider = getDataProvider();
85
+ const provider = this.resolveProvider();
75
86
  if (!provider) {
76
87
  throw new Error('No data provider available');
77
88
  }
@@ -83,6 +94,11 @@ export class TransactionManager {
83
94
  catch (error) {
84
95
  // Log but don't throw - we're already in an error state
85
96
  console.error('Failed to rollback transaction:', error);
97
+ if (provider.TransactionDepth === 0) {
98
+ // The provider already dropped its handle, so there is nothing left to retry. The rollback
99
+ // is still reported as failed: the server-side transaction was not confirmed as rolled back.
100
+ this.inTransaction = false;
101
+ }
86
102
  return false;
87
103
  }
88
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transaction-manager.js","sourceRoot":"","sources":["../../src/lib/transaction-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transaction-manager.js","sourceRoot":"","sources":["../../src/lib/transaction-manager.ts"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ export declare class SyncPushPlugin extends BaseCLIPlugin {
24
24
  ci: Interfaces.BooleanFlag<boolean>;
25
25
  'no-validate': Interfaces.BooleanFlag<boolean>;
26
26
  'delete-db-only': Interfaces.BooleanFlag<boolean>;
27
+ 'isolated-transactions': Interfaces.BooleanFlag<boolean>;
27
28
  'parallel-batch-size': Interfaces.OptionFlag<number, Interfaces.CustomOptions>;
28
29
  include: Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
29
30
  exclude: Interfaces.OptionFlag<string, Interfaces.CustomOptions>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAyB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAqCpH,qBACa,cAAe,SAAQ,aAAa;IAC/C,MAAM,CAAC,WAAW,SAA6C;IAE/D,MAAM,CAAC,QAAQ,WAKb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;MAyBV;IAEF,MAAM,CAAC,KAAK,EAAE,WAAW,CAgBvB;cAEc,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;IAuK/C,0FAA0F;IAC1F,OAAO,CAAC,qBAAqB;IAqD7B,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;IAKjB;;;;OAIG;cACa,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAU5D;AAMD,qBACa,cAAe,SAAQ,aAAa;IAC/C,MAAM,CAAC,WAAW,SAAgD;IAElE,MAAM,CAAC,QAAQ,WAIb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;;;;;MAgBV;IAEF,MAAM,CAAC,KAAK,EAAE,WAAW,CAevB;cAEc,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;YAwJjC,qBAAqB;IAoBnC,OAAO,CAAC,iCAAiC;cAezB,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAU5D;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAS,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAGrD,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAyB,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAgEpH,qBACa,cAAe,SAAQ,aAAa;IAC/C,MAAM,CAAC,WAAW,SAA6C;IAE/D,MAAM,CAAC,QAAQ,WAKb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;MAgCV;IAEF,MAAM,CAAC,KAAK,EAAE,WAAW,CAkBvB;cAEc,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;IAkK/C,0FAA0F;IAC1F,OAAO,CAAC,qBAAqB;IAqD7B,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;IAKjB;;;;OAIG;cACa,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAU5D;AAMD,qBACa,cAAe,SAAQ,aAAa;IAC/C,MAAM,CAAC,WAAW,SAAgD;IAElE,MAAM,CAAC,QAAQ,WAIb;IAEF,MAAM,CAAC,KAAK;;;;;;;;;;;;;;;;MAgBV;IAEF,MAAM,CAAC,KAAK,EAAE,WAAW,CAevB;cAEc,OAAO,IAAI,OAAO,CAAC,WAAW,CAAC;YAwJjC,qBAAqB;IAoBnC,OAAO,CAAC,iCAAiC;cAezB,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAU5D;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
@@ -26,6 +26,8 @@ import { BaseCLIPlugin } from '@memberjunction/cli-core';
26
26
  import fs from 'fs';
27
27
  import path from 'path';
28
28
  import { writeFileSync } from 'fs';
29
+ // PushAbortedError lives in a dependency-free module, so importing it does not load the engine.
30
+ import { PushAbortedError } from '../lib/push-outcome.js';
29
31
  /** Shared loader for the heavy sync engine. Dynamic so the plugin module stays light. */
30
32
  async function loadEngine() {
31
33
  return import('../index.js');
@@ -43,6 +45,29 @@ function flushAndExit(code) {
43
45
  hardExit.unref();
44
46
  process.stdout.write('', () => process.exit(code));
45
47
  }
48
+ /**
49
+ * The `data` block for a failed push. `PushAbortedError` carries the counts the push reached and
50
+ * the SQL log path; anything else failed before the push started and has nothing to report.
51
+ */
52
+ function failureData(error, dryRun) {
53
+ if (!(error instanceof PushAbortedError)) {
54
+ return undefined;
55
+ }
56
+ const { totals } = error;
57
+ return {
58
+ created: totals.created,
59
+ updated: totals.updated,
60
+ unchanged: totals.unchanged,
61
+ deleted: totals.deleted,
62
+ skipped: totals.skipped,
63
+ deferred: totals.deferred,
64
+ errorCount: totals.errors,
65
+ dryRun,
66
+ sqlLogPath: error.sqlLogPath,
67
+ rolledBack: error.rolledBack,
68
+ committedOutsideTransaction: error.committedWrites.length,
69
+ };
70
+ }
46
71
  // ─────────────────────────────────────────────────────────────────────────────
47
72
  // mj sync push
48
73
  // ─────────────────────────────────────────────────────────────────────────────
@@ -63,9 +88,15 @@ let SyncPushPlugin = class SyncPushPlugin extends BaseCLIPlugin {
63
88
  description: 'Delete database-only records that reference records being deleted (prevents FK errors)',
64
89
  default: false,
65
90
  }),
91
+ 'isolated-transactions': Flags.boolean({
92
+ description: 'Give every JSON-root graph its own connection and transaction, so siblings are written in parallel and each ' +
93
+ 'create/update commits as it is saved (a failure does not roll those back). Off by default: the whole push runs ' +
94
+ 'in one transaction, one graph at a time. Overrides push.isolatedTransactions in every .mj-sync.json, in both ' +
95
+ 'directions — use --no-isolated-transactions to force one run back to a single transaction',
96
+ allowNo: true,
97
+ }),
66
98
  'parallel-batch-size': Flags.integer({
67
- description: 'JSON-root graphs to process in parallel (default: 10, not 1)',
68
- default: 10,
99
+ description: 'JSON-root graphs to process in parallel in a directory using isolated transactions (default: 10)',
69
100
  min: 1,
70
101
  max: 50,
71
102
  }),
@@ -91,6 +122,8 @@ let SyncPushPlugin = class SyncPushPlugin extends BaseCLIPlugin {
91
122
  { name: '--ci', type: 'boolean', description: 'No prompts; non-zero exit on error' },
92
123
  { name: '--no-validate', type: 'boolean', description: 'Skip pre-push validation' },
93
124
  { name: '--incremental', type: 'boolean', description: 'Skip unchanged files using stored checksums' },
125
+ { name: '--isolated-transactions', type: 'boolean', description: 'Per-graph connections; creates/updates commit as they go and are not rolled back' },
126
+ { name: '--parallel-batch-size', type: 'number', description: 'Graphs at once in an isolated directory (default 10)' },
94
127
  { name: '--format', type: 'text|json|md', description: 'Output format (json for machine-readable result)' },
95
128
  ],
96
129
  examples: ['mj sync push --dir=ai-agents', 'mj sync push --ci --format=json'],
@@ -164,6 +197,7 @@ let SyncPushPlugin = class SyncPushPlugin extends BaseCLIPlugin {
164
197
  noValidate: flags['no-validate'],
165
198
  deleteDbOnly: flags['delete-db-only'],
166
199
  parallelBatchSize: flags['parallel-batch-size'],
200
+ isolatedTransactions: flags['isolated-transactions'],
167
201
  include: includeFilter,
168
202
  exclude: excludeFilter,
169
203
  incremental: flags.incremental,
@@ -198,20 +232,9 @@ let SyncPushPlugin = class SyncPushPlugin extends BaseCLIPlugin {
198
232
  for (const w of result.warnings)
199
233
  if (!warnings.includes(w))
200
234
  warnings.push(w);
201
- // Recovery decision: with non-fatal errors, the original CLI asked whether to
202
- // keep the successfully-committed changes. Preserve that for interactive text
203
- // mode. CI / non-interactive keeps the failure (no prompt possible).
204
- let success = result.errors === 0 && errors.length === 0;
205
- if (!success && result.errors > 0 && !flags.ci && isText) {
206
- const commit = await confirm({
207
- message: 'Push completed with errors. Do you want to commit the successful changes?',
208
- default: false,
209
- });
210
- if (commit)
211
- success = true;
212
- else
213
- warnings.push('Push cancelled due to errors.');
214
- }
235
+ // A push that fails rolls back and throws (see the catch below), so a result with
236
+ // errors only comes from a dry run. There is nothing to commit, so no prompt.
237
+ const success = result.errors === 0 && errors.length === 0;
215
238
  if (isText) {
216
239
  this.renderPushTextSummary(formatter, result, flags['change-detail'], startTime, endTime);
217
240
  }
@@ -246,7 +269,10 @@ let SyncPushPlugin = class SyncPushPlugin extends BaseCLIPlugin {
246
269
  this.Host.FailStep('Push failed', message);
247
270
  if (errors.length === 0)
248
271
  errors.push({ message });
249
- return { success: false, command: 'sync:push', durationSeconds: (Date.now() - startTime) / 1000, errors, warnings };
272
+ // A failed push still reports what it did. Without this the JSON consumer loses the counts
273
+ // and the SQL log path on exactly the runs that need them.
274
+ const data = failureData(error, flags['dry-run'] === true);
275
+ return { success: false, command: 'sync:push', durationSeconds: (Date.now() - startTime) / 1000, data, errors, warnings };
250
276
  }
251
277
  }
252
278
  /** Reproduces the original rich text summary (recap, change-detail file, box, status). */