@fakeware/core 0.0.5 → 0.0.7
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/dist/config/index.d.mts +2 -52
- package/dist/config/index.mjs +2 -36
- package/dist/config-DGneBZWH.mjs +133 -0
- package/dist/config-DGneBZWH.mjs.map +1 -0
- package/dist/index-Brciwig_.d.mts +62 -0
- package/dist/index-jYm7NShY.d.mts +68 -0
- package/dist/index.d.mts +144 -0
- package/dist/index.mjs +610 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shopware/index.d.mts +2 -27
- package/dist/shopware/index.mjs +2 -94
- package/dist/shopware-CIZF8Nuo.mjs +204 -0
- package/dist/shopware-CIZF8Nuo.mjs.map +1 -0
- package/package.json +15 -1
- package/dist/config/index.mjs.map +0 -1
- package/dist/shopware/index.mjs.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { l as LoadModuleError, s as ConfigError, u as loadModule } from "./config-DGneBZWH.mjs";
|
|
2
|
+
import { r as estimateSyncBytes } from "./shopware-CIZF8Nuo.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
//#region src/define/errors.ts
|
|
7
|
+
var RefError = class extends Error {};
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/define/is-plain-object.ts
|
|
10
|
+
function isPlainObject(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/define/ids.ts
|
|
15
|
+
const FAKEWARE_NAMESPACE = "b1e0a3f4-6c2d-5a8b-9e7f-0d1c2b3a4e5f";
|
|
16
|
+
function uuidBytes(uuid) {
|
|
17
|
+
const hex = uuid.replace(/-/g, "");
|
|
18
|
+
const bytes = new Uint8Array(16);
|
|
19
|
+
for (let i = 0; i < 16; i++) bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
const NAMESPACE_BYTES = uuidBytes(FAKEWARE_NAMESPACE);
|
|
23
|
+
function uuidv5(name) {
|
|
24
|
+
const hash = createHash("sha1");
|
|
25
|
+
hash.update(NAMESPACE_BYTES);
|
|
26
|
+
hash.update(name, "utf8");
|
|
27
|
+
const bytes = hash.digest().subarray(0, 16);
|
|
28
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
29
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
30
|
+
return bytes.toString("hex");
|
|
31
|
+
}
|
|
32
|
+
function deterministicId(entity, key) {
|
|
33
|
+
return uuidv5(`${entity}:${key}`);
|
|
34
|
+
}
|
|
35
|
+
function canonicalize(value) {
|
|
36
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
37
|
+
if (isPlainObject(value)) {
|
|
38
|
+
const sorted = {};
|
|
39
|
+
for (const k of Object.keys(value).sort()) sorted[k] = canonicalize(value[k]);
|
|
40
|
+
return sorted;
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function recordHash(payload) {
|
|
45
|
+
return createHash("sha256").update(JSON.stringify(canonicalize(payload))).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/define/registry.ts
|
|
49
|
+
let entries = [];
|
|
50
|
+
function resetRegistry() {
|
|
51
|
+
entries = [];
|
|
52
|
+
}
|
|
53
|
+
function staticKey(value) {
|
|
54
|
+
if (typeof value !== "function") {
|
|
55
|
+
const k = value.$key;
|
|
56
|
+
if (typeof k === "string") return k;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function defineRecords(entity, recordOrRecords) {
|
|
60
|
+
const list = Array.isArray(recordOrRecords) ? recordOrRecords : [recordOrRecords];
|
|
61
|
+
for (const value of list) entries.push({
|
|
62
|
+
entity,
|
|
63
|
+
key: staticKey(value),
|
|
64
|
+
value
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function drain() {
|
|
68
|
+
const order = [];
|
|
69
|
+
const byEntity = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const e of entries) {
|
|
71
|
+
let bucket = byEntity.get(e.entity);
|
|
72
|
+
if (!bucket) {
|
|
73
|
+
bucket = [];
|
|
74
|
+
byEntity.set(e.entity, bucket);
|
|
75
|
+
order.push(e.entity);
|
|
76
|
+
}
|
|
77
|
+
bucket.push(e);
|
|
78
|
+
}
|
|
79
|
+
return order.map((entity) => ({
|
|
80
|
+
entity,
|
|
81
|
+
entries: byEntity.get(entity)
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function buildRefIndex(drained) {
|
|
85
|
+
const refIndex = { byEntity: /* @__PURE__ */ new Map() };
|
|
86
|
+
const ids = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const { entity, entries: bucket } of drained) {
|
|
88
|
+
const slot = {
|
|
89
|
+
byKey: /* @__PURE__ */ new Map(),
|
|
90
|
+
all: []
|
|
91
|
+
};
|
|
92
|
+
refIndex.byEntity.set(entity, slot);
|
|
93
|
+
bucket.forEach((entry, i) => {
|
|
94
|
+
const id = deterministicId(entity, entry.key ?? String(i));
|
|
95
|
+
ids.set(entry, id);
|
|
96
|
+
slot.all.push(id);
|
|
97
|
+
if (entry.key) slot.byKey.set(entry.key, id);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
refIndex,
|
|
102
|
+
ids
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/define/define.ts
|
|
107
|
+
function define(entity, records) {
|
|
108
|
+
defineRecords(entity, records);
|
|
109
|
+
}
|
|
110
|
+
function many(n, fn) {
|
|
111
|
+
return Array.from({ length: n }, () => fn);
|
|
112
|
+
}
|
|
113
|
+
let active;
|
|
114
|
+
function setActiveRefIndex(refIndex) {
|
|
115
|
+
active = refIndex;
|
|
116
|
+
}
|
|
117
|
+
function requireActive() {
|
|
118
|
+
if (!active) throw new RefError("ref()/refs() may only be called while resolving definitions.");
|
|
119
|
+
return active;
|
|
120
|
+
}
|
|
121
|
+
function ref(path) {
|
|
122
|
+
const slash = path.indexOf("/");
|
|
123
|
+
if (slash === -1) throw new RefError(`ref('${path}') must be of the form 'entity/key'.`);
|
|
124
|
+
const entity = path.slice(0, slash);
|
|
125
|
+
const key = path.slice(slash + 1);
|
|
126
|
+
const id = requireActive().byEntity.get(entity)?.byKey.get(key);
|
|
127
|
+
if (!id) throw new RefError(`ref('${path}') does not match any defined record.`);
|
|
128
|
+
return id;
|
|
129
|
+
}
|
|
130
|
+
function refs(entity) {
|
|
131
|
+
const slot = requireActive().byEntity.get(entity);
|
|
132
|
+
if (!slot) throw new RefError(`refs('${entity}') does not match any defined entity.`);
|
|
133
|
+
return [...slot.all];
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/define/resolve.ts
|
|
137
|
+
function resolveValue(value, ctx) {
|
|
138
|
+
if (typeof value === "function") return resolveValue(value(ctx), ctx);
|
|
139
|
+
if (Array.isArray(value)) return value.map((item) => resolveValue(item, ctx));
|
|
140
|
+
if (isPlainObject(value)) {
|
|
141
|
+
const out = {};
|
|
142
|
+
for (const [key, v] of Object.entries(value)) {
|
|
143
|
+
if (key === "$key") continue;
|
|
144
|
+
out[key] = resolveValue(v, ctx);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/engine/errors.ts
|
|
152
|
+
var GraphError = class extends Error {};
|
|
153
|
+
var TransactionError = class extends Error {
|
|
154
|
+
rolledBack;
|
|
155
|
+
failedEntity;
|
|
156
|
+
unrevertableUpdates;
|
|
157
|
+
compensationErrors;
|
|
158
|
+
constructor(message, options) {
|
|
159
|
+
super(message, { cause: options.cause });
|
|
160
|
+
this.name = "TransactionError";
|
|
161
|
+
this.rolledBack = options.rolledBack;
|
|
162
|
+
this.failedEntity = options.failedEntity;
|
|
163
|
+
this.unrevertableUpdates = options.unrevertableUpdates ?? false;
|
|
164
|
+
this.compensationErrors = options.compensationErrors ?? [];
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/engine/build-graph.ts
|
|
169
|
+
function ownerByIdOf(refIndex) {
|
|
170
|
+
const owner = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const [entity, slot] of refIndex.byEntity) for (const id of slot.all) owner.set(id, entity);
|
|
172
|
+
return owner;
|
|
173
|
+
}
|
|
174
|
+
function collectIdRefs(value, ownerById, into) {
|
|
175
|
+
if (typeof value === "string") {
|
|
176
|
+
const owner = ownerById.get(value);
|
|
177
|
+
if (owner) into.add(owner);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (Array.isArray(value)) {
|
|
181
|
+
for (const item of value) collectIdRefs(item, ownerById, into);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (isPlainObject(value)) for (const v of Object.values(value)) collectIdRefs(v, ownerById, into);
|
|
185
|
+
}
|
|
186
|
+
function topoSort(entities, edges) {
|
|
187
|
+
const indegree = new Map(entities.map((e) => [e, 0]));
|
|
188
|
+
const dependents = new Map(entities.map((e) => [e, []]));
|
|
189
|
+
for (const [entity, deps] of edges) for (const dep of deps) {
|
|
190
|
+
indegree.set(entity, (indegree.get(entity) ?? 0) + 1);
|
|
191
|
+
dependents.get(dep)?.push(entity);
|
|
192
|
+
}
|
|
193
|
+
const queue = entities.filter((e) => (indegree.get(e) ?? 0) === 0);
|
|
194
|
+
const ordered = [];
|
|
195
|
+
while (queue.length > 0) {
|
|
196
|
+
const entity = queue.shift();
|
|
197
|
+
ordered.push(entity);
|
|
198
|
+
for (const dependent of dependents.get(entity) ?? []) {
|
|
199
|
+
const next = (indegree.get(dependent) ?? 0) - 1;
|
|
200
|
+
indegree.set(dependent, next);
|
|
201
|
+
if (next === 0) queue.push(dependent);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (ordered.length !== entities.length) throw new GraphError(`Reference cycle between entities: ${entities.filter((e) => !ordered.includes(e)).join(", ")}.`);
|
|
205
|
+
return ordered;
|
|
206
|
+
}
|
|
207
|
+
function buildWritePlan(drained) {
|
|
208
|
+
const { refIndex, ids } = buildRefIndex(drained);
|
|
209
|
+
const ownerById = ownerByIdOf(refIndex);
|
|
210
|
+
const entities = drained.map((d) => d.entity);
|
|
211
|
+
const records = /* @__PURE__ */ new Map();
|
|
212
|
+
const edges = new Map(entities.map((e) => [e, /* @__PURE__ */ new Set()]));
|
|
213
|
+
setActiveRefIndex(refIndex);
|
|
214
|
+
try {
|
|
215
|
+
for (const { entity, entries } of drained) {
|
|
216
|
+
const out = [];
|
|
217
|
+
entries.forEach((entry, i) => {
|
|
218
|
+
const ctx = {
|
|
219
|
+
index: i,
|
|
220
|
+
count: entries.length,
|
|
221
|
+
ref,
|
|
222
|
+
refs
|
|
223
|
+
};
|
|
224
|
+
const payload = resolveValue(entry.value, ctx);
|
|
225
|
+
const id = ids.get(entry);
|
|
226
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
227
|
+
collectIdRefs(payload, ownerById, referenced);
|
|
228
|
+
for (const dep of referenced) if (dep !== entity) edges.get(entity)?.add(dep);
|
|
229
|
+
out.push({
|
|
230
|
+
...payload,
|
|
231
|
+
id
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
records.set(entity, out);
|
|
235
|
+
}
|
|
236
|
+
} finally {
|
|
237
|
+
setActiveRefIndex(void 0);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
order: topoSort(entities, edges),
|
|
241
|
+
records
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/engine/discover.ts
|
|
246
|
+
const DATA_DIR = "data";
|
|
247
|
+
function isDataFile(name) {
|
|
248
|
+
return name.endsWith(".ts") && !name.endsWith(".test.ts") && !name.endsWith(".d.ts");
|
|
249
|
+
}
|
|
250
|
+
async function discoverDataFiles(projectRoot) {
|
|
251
|
+
const root = join(projectRoot, DATA_DIR);
|
|
252
|
+
let names;
|
|
253
|
+
try {
|
|
254
|
+
names = await readdir(root, { recursive: true });
|
|
255
|
+
} catch {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
return names.filter(isDataFile).sort().map((name) => join(root, name));
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/engine/evaluate.ts
|
|
262
|
+
async function evaluateDataFiles(files) {
|
|
263
|
+
resetRegistry();
|
|
264
|
+
for (const file of files) await loadModule(file);
|
|
265
|
+
return drain();
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region src/engine/manifest.ts
|
|
269
|
+
const MANIFEST_DIR = ".fakeware";
|
|
270
|
+
const CURRENT_VERSION = 1;
|
|
271
|
+
function shopKey(shopwareUrl) {
|
|
272
|
+
return createHash("sha256").update(shopwareUrl).digest("hex").slice(0, 16);
|
|
273
|
+
}
|
|
274
|
+
function manifestPath(projectRoot, shopwareUrl) {
|
|
275
|
+
return join(projectRoot, MANIFEST_DIR, `${shopKey(shopwareUrl)}.json`);
|
|
276
|
+
}
|
|
277
|
+
function checksumOf(entities) {
|
|
278
|
+
const canonical = entities.map((e) => ({
|
|
279
|
+
entity: e.entity,
|
|
280
|
+
records: [...e.records].sort((a, b) => a.id.localeCompare(b.id))
|
|
281
|
+
})).sort((a, b) => a.entity.localeCompare(b.entity));
|
|
282
|
+
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
283
|
+
}
|
|
284
|
+
function buildManifest(input) {
|
|
285
|
+
return {
|
|
286
|
+
version: CURRENT_VERSION,
|
|
287
|
+
fakewareVersion: input.fakewareVersion,
|
|
288
|
+
createdAt: input.createdAt,
|
|
289
|
+
shopwareUrl: input.shopwareUrl,
|
|
290
|
+
entities: input.entities,
|
|
291
|
+
checksum: checksumOf(input.entities)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function readManifest(projectRoot, shopwareUrl) {
|
|
295
|
+
const path = manifestPath(projectRoot, shopwareUrl);
|
|
296
|
+
let contents;
|
|
297
|
+
try {
|
|
298
|
+
contents = await readFile(path, "utf8");
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const parsed = JSON.parse(contents);
|
|
303
|
+
switch (parsed.version) {
|
|
304
|
+
case CURRENT_VERSION: {
|
|
305
|
+
const manifest = parsed;
|
|
306
|
+
if (checksumOf(manifest.entities) !== manifest.checksum) throw new ConfigError(`Manifest at ${path} is corrupt (checksum mismatch). Re-run \`fakeware up\`.`);
|
|
307
|
+
return manifest;
|
|
308
|
+
}
|
|
309
|
+
default: throw new ConfigError(`Unsupported manifest version ${parsed.version} (this CLI understands up to ${CURRENT_VERSION}). Upgrade fakeware.`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function writeManifest(projectRoot, manifest) {
|
|
313
|
+
const path = manifestPath(projectRoot, manifest.shopwareUrl);
|
|
314
|
+
await mkdir(dirname(path), { recursive: true });
|
|
315
|
+
await writeFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
316
|
+
}
|
|
317
|
+
async function removeManifest(projectRoot, shopwareUrl) {
|
|
318
|
+
await rm(manifestPath(projectRoot, shopwareUrl), { force: true });
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/engine/run.ts
|
|
322
|
+
function priorHashes(manifest) {
|
|
323
|
+
const map = /* @__PURE__ */ new Map();
|
|
324
|
+
for (const e of manifest?.entities ?? []) map.set(e.entity, new Map(e.records.map((r) => [r.id, r.hash])));
|
|
325
|
+
return map;
|
|
326
|
+
}
|
|
327
|
+
function resolveTransaction(opts) {
|
|
328
|
+
return opts.transaction ?? opts.loaded.config.transaction;
|
|
329
|
+
}
|
|
330
|
+
function diffEntity(entity, records, prior) {
|
|
331
|
+
const toWrite = [];
|
|
332
|
+
const createdIds = [];
|
|
333
|
+
let created = 0;
|
|
334
|
+
let updated = 0;
|
|
335
|
+
let unchanged = 0;
|
|
336
|
+
return {
|
|
337
|
+
entity,
|
|
338
|
+
toWrite,
|
|
339
|
+
createdIds,
|
|
340
|
+
manifestRecords: records.map((record) => {
|
|
341
|
+
const hash = recordHash(record);
|
|
342
|
+
const previous = prior.get(record.id);
|
|
343
|
+
if (previous === void 0) {
|
|
344
|
+
created++;
|
|
345
|
+
createdIds.push(record.id);
|
|
346
|
+
} else if (previous === hash) unchanged++;
|
|
347
|
+
else updated++;
|
|
348
|
+
if (previous !== hash) toWrite.push(record);
|
|
349
|
+
return {
|
|
350
|
+
id: record.id,
|
|
351
|
+
hash
|
|
352
|
+
};
|
|
353
|
+
}),
|
|
354
|
+
step: {
|
|
355
|
+
entity,
|
|
356
|
+
created,
|
|
357
|
+
updated,
|
|
358
|
+
unchanged,
|
|
359
|
+
deleted: 0
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function partialWrite(w, committed) {
|
|
364
|
+
if (committed <= 0) return null;
|
|
365
|
+
const createdSet = new Set(w.createdIds);
|
|
366
|
+
const prefix = w.toWrite.slice(0, committed);
|
|
367
|
+
const createdIds = prefix.map((r) => r.id).filter((id) => createdSet.has(id));
|
|
368
|
+
const updated = prefix.length - createdIds.length;
|
|
369
|
+
if (createdIds.length === 0 && updated === 0) return null;
|
|
370
|
+
return {
|
|
371
|
+
entity: w.entity,
|
|
372
|
+
toWrite: [],
|
|
373
|
+
createdIds,
|
|
374
|
+
manifestRecords: [],
|
|
375
|
+
step: {
|
|
376
|
+
entity: w.entity,
|
|
377
|
+
created: 0,
|
|
378
|
+
updated,
|
|
379
|
+
unchanged: 0,
|
|
380
|
+
deleted: 0
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function runUp(opts) {
|
|
385
|
+
const { loaded, sink, dryRun, reporter } = opts;
|
|
386
|
+
const tx = resolveTransaction(opts);
|
|
387
|
+
const plan = buildWritePlan(await evaluateDataFiles(await discoverDataFiles(loaded.projectRoot)));
|
|
388
|
+
const prior = priorHashes(await readManifest(loaded.projectRoot, loaded.connection.url));
|
|
389
|
+
const writes = plan.order.map((entity) => diffEntity(entity, plan.records.get(entity) ?? [], prior.get(entity) ?? /* @__PURE__ */ new Map()));
|
|
390
|
+
const steps = writes.map((w) => w.step);
|
|
391
|
+
const manifestEntities = writes.map((w) => ({
|
|
392
|
+
entity: w.entity,
|
|
393
|
+
records: w.manifestRecords
|
|
394
|
+
}));
|
|
395
|
+
const committed = steps.reduce((n, s) => n + s.created + s.updated, 0);
|
|
396
|
+
const pending = writes.filter((w) => w.toWrite.length > 0);
|
|
397
|
+
if (dryRun || pending.length === 0) {
|
|
398
|
+
for (const w of writes) {
|
|
399
|
+
reporter?.onStart?.(w.entity);
|
|
400
|
+
reporter?.onStep?.(w.step);
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
steps,
|
|
404
|
+
manifestWritten: false,
|
|
405
|
+
mode: dryRun ? "dry-run" : "noop",
|
|
406
|
+
committed: 0,
|
|
407
|
+
rolledBack: 0
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const writeManifestNow = (entities) => writeManifest(loaded.projectRoot, buildManifest({
|
|
411
|
+
fakewareVersion: opts.fakewareVersion ?? "0.0.0",
|
|
412
|
+
createdAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
413
|
+
shopwareUrl: loaded.connection.url,
|
|
414
|
+
entities
|
|
415
|
+
}));
|
|
416
|
+
const operations = pending.map((w) => ({
|
|
417
|
+
entity: w.entity,
|
|
418
|
+
action: "upsert",
|
|
419
|
+
records: w.toWrite
|
|
420
|
+
}));
|
|
421
|
+
if (tx.atomic && estimateSyncBytes(operations) <= 5242880) {
|
|
422
|
+
reporter?.onTransactionStart?.({ mode: "atomic" });
|
|
423
|
+
try {
|
|
424
|
+
await sink.applyAtomic(operations);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const message = "Apply failed — Shopware rolled back all changes.";
|
|
427
|
+
reporter?.onStop?.({
|
|
428
|
+
failedEntity: "",
|
|
429
|
+
error,
|
|
430
|
+
message
|
|
431
|
+
});
|
|
432
|
+
throw new TransactionError(message, {
|
|
433
|
+
cause: error,
|
|
434
|
+
rolledBack: [],
|
|
435
|
+
failedEntity: ""
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
await writeManifestNow(manifestEntities);
|
|
439
|
+
reporter?.onCommit?.({ committed });
|
|
440
|
+
return {
|
|
441
|
+
steps,
|
|
442
|
+
manifestWritten: true,
|
|
443
|
+
mode: "atomic",
|
|
444
|
+
committed,
|
|
445
|
+
rolledBack: 0
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
reporter?.onTransactionStart?.({ mode: "saga" });
|
|
449
|
+
const written = [];
|
|
450
|
+
const skipped = [];
|
|
451
|
+
for (const w of pending) {
|
|
452
|
+
reporter?.onStart?.(w.entity, w.toWrite.length);
|
|
453
|
+
let committedRecords = 0;
|
|
454
|
+
try {
|
|
455
|
+
await sink.upsert(w.entity, w.toWrite, (progress) => {
|
|
456
|
+
committedRecords = progress.records;
|
|
457
|
+
reporter?.onBatch?.(progress);
|
|
458
|
+
});
|
|
459
|
+
written.push(w);
|
|
460
|
+
reporter?.onStep?.(w.step);
|
|
461
|
+
} catch (error) {
|
|
462
|
+
if (tx.onError === "stop") {
|
|
463
|
+
const message = `Writing ${w.entity} failed — stopped.`;
|
|
464
|
+
reporter?.onStop?.({
|
|
465
|
+
failedEntity: w.entity,
|
|
466
|
+
error,
|
|
467
|
+
message
|
|
468
|
+
});
|
|
469
|
+
throw new TransactionError(message, {
|
|
470
|
+
cause: error,
|
|
471
|
+
rolledBack: [],
|
|
472
|
+
failedEntity: w.entity
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (tx.onError === "continue") {
|
|
476
|
+
skipped.push({
|
|
477
|
+
entity: w.entity,
|
|
478
|
+
error
|
|
479
|
+
});
|
|
480
|
+
reporter?.onSkip?.({
|
|
481
|
+
entity: w.entity,
|
|
482
|
+
error
|
|
483
|
+
});
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const message = `Could not apply ${w.entity}.`;
|
|
487
|
+
reporter?.onStop?.({
|
|
488
|
+
failedEntity: w.entity,
|
|
489
|
+
error,
|
|
490
|
+
message
|
|
491
|
+
});
|
|
492
|
+
const partial = partialWrite(w, committedRecords);
|
|
493
|
+
const { rolledBack, unrevertableUpdates, compensationErrors } = await compensate(sink, partial ? [...written, partial] : written, reporter);
|
|
494
|
+
throw new TransactionError(message, {
|
|
495
|
+
cause: error,
|
|
496
|
+
rolledBack,
|
|
497
|
+
failedEntity: w.entity,
|
|
498
|
+
unrevertableUpdates,
|
|
499
|
+
compensationErrors
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (const w of writes) if (!written.includes(w) && !skipped.some((s) => s.entity === w.entity)) {
|
|
504
|
+
reporter?.onStart?.(w.entity);
|
|
505
|
+
reporter?.onStep?.(w.step);
|
|
506
|
+
}
|
|
507
|
+
if (skipped.length > 0) {
|
|
508
|
+
await writeManifestNow(restrictManifest(manifestEntities, prior, new Set(written.map((w) => w.entity))));
|
|
509
|
+
const first = skipped[0];
|
|
510
|
+
throw new TransactionError(`Applied with ${skipped.length} skipped entit${skipped.length === 1 ? "y" : "ies"}.`, {
|
|
511
|
+
cause: skipped,
|
|
512
|
+
rolledBack: [],
|
|
513
|
+
failedEntity: first.entity
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
await writeManifestNow(manifestEntities);
|
|
517
|
+
reporter?.onCommit?.({ committed });
|
|
518
|
+
return {
|
|
519
|
+
steps,
|
|
520
|
+
manifestWritten: true,
|
|
521
|
+
mode: "saga",
|
|
522
|
+
committed,
|
|
523
|
+
rolledBack: 0
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async function compensate(sink, written, reporter) {
|
|
527
|
+
const rolledBack = [];
|
|
528
|
+
const compensationErrors = [];
|
|
529
|
+
for (const w of [...written].reverse()) {
|
|
530
|
+
if (w.createdIds.length === 0) continue;
|
|
531
|
+
try {
|
|
532
|
+
await sink.delete(w.entity, w.createdIds);
|
|
533
|
+
rolledBack.push({
|
|
534
|
+
entity: w.entity,
|
|
535
|
+
created: 0,
|
|
536
|
+
updated: 0,
|
|
537
|
+
unchanged: 0,
|
|
538
|
+
deleted: w.createdIds.length
|
|
539
|
+
});
|
|
540
|
+
reporter?.onCompensate?.(w.entity, w.createdIds.length);
|
|
541
|
+
} catch (error) {
|
|
542
|
+
compensationErrors.push(error);
|
|
543
|
+
reporter?.onCompensateFail?.(w.entity);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
rolledBack,
|
|
548
|
+
unrevertableUpdates: written.some((w) => w.step.updated > 0),
|
|
549
|
+
compensationErrors
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function restrictManifest(desired, prior, writtenEntities) {
|
|
553
|
+
const out = [];
|
|
554
|
+
const seen = /* @__PURE__ */ new Set();
|
|
555
|
+
for (const e of desired) {
|
|
556
|
+
seen.add(e.entity);
|
|
557
|
+
if (writtenEntities.has(e.entity)) out.push(e);
|
|
558
|
+
else {
|
|
559
|
+
const priorRecords = prior.get(e.entity);
|
|
560
|
+
if (priorRecords && priorRecords.size > 0) out.push({
|
|
561
|
+
entity: e.entity,
|
|
562
|
+
records: [...priorRecords].map(([id, hash]) => ({
|
|
563
|
+
id,
|
|
564
|
+
hash
|
|
565
|
+
}))
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
for (const [entity, records] of prior) if (!seen.has(entity) && records.size > 0) out.push({
|
|
570
|
+
entity,
|
|
571
|
+
records: [...records].map(([id, hash]) => ({
|
|
572
|
+
id,
|
|
573
|
+
hash
|
|
574
|
+
}))
|
|
575
|
+
});
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
async function runDown(opts) {
|
|
579
|
+
const { loaded, sink, dryRun, reporter } = opts;
|
|
580
|
+
const manifest = await readManifest(loaded.projectRoot, loaded.connection.url);
|
|
581
|
+
if (!manifest) return {
|
|
582
|
+
steps: [],
|
|
583
|
+
reverted: false
|
|
584
|
+
};
|
|
585
|
+
reporter?.onTransactionStart?.({ mode: "saga" });
|
|
586
|
+
const steps = [];
|
|
587
|
+
for (const entity of [...manifest.entities].reverse()) {
|
|
588
|
+
const ids = entity.records.map((r) => r.id);
|
|
589
|
+
reporter?.onStart?.(entity.entity, ids.length);
|
|
590
|
+
if (!dryRun && ids.length > 0) await sink.delete(entity.entity, ids, (progress) => reporter?.onBatch?.(progress));
|
|
591
|
+
const step = {
|
|
592
|
+
entity: entity.entity,
|
|
593
|
+
created: 0,
|
|
594
|
+
updated: 0,
|
|
595
|
+
unchanged: 0,
|
|
596
|
+
deleted: ids.length
|
|
597
|
+
};
|
|
598
|
+
steps.push(step);
|
|
599
|
+
reporter?.onStep?.(step);
|
|
600
|
+
}
|
|
601
|
+
if (!dryRun) await removeManifest(loaded.projectRoot, loaded.connection.url);
|
|
602
|
+
return {
|
|
603
|
+
steps,
|
|
604
|
+
reverted: !dryRun
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
//#endregion
|
|
608
|
+
export { GraphError, LoadModuleError, RefError, TransactionError, define, many, readManifest, ref, refs, runDown, runUp };
|
|
609
|
+
|
|
610
|
+
//# sourceMappingURL=index.mjs.map
|