@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.
- package/README.md +88 -25
- package/dist/config.d.ts +42 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/EntityPropertyExtractor.d.ts +16 -0
- package/dist/lib/EntityPropertyExtractor.d.ts.map +1 -1
- package/dist/lib/EntityPropertyExtractor.js +34 -0
- package/dist/lib/EntityPropertyExtractor.js.map +1 -1
- package/dist/lib/FieldExternalizer.d.ts +1 -2
- package/dist/lib/FieldExternalizer.d.ts.map +1 -1
- package/dist/lib/FieldExternalizer.js.map +1 -1
- package/dist/lib/RecordProcessor.d.ts +7 -0
- package/dist/lib/RecordProcessor.d.ts.map +1 -1
- package/dist/lib/RecordProcessor.js +27 -1
- package/dist/lib/RecordProcessor.js.map +1 -1
- package/dist/lib/existing-record-files.d.ts +12 -0
- package/dist/lib/existing-record-files.d.ts.map +1 -0
- package/dist/lib/existing-record-files.js +85 -0
- package/dist/lib/existing-record-files.js.map +1 -0
- package/dist/lib/file-backup-manager.d.ts +10 -0
- package/dist/lib/file-backup-manager.d.ts.map +1 -1
- package/dist/lib/file-backup-manager.js +17 -0
- package/dist/lib/file-backup-manager.js.map +1 -1
- package/dist/lib/file-write-batch.d.ts +0 -4
- package/dist/lib/file-write-batch.d.ts.map +1 -1
- package/dist/lib/file-write-batch.js +10 -8
- package/dist/lib/file-write-batch.js.map +1 -1
- package/dist/lib/graph-provider-pool.d.ts +62 -33
- package/dist/lib/graph-provider-pool.d.ts.map +1 -1
- package/dist/lib/graph-provider-pool.js +131 -79
- package/dist/lib/graph-provider-pool.js.map +1 -1
- package/dist/lib/json-subproperty-externalization.d.ts +77 -0
- package/dist/lib/json-subproperty-externalization.d.ts.map +1 -0
- package/dist/lib/json-subproperty-externalization.js +139 -0
- package/dist/lib/json-subproperty-externalization.js.map +1 -0
- package/dist/lib/push-outcome.d.ts +66 -0
- package/dist/lib/push-outcome.d.ts.map +1 -0
- package/dist/lib/push-outcome.js +81 -0
- package/dist/lib/push-outcome.js.map +1 -0
- package/dist/lib/push-write-mode.d.ts +47 -0
- package/dist/lib/push-write-mode.d.ts.map +1 -0
- package/dist/lib/push-write-mode.js +53 -0
- package/dist/lib/push-write-mode.js.map +1 -0
- package/dist/lib/record-primary-key.d.ts +35 -0
- package/dist/lib/record-primary-key.d.ts.map +1 -0
- package/dist/lib/record-primary-key.js +44 -0
- package/dist/lib/record-primary-key.js.map +1 -0
- package/dist/lib/transaction-manager.d.ts +11 -1
- package/dist/lib/transaction-manager.d.ts.map +1 -1
- package/dist/lib/transaction-manager.js +20 -4
- package/dist/lib/transaction-manager.js.map +1 -1
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +43 -17
- package/dist/plugins/index.js.map +1 -1
- package/dist/services/PullService.d.ts +1 -3
- package/dist/services/PullService.d.ts.map +1 -1
- package/dist/services/PullService.js +28 -68
- package/dist/services/PullService.js.map +1 -1
- package/dist/services/PushService.d.ts +109 -0
- package/dist/services/PushService.d.ts.map +1 -1
- package/dist/services/PushService.js +631 -319
- package/dist/services/PushService.js.map +1 -1
- 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":"AAAA;;;GAGG;AAEH,OAAO,IAAI,MAAM,MAAM,CAAC;AAsCxB;;;;GAIG;AACH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAQzC,YAAY,OAA2B;QACrC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;QAC/B,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;QAC/C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACvC,CAAC;IAED,qEAAqE;IACrE,IAAI,gBAAgB;QAClB,OAAO,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC;IAC9D,CAAC;CACF;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAmB,EAAE,eAAiC,EAAE,GAAW;IACzG,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,mFAAmF;gBACjF,2DAA2D;YAC7D,GAAG,uBAAuB,CAAC,eAAe,EAAE,GAAG,CAAC;SACjD,CAAC;IACJ,CAAC;IACD,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,oFAAoF,CAAC,CAAC;IAChG,CAAC;IACD,OAAO;QACL,iDAAiD,eAAe,CAAC,MAAM,sBAAsB;YAC3F,SAAS,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,8CAA8C;YAC9F,GAAG,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,yBAAyB;YACzE,GAAG,eAAe,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,+DAA+D;QAC/G,GAAG,uBAAuB,CAAC,eAAe,EAAE,GAAG,CAAC;KACjD,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,uBAAuB,CAAC,eAAiC,EAAE,GAAW;IACpF,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAC;IACnD,KAAK,MAAM,KAAK,IAAI,eAAe,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjB,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,QAAQ,KAAK,MAAM,CAAC,MAAM,YAAY,CAAC,CAAC;QACzF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,UAAU,OAAO,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;QACjF,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc,EAAE,QAA4B;IAChF,MAAM,IAAI,GAAG,+FAA+F,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;IAC/H,IAAI,QAAQ,KAAK,YAAY,EAAE,CAAC;QAC9B,OAAO,CACL,GAAG,IAAI,qFAAqF;YAC5F,qEAAqE,CACtE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -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
|
-
|
|
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;
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"transaction-manager.js","sourceRoot":"","sources":["../../src/lib/transaction-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/plugins/index.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
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
|
-
|
|
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). */
|