@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
|
@@ -1,26 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provider assignment per JSON-root graph for `mj sync push`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* so
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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,
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
125
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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":"AAAA
|
|
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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Externalizing a property *inside* a JSON field.
|
|
3
|
+
*
|
|
4
|
+
* `pull.externalizeFields` names entity fields, so it can move a whole column into
|
|
5
|
+
* a side file (`TemplateText` → `templates/x.md`) but cannot reach a single property
|
|
6
|
+
* inside a JSON column. That is the wrong granularity whenever a column mixes
|
|
7
|
+
* hand-authored config with a machine-generated artifact: MJ Tests keep step budgets,
|
|
8
|
+
* oracles and the recorded `ReplayScript` in one `Configuration` blob, and
|
|
9
|
+
* externalizing the whole thing would bury the readable half.
|
|
10
|
+
*
|
|
11
|
+
* A dotted config field (`Configuration.ReplayScript`) externalizes just the leaf and
|
|
12
|
+
* leaves an `@file:` reference in its place. Push already resolves nested references
|
|
13
|
+
* when it walks a metadata object, so the value round-trips with no push-side change.
|
|
14
|
+
*
|
|
15
|
+
* The file IO stays in `FieldExternalizer` — it is injected here as `externalize`, so
|
|
16
|
+
* this module is only the JSON walk: find the leaf, hand it over, splice the reference
|
|
17
|
+
* back. Pattern placeholders, path preservation and the unchanged-content skip all come
|
|
18
|
+
* from that existing code.
|
|
19
|
+
*/
|
|
20
|
+
/** A sub-property externalization resolved against one field. */
|
|
21
|
+
export interface SubPropertyExternalization {
|
|
22
|
+
/** Property path within the field's JSON, e.g. `['ReplayScript']`. */
|
|
23
|
+
path: string[];
|
|
24
|
+
/** The `@file:` pattern to externalize to. */
|
|
25
|
+
pattern: string;
|
|
26
|
+
}
|
|
27
|
+
/** Writes a value to a file and returns the `@file:` reference naming it. */
|
|
28
|
+
export type ExternalizeLeaf = (value: unknown, pattern: string, existingRef?: string, leafName?: string) => Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* The one `FieldExternalizer` method this module needs. `recordData` is declared as a
|
|
31
|
+
* property bag rather than `BaseEntity` because that is all it is ever used for —
|
|
32
|
+
* `RecordProcessor` hands the pulled properties straight through, and the externalizer
|
|
33
|
+
* only reads `Name` / `ID` / field placeholders off it to build the filename.
|
|
34
|
+
*/
|
|
35
|
+
export interface FieldExternalizerLike {
|
|
36
|
+
externalizeField(fieldName: string, fieldValue: unknown, pattern: string, recordData: Record<string, unknown>, targetDir: string, existingFileReference?: string, mergeStrategy?: string, verbose?: boolean): Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Split an `externalizeFields` entry into the entity field it targets and the property
|
|
40
|
+
* path within that field. A name with no dot yields an empty path, which is how callers
|
|
41
|
+
* tell a whole-field config (the existing behavior) from a sub-property one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseExternalizePath(configField: string): {
|
|
44
|
+
field: string;
|
|
45
|
+
path: string[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The sub-property externalizations rooted at `fieldName`. Whole-field entries are
|
|
49
|
+
* excluded so they continue down the original code path untouched, and the legacy
|
|
50
|
+
* string-array config form yields nothing because it can only name whole fields.
|
|
51
|
+
*/
|
|
52
|
+
export declare function findSubPropertyExternalizations(fieldName: string, externalizeConfig: unknown): SubPropertyExternalization[];
|
|
53
|
+
/** Read the value at `path`, or `undefined` if any step is missing or not an object. */
|
|
54
|
+
export declare function getAtPath(value: unknown, path: string[]): unknown;
|
|
55
|
+
/**
|
|
56
|
+
* Bind a `FieldExternalizer` into the `ExternalizeLeaf` shape this module walks with.
|
|
57
|
+
*
|
|
58
|
+
* The serialization here is load-bearing. `FieldExternalizer` writes `String(value)` and
|
|
59
|
+
* only pretty-prints when the field name hints at JSON, so handing it a parsed object
|
|
60
|
+
* would write the literal text `[object Object]`. Serializing first gives a diffable file
|
|
61
|
+
* and a stable string for the unchanged-content comparison that skips redundant writes.
|
|
62
|
+
*/
|
|
63
|
+
export declare function fieldExternalizerAdapter(externalizer: FieldExternalizerLike, recordProperties: Record<string, unknown>, targetDir: string, mergeStrategy?: string, verbose?: boolean): ExternalizeLeaf;
|
|
64
|
+
/**
|
|
65
|
+
* Externalize each configured sub-property of `fieldValue`, returning the field value
|
|
66
|
+
* with `@file:` references in their place.
|
|
67
|
+
*
|
|
68
|
+
* A path the value does not carry is skipped entirely — no file, no key — so a record
|
|
69
|
+
* that never produced the artifact stays byte-identical. `existingFieldValue` is the
|
|
70
|
+
* same field as it currently stands in the metadata file; the reference found at each
|
|
71
|
+
* path is forwarded so `FieldExternalizer` can keep writing to the path already chosen
|
|
72
|
+
* there instead of relocating the file to the pattern's default.
|
|
73
|
+
*
|
|
74
|
+
* Returns the input unchanged when nothing is configured or the value is not JSON.
|
|
75
|
+
*/
|
|
76
|
+
export declare function externalizeSubProperties(fieldValue: unknown, configs: SubPropertyExternalization[], externalize: ExternalizeLeaf, existingFieldValue?: unknown): Promise<unknown>;
|
|
77
|
+
//# sourceMappingURL=json-subproperty-externalization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-subproperty-externalization.d.ts","sourceRoot":"","sources":["../../src/lib/json-subproperty-externalization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,iEAAiE;AACjE,MAAM,WAAW,0BAA0B;IACvC,sEAAsE;IACtE,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,6EAA6E;AAC7E,MAAM,MAAM,eAAe,GAAG,CAC1B,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE,MAAM,KAChB,OAAO,CAAC,MAAM,CAAC,CAAC;AAErB;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IAClC,gBAAgB,CACZ,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,OAAO,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,SAAS,EAAE,MAAM,EACjB,qBAAqB,CAAC,EAAE,MAAM,EAC9B,aAAa,CAAC,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,OAAO,GAClB,OAAO,CAAC,MAAM,CAAC,CAAC;CACtB;AAMD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAG3F;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC3C,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,OAAO,GAC3B,0BAA0B,EAAE,CAe9B;AAED,wFAAwF;AACxF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CASjE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACpC,YAAY,EAAE,qBAAqB,EACnC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzC,SAAS,EAAE,MAAM,EACjB,aAAa,GAAE,MAAgB,EAC/B,OAAO,CAAC,EAAE,OAAO,GAClB,eAAe,CAcjB;AA+BD;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAC1C,UAAU,EAAE,OAAO,EACnB,OAAO,EAAE,0BAA0B,EAAE,EACrC,WAAW,EAAE,eAAe,EAC5B,kBAAkB,CAAC,EAAE,OAAO,GAC7B,OAAO,CAAC,OAAO,CAAC,CA4BlB"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Externalizing a property *inside* a JSON field.
|
|
3
|
+
*
|
|
4
|
+
* `pull.externalizeFields` names entity fields, so it can move a whole column into
|
|
5
|
+
* a side file (`TemplateText` → `templates/x.md`) but cannot reach a single property
|
|
6
|
+
* inside a JSON column. That is the wrong granularity whenever a column mixes
|
|
7
|
+
* hand-authored config with a machine-generated artifact: MJ Tests keep step budgets,
|
|
8
|
+
* oracles and the recorded `ReplayScript` in one `Configuration` blob, and
|
|
9
|
+
* externalizing the whole thing would bury the readable half.
|
|
10
|
+
*
|
|
11
|
+
* A dotted config field (`Configuration.ReplayScript`) externalizes just the leaf and
|
|
12
|
+
* leaves an `@file:` reference in its place. Push already resolves nested references
|
|
13
|
+
* when it walks a metadata object, so the value round-trips with no push-side change.
|
|
14
|
+
*
|
|
15
|
+
* The file IO stays in `FieldExternalizer` — it is injected here as `externalize`, so
|
|
16
|
+
* this module is only the JSON walk: find the leaf, hand it over, splice the reference
|
|
17
|
+
* back. Pattern placeholders, path preservation and the unchanged-content skip all come
|
|
18
|
+
* from that existing code.
|
|
19
|
+
*/
|
|
20
|
+
function isPlainObject(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Split an `externalizeFields` entry into the entity field it targets and the property
|
|
25
|
+
* path within that field. A name with no dot yields an empty path, which is how callers
|
|
26
|
+
* tell a whole-field config (the existing behavior) from a sub-property one.
|
|
27
|
+
*/
|
|
28
|
+
export function parseExternalizePath(configField) {
|
|
29
|
+
const [field, ...path] = configField.split('.');
|
|
30
|
+
return { field, path };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The sub-property externalizations rooted at `fieldName`. Whole-field entries are
|
|
34
|
+
* excluded so they continue down the original code path untouched, and the legacy
|
|
35
|
+
* string-array config form yields nothing because it can only name whole fields.
|
|
36
|
+
*/
|
|
37
|
+
export function findSubPropertyExternalizations(fieldName, externalizeConfig) {
|
|
38
|
+
if (!Array.isArray(externalizeConfig)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const found = [];
|
|
42
|
+
for (const entry of externalizeConfig) {
|
|
43
|
+
if (!isPlainObject(entry) || typeof entry.field !== 'string' || typeof entry.pattern !== 'string') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const { field, path } = parseExternalizePath(entry.field);
|
|
47
|
+
if (path.length > 0 && field === fieldName) {
|
|
48
|
+
found.push({ path, pattern: entry.pattern });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
/** Read the value at `path`, or `undefined` if any step is missing or not an object. */
|
|
54
|
+
export function getAtPath(value, path) {
|
|
55
|
+
let cursor = value;
|
|
56
|
+
for (const key of path) {
|
|
57
|
+
if (!isPlainObject(cursor)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
cursor = cursor[key];
|
|
61
|
+
}
|
|
62
|
+
return cursor;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bind a `FieldExternalizer` into the `ExternalizeLeaf` shape this module walks with.
|
|
66
|
+
*
|
|
67
|
+
* The serialization here is load-bearing. `FieldExternalizer` writes `String(value)` and
|
|
68
|
+
* only pretty-prints when the field name hints at JSON, so handing it a parsed object
|
|
69
|
+
* would write the literal text `[object Object]`. Serializing first gives a diffable file
|
|
70
|
+
* and a stable string for the unchanged-content comparison that skips redundant writes.
|
|
71
|
+
*/
|
|
72
|
+
export function fieldExternalizerAdapter(externalizer, recordProperties, targetDir, mergeStrategy = 'merge', verbose) {
|
|
73
|
+
return async (value, pattern, existingRef, leafName) => {
|
|
74
|
+
const serialized = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
|
|
75
|
+
return externalizer.externalizeField(leafName ?? '', serialized, pattern, recordProperties, targetDir, existingRef, mergeStrategy, verbose);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** A copy of `root` with `path` set to `leaf`. Untouched branches are shared, not cloned. */
|
|
79
|
+
function withLeaf(root, path, leaf) {
|
|
80
|
+
const [head, ...rest] = path;
|
|
81
|
+
if (rest.length === 0) {
|
|
82
|
+
return { ...root, [head]: leaf };
|
|
83
|
+
}
|
|
84
|
+
const child = root[head];
|
|
85
|
+
if (!isPlainObject(child)) {
|
|
86
|
+
return root;
|
|
87
|
+
}
|
|
88
|
+
return { ...root, [head]: withLeaf(child, rest, leaf) };
|
|
89
|
+
}
|
|
90
|
+
/** JSON columns arrive as a string from the database and as an object from a metadata file. */
|
|
91
|
+
function coerceToObject(value) {
|
|
92
|
+
if (isPlainObject(value)) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value === 'string') {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(value);
|
|
98
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Externalize each configured sub-property of `fieldValue`, returning the field value
|
|
108
|
+
* with `@file:` references in their place.
|
|
109
|
+
*
|
|
110
|
+
* A path the value does not carry is skipped entirely — no file, no key — so a record
|
|
111
|
+
* that never produced the artifact stays byte-identical. `existingFieldValue` is the
|
|
112
|
+
* same field as it currently stands in the metadata file; the reference found at each
|
|
113
|
+
* path is forwarded so `FieldExternalizer` can keep writing to the path already chosen
|
|
114
|
+
* there instead of relocating the file to the pattern's default.
|
|
115
|
+
*
|
|
116
|
+
* Returns the input unchanged when nothing is configured or the value is not JSON.
|
|
117
|
+
*/
|
|
118
|
+
export async function externalizeSubProperties(fieldValue, configs, externalize, existingFieldValue) {
|
|
119
|
+
if (!configs || configs.length === 0) {
|
|
120
|
+
return fieldValue;
|
|
121
|
+
}
|
|
122
|
+
const parsed = coerceToObject(fieldValue);
|
|
123
|
+
if (!parsed) {
|
|
124
|
+
return fieldValue;
|
|
125
|
+
}
|
|
126
|
+
const existing = coerceToObject(existingFieldValue);
|
|
127
|
+
let result = parsed;
|
|
128
|
+
for (const { path, pattern } of configs) {
|
|
129
|
+
const leaf = getAtPath(result, path);
|
|
130
|
+
if (leaf === undefined || leaf === null) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const existingRef = existing ? getAtPath(existing, path) : undefined;
|
|
134
|
+
const reference = await externalize(leaf, pattern, typeof existingRef === 'string' ? existingRef : undefined, path[path.length - 1]);
|
|
135
|
+
result = withLeaf(result, path, reference);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=json-subproperty-externalization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-subproperty-externalization.js","sourceRoot":"","sources":["../../src/lib/json-subproperty-externalization.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"}
|