@cosmicdrift/kumiko-framework 0.2.2 → 0.3.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/CHANGELOG.md +54 -0
- package/package.json +124 -38
- package/src/__tests__/full-stack.integration.ts +2 -2
- package/src/api/auth-routes.ts +5 -5
- package/src/api/jwt.ts +2 -2
- package/src/api/route-registrars.ts +1 -1
- package/src/api/routes.ts +3 -3
- package/src/api/server.ts +6 -7
- package/src/auth/__tests__/roles.test.ts +24 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/roles.ts +42 -0
- package/src/compliance/__tests__/duration-spec.test.ts +72 -0
- package/src/compliance/__tests__/profiles.test.ts +308 -0
- package/src/compliance/__tests__/sub-processors.test.ts +139 -0
- package/src/compliance/duration-spec.ts +44 -0
- package/src/compliance/index.ts +31 -0
- package/src/compliance/override-schema.ts +136 -0
- package/src/compliance/profiles.ts +427 -0
- package/src/compliance/sub-processors.ts +152 -0
- package/src/db/__tests__/big-int-field.test.ts +131 -0
- package/src/db/assert-exists-in.ts +2 -2
- package/src/db/cursor.ts +3 -3
- package/src/db/event-store-executor.ts +19 -13
- package/src/db/located-timestamp.ts +1 -1
- package/src/db/money.ts +12 -2
- package/src/db/pg-error.ts +1 -1
- package/src/db/row-helpers.ts +1 -1
- package/src/db/table-builder.ts +20 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
- package/src/engine/__tests__/build-target.test.ts +135 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +4 -4
- package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
- package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
- package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
- package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
- package/src/engine/__tests__/raw-table.test.ts +2 -2
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
- package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
- package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
- package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
- package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
- package/src/engine/__tests__/steps-read.test.ts +142 -0
- package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
- package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
- package/src/engine/__tests__/steps-workflow.test.ts +198 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
- package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
- package/src/engine/boot-validator/api-ext.ts +77 -0
- package/src/engine/boot-validator/config-deps.ts +163 -0
- package/src/engine/boot-validator/entity-handler.ts +466 -0
- package/src/engine/boot-validator/index.ts +159 -0
- package/src/engine/boot-validator/ownership.ts +198 -0
- package/src/engine/boot-validator/pii-retention.ts +155 -0
- package/src/engine/boot-validator/screens-nav.ts +624 -0
- package/src/engine/boot-validator.ts +1 -1528
- package/src/engine/build-app-schema.ts +1 -1
- package/src/engine/build-target.ts +99 -0
- package/src/engine/codemod/index.ts +15 -0
- package/src/engine/codemod/pipeline-codemod.ts +641 -0
- package/src/engine/config-helpers.ts +9 -19
- package/src/engine/constants.ts +1 -1
- package/src/engine/define-feature.ts +127 -9
- package/src/engine/define-handler.ts +89 -3
- package/src/engine/define-roles.ts +2 -2
- package/src/engine/define-step.ts +28 -0
- package/src/engine/define-workflow.ts +110 -0
- package/src/engine/entity-handlers.ts +10 -9
- package/src/engine/event-helpers.ts +4 -4
- package/src/engine/extension-names.ts +105 -0
- package/src/engine/extensions/user-data.ts +106 -0
- package/src/engine/factories.ts +26 -16
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
- package/src/engine/feature-ast/extractors/index.ts +74 -0
- package/src/engine/feature-ast/extractors/round1.ts +110 -0
- package/src/engine/feature-ast/extractors/round2.ts +253 -0
- package/src/engine/feature-ast/extractors/round3.ts +471 -0
- package/src/engine/feature-ast/extractors/round4.ts +1365 -0
- package/src/engine/feature-ast/extractors/round5.ts +72 -0
- package/src/engine/feature-ast/extractors/round6.ts +66 -0
- package/src/engine/feature-ast/extractors/shared.ts +177 -0
- package/src/engine/feature-ast/parse.ts +13 -0
- package/src/engine/feature-ast/patch.ts +9 -1
- package/src/engine/feature-ast/patcher.ts +10 -3
- package/src/engine/feature-ast/patterns.ts +71 -1
- package/src/engine/feature-ast/render.ts +31 -1
- package/src/engine/index.ts +66 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
- package/src/engine/pattern-library/library.ts +78 -2
- package/src/engine/pipeline.ts +88 -0
- package/src/engine/projection-helpers.ts +1 -1
- package/src/engine/read-claim.ts +1 -1
- package/src/engine/registry.ts +30 -2
- package/src/engine/resolve-config-or-param.ts +4 -0
- package/src/engine/run-pipeline.ts +162 -0
- package/src/engine/schema-builder.ts +10 -4
- package/src/engine/state-machine.ts +1 -1
- package/src/engine/steps/_drizzle-boundary.ts +19 -0
- package/src/engine/steps/_duration-utils.ts +33 -0
- package/src/engine/steps/_no-return-guard.ts +21 -0
- package/src/engine/steps/_resolver-utils.ts +42 -0
- package/src/engine/steps/_step-dispatch-constants.ts +38 -0
- package/src/engine/steps/aggregate-append-event.ts +56 -0
- package/src/engine/steps/aggregate-create.ts +56 -0
- package/src/engine/steps/aggregate-update.ts +68 -0
- package/src/engine/steps/branch.ts +84 -0
- package/src/engine/steps/call-feature.ts +49 -0
- package/src/engine/steps/compute.ts +41 -0
- package/src/engine/steps/for-each.ts +111 -0
- package/src/engine/steps/mail-send.ts +44 -0
- package/src/engine/steps/read-find-many.ts +51 -0
- package/src/engine/steps/read-find-one.ts +58 -0
- package/src/engine/steps/retry.ts +87 -0
- package/src/engine/steps/return.ts +34 -0
- package/src/engine/steps/unsafe-projection-delete.ts +46 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
- package/src/engine/steps/wait-for-event.ts +71 -0
- package/src/engine/steps/wait.ts +69 -0
- package/src/engine/steps/webhook-send.ts +71 -0
- package/src/engine/system-user.ts +1 -1
- package/src/engine/types/feature.ts +143 -1
- package/src/engine/types/fields.ts +134 -10
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/identifiers.ts +1 -0
- package/src/engine/types/index.ts +15 -1
- package/src/engine/types/step.ts +334 -0
- package/src/engine/types/target-ref.ts +21 -0
- package/src/engine/types/tree-node.ts +130 -0
- package/src/engine/types/workspace.ts +7 -0
- package/src/engine/validate-projection-allowlist.ts +161 -0
- package/src/event-store/snapshot.ts +1 -1
- package/src/event-store/upcaster-dead-letter.ts +1 -1
- package/src/event-store/upcaster.ts +1 -1
- package/src/files/__tests__/read-stream.test.ts +105 -0
- package/src/files/__tests__/write-stream.test.ts +233 -0
- package/src/files/__tests__/zip-stream.test.ts +357 -0
- package/src/files/file-routes.ts +1 -1
- package/src/files/in-memory-provider.ts +38 -0
- package/src/files/index.ts +3 -0
- package/src/files/local-provider.ts +58 -1
- package/src/files/types.ts +36 -8
- package/src/files/zip-stream.ts +251 -0
- package/src/jobs/job-runner.ts +10 -10
- package/src/lifecycle/lifecycle.ts +0 -3
- package/src/logging/index.ts +1 -0
- package/src/logging/pino-logger.ts +11 -7
- package/src/logging/utils.ts +24 -0
- package/src/observability/prometheus-meter.ts +7 -5
- package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
- package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
- package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
- package/src/pipeline/append-event-core.ts +22 -6
- package/src/pipeline/dispatcher-utils.ts +188 -0
- package/src/pipeline/dispatcher.ts +63 -283
- package/src/pipeline/distributed-lock.ts +1 -1
- package/src/pipeline/entity-cache.ts +2 -2
- package/src/pipeline/event-consumer-state.ts +0 -13
- package/src/pipeline/event-dispatcher.ts +4 -4
- package/src/pipeline/index.ts +0 -2
- package/src/pipeline/lifecycle-pipeline.ts +6 -12
- package/src/pipeline/msp-rebuild.ts +5 -5
- package/src/pipeline/multi-stream-apply-context.ts +6 -7
- package/src/pipeline/projection-rebuild.ts +2 -2
- package/src/pipeline/projection-state.ts +0 -12
- package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
- package/src/rate-limit/resolver.ts +1 -1
- package/src/search/in-memory-adapter.ts +1 -1
- package/src/search/meilisearch-adapter.ts +3 -3
- package/src/search/types.ts +1 -1
- package/src/secrets/leak-guard.ts +2 -2
- package/src/stack/request-helper.ts +9 -5
- package/src/stack/test-stack.ts +1 -1
- package/src/testing/handler-context.ts +4 -4
- package/src/testing/http-cookies.ts +1 -1
- package/src/time/tz-context.ts +1 -2
- package/src/ui-types/index.ts +4 -0
- package/src/engine/feature-ast/extractors.ts +0 -2562
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { getTemporal } from "../time";
|
|
2
|
+
|
|
3
|
+
// Streaming-ZIP-Builder (S2.U3 Atom 3a) — pure-JS, dependency-frei.
|
|
4
|
+
//
|
|
5
|
+
// **Format-Wahl: STORE (kein DEFLATE).** Compression-frei + minimal —
|
|
6
|
+
// fuer User-Data-Export-Bundles akzeptabel weil:
|
|
7
|
+
// - Storage-Provider (S3/R2) komprimiert eh on-the-wire (HTTP gzip)
|
|
8
|
+
// - JSON-Snippets sind klein, File-Binaries sind oft schon komprimiert
|
|
9
|
+
// (PNG/JPG/PDF/MP4/...)
|
|
10
|
+
// - DEFLATE braucht zlib + buffering pro Entry, killt das Streaming-
|
|
11
|
+
// Property
|
|
12
|
+
//
|
|
13
|
+
// **Streaming-Property:** Caller gibt `AsyncIterable<ZipEntry>`, jeder
|
|
14
|
+
// Entry hat `data: AsyncIterable<Uint8Array>`. ZIP-Bytes werden
|
|
15
|
+
// chunk-fuer-chunk yieldet — kein Entry haengt im Memory bis er fertig
|
|
16
|
+
// ist. Storage-Provider's writeStream konsumiert direkt durch.
|
|
17
|
+
//
|
|
18
|
+
// **ZIP-Standard:** APPNOTE.TXT v6.3.10 (April 2022). STORE-Method,
|
|
19
|
+
// kein ZIP64 (max 4 GB pro Entry, max 65535 Entries — fuer User-Daten-
|
|
20
|
+
// Exports ausreichend; ZIP64-Support kommt wenn ein realer User mit
|
|
21
|
+
// >4 GB single-File auftaucht). Kein UTF-8-Flag (filenames sind ASCII
|
|
22
|
+
// in unserem Use-Case: "profile.json", "files/<id>.<ext>").
|
|
23
|
+
//
|
|
24
|
+
// **CRC32:** klassisches IEEE-802.3-Polynom (0xEDB88320). Lookup-Table
|
|
25
|
+
// einmal initialisiert + cached.
|
|
26
|
+
|
|
27
|
+
const ZIP_VERSION_NEEDED = 20; // 2.0 = STORE
|
|
28
|
+
const ZIP_METHOD_STORE = 0;
|
|
29
|
+
|
|
30
|
+
// Local file header signature: 0x04034b50 = "PK\x03\x04"
|
|
31
|
+
const LOCAL_FILE_HEADER_SIG = 0x04034b50;
|
|
32
|
+
// Central directory file header signature: 0x02014b50 = "PK\x01\x02"
|
|
33
|
+
const CENTRAL_DIR_HEADER_SIG = 0x02014b50;
|
|
34
|
+
// End of central directory record signature: 0x06054b50 = "PK\x05\x06"
|
|
35
|
+
const EOCD_SIG = 0x06054b50;
|
|
36
|
+
|
|
37
|
+
const VERSION_MADE_BY = 0x031e; // 0x03 = UNIX, 0x1e = ZIP 3.0
|
|
38
|
+
|
|
39
|
+
// General Purpose Flag Bit 11 (0x0800) = UTF-8 filename + comment encoding.
|
|
40
|
+
// Wird default-on gesetzt: ASCII ist gueltiges UTF-8, also Win-Win.
|
|
41
|
+
// Ohne Flag interpretieren aeltere ZIP-Tools filenames als CP437 — bei
|
|
42
|
+
// Umlauten ("Bügel.pdf") wird das Mojibake.
|
|
43
|
+
const GENERAL_PURPOSE_FLAGS = 0x0800;
|
|
44
|
+
|
|
45
|
+
// External-Attrs (UNIX-Mode in High-Bytes) fuer regulaere Dateien mit
|
|
46
|
+
// 0644-Permissions: `(S_IFREG | 0644) << 16 = 0o100644 << 16 = 0x81a40000`.
|
|
47
|
+
// S_IFREG = 0o100000 (regular file), 0o644 = rw-r--r--. Ohne diese
|
|
48
|
+
// Bits entpackt Info-ZIP mit Mode 0000 (EACCES beim Read).
|
|
49
|
+
const UNIX_REGULAR_FILE_0644 = 0o100644 << 16; // 0x81a40000
|
|
50
|
+
|
|
51
|
+
// ZIP-Format-Limits ohne ZIP64-Extension. Atom 3a-Scope ist STORE-only;
|
|
52
|
+
// ZIP64 kommt wenn ein realer User-Export diese Caps reisst.
|
|
53
|
+
const ZIP_MAX_ENTRY_SIZE = 0xffffffff; // uint32 → 4 GB
|
|
54
|
+
const ZIP_MAX_ENTRIES = 0xffff; // uint16 → 65535
|
|
55
|
+
|
|
56
|
+
export interface ZipEntry {
|
|
57
|
+
/** Pfad im ZIP, slash-separated. ASCII (kein UTF-8-Flag gesetzt). */
|
|
58
|
+
readonly path: string;
|
|
59
|
+
/** Body als AsyncIterable — kann lazy generated sein (kein Upfront-Memory). */
|
|
60
|
+
readonly data: AsyncIterable<Uint8Array>;
|
|
61
|
+
/**
|
|
62
|
+
* Optional Modification-Time. Default = Now. ZIP nutzt MS-DOS-Format
|
|
63
|
+
* (2-Sek-Aufloesung). Wer Audit-Trail-Zeitpunkte haben will, setzt
|
|
64
|
+
* den Generation-Timestamp ueber alle Entries.
|
|
65
|
+
*/
|
|
66
|
+
readonly mtime?: InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Erzeugt einen ZIP-Stream als `AsyncIterable<Uint8Array>`. Caller (z.B.
|
|
71
|
+
* `FileStorageProvider.writeStream`) konsumiert chunk-fuer-chunk, kein
|
|
72
|
+
* Entry haengt im Memory bis fertig.
|
|
73
|
+
*
|
|
74
|
+
* Streaming-Lifecycle pro Entry:
|
|
75
|
+
* 1. Local File Header (mit CRC32+size=0 als Placeholder; wir nutzen
|
|
76
|
+
* KEINE Streaming-Data-Descriptor weil Storage-Provider Random-
|
|
77
|
+
* Access nicht garantiert — wir collecten den Body in Memory pro
|
|
78
|
+
* Entry um CRC + size **vor** dem Header zu kennen.
|
|
79
|
+
* Trade-off: 1 Entry-Body im Memory at-a-time; bei <100 MB Files
|
|
80
|
+
* OK, bei >1 GB single-Files braeuchte ZIP64+Streaming-Descriptor).
|
|
81
|
+
* 2. File Body (raw bytes)
|
|
82
|
+
* Am Ende:
|
|
83
|
+
* 3. Central Directory (eine Liste-Entry pro File mit CRC + size)
|
|
84
|
+
* 4. EOCD (End Of Central Directory)
|
|
85
|
+
*/
|
|
86
|
+
export async function* createZipStream(
|
|
87
|
+
entries: AsyncIterable<ZipEntry>,
|
|
88
|
+
): AsyncIterable<Uint8Array> {
|
|
89
|
+
const centralDirRecords: Uint8Array[] = [];
|
|
90
|
+
let offset = 0; // Bytes-Counter fuer central-dir entry's local-header-offset
|
|
91
|
+
|
|
92
|
+
for await (const entry of entries) {
|
|
93
|
+
// Body in Memory collecten — wir brauchen CRC32 + size VOR dem
|
|
94
|
+
// local-file-header (kein streaming-data-descriptor weil
|
|
95
|
+
// file-storage-providers nicht zwingend seek-able sind, aber wir
|
|
96
|
+
// wollen vermeiden dass der konsumierende Storage zwei-pass laesst).
|
|
97
|
+
const body = await collectBody(entry.data);
|
|
98
|
+
|
|
99
|
+
// Hard-Limit: STORE-Mode ohne ZIP64-Extension cappt bei uint32 (4 GB).
|
|
100
|
+
// Bei Ueberschreitung wuerde der Integer-Wrap silent ein korruptes
|
|
101
|
+
// ZIP produzieren (Decoder lesen Muell-Sizes). Lieber early-fail mit
|
|
102
|
+
// klarer Begruendung — Worker (Atom 3b) fängt das + setzt Job=failed.
|
|
103
|
+
if (body.byteLength > ZIP_MAX_ENTRY_SIZE) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`ZIP-Entry "${entry.path}" exceeds 4 GB limit (${body.byteLength} bytes). ` +
|
|
106
|
+
`STORE-mode without ZIP64-extension caps at uint32. ` +
|
|
107
|
+
`Add ZIP64-support before exporting >4 GB single-files.`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (centralDirRecords.length >= ZIP_MAX_ENTRIES) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`ZIP archive exceeds ${ZIP_MAX_ENTRIES}-entry limit. ` +
|
|
113
|
+
`Add ZIP64-support before exporting >${ZIP_MAX_ENTRIES} entries.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const crc = crc32(body);
|
|
118
|
+
const size = body.byteLength;
|
|
119
|
+
const filenameBytes = new TextEncoder().encode(entry.path);
|
|
120
|
+
const dosTime = toDosTime(entry.mtime ?? getTemporal().Now.instant());
|
|
121
|
+
|
|
122
|
+
// Local File Header
|
|
123
|
+
const lfh = new Uint8Array(30 + filenameBytes.byteLength);
|
|
124
|
+
const lfhView = new DataView(lfh.buffer);
|
|
125
|
+
lfhView.setUint32(0, LOCAL_FILE_HEADER_SIG, true);
|
|
126
|
+
lfhView.setUint16(4, ZIP_VERSION_NEEDED, true);
|
|
127
|
+
lfhView.setUint16(6, GENERAL_PURPOSE_FLAGS, true);
|
|
128
|
+
lfhView.setUint16(8, ZIP_METHOD_STORE, true);
|
|
129
|
+
lfhView.setUint16(10, dosTime.time, true);
|
|
130
|
+
lfhView.setUint16(12, dosTime.date, true);
|
|
131
|
+
lfhView.setUint32(14, crc, true);
|
|
132
|
+
lfhView.setUint32(18, size, true); // compressed size = uncompressed (STORE)
|
|
133
|
+
lfhView.setUint32(22, size, true);
|
|
134
|
+
lfhView.setUint16(26, filenameBytes.byteLength, true);
|
|
135
|
+
lfhView.setUint16(28, 0, true); // extra field length
|
|
136
|
+
lfh.set(filenameBytes, 30);
|
|
137
|
+
|
|
138
|
+
yield lfh;
|
|
139
|
+
yield body;
|
|
140
|
+
|
|
141
|
+
// Central-Directory-Eintrag fuer diesen Entry — wir collecten ihn,
|
|
142
|
+
// emittieren ihn am Ende.
|
|
143
|
+
const cdh = new Uint8Array(46 + filenameBytes.byteLength);
|
|
144
|
+
const cdhView = new DataView(cdh.buffer);
|
|
145
|
+
cdhView.setUint32(0, CENTRAL_DIR_HEADER_SIG, true);
|
|
146
|
+
cdhView.setUint16(4, VERSION_MADE_BY, true);
|
|
147
|
+
cdhView.setUint16(6, ZIP_VERSION_NEEDED, true);
|
|
148
|
+
cdhView.setUint16(8, GENERAL_PURPOSE_FLAGS, true);
|
|
149
|
+
cdhView.setUint16(10, ZIP_METHOD_STORE, true);
|
|
150
|
+
cdhView.setUint16(12, dosTime.time, true);
|
|
151
|
+
cdhView.setUint16(14, dosTime.date, true);
|
|
152
|
+
cdhView.setUint32(16, crc, true);
|
|
153
|
+
cdhView.setUint32(20, size, true);
|
|
154
|
+
cdhView.setUint32(24, size, true);
|
|
155
|
+
cdhView.setUint16(28, filenameBytes.byteLength, true);
|
|
156
|
+
cdhView.setUint16(30, 0, true); // extra field length
|
|
157
|
+
cdhView.setUint16(32, 0, true); // comment length
|
|
158
|
+
cdhView.setUint16(34, 0, true); // disk number start
|
|
159
|
+
cdhView.setUint16(36, 0, true); // internal file attrs
|
|
160
|
+
cdhView.setUint32(38, UNIX_REGULAR_FILE_0644, true);
|
|
161
|
+
cdhView.setUint32(42, offset, true); // local-header-offset
|
|
162
|
+
cdh.set(filenameBytes, 46);
|
|
163
|
+
centralDirRecords.push(cdh);
|
|
164
|
+
|
|
165
|
+
offset += lfh.byteLength + body.byteLength;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Central Directory: alle entries hintereinander
|
|
169
|
+
const centralDirStart = offset;
|
|
170
|
+
let centralDirSize = 0;
|
|
171
|
+
for (const record of centralDirRecords) {
|
|
172
|
+
yield record;
|
|
173
|
+
centralDirSize += record.byteLength;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// EOCD (End Of Central Directory)
|
|
177
|
+
const eocd = new Uint8Array(22);
|
|
178
|
+
const eocdView = new DataView(eocd.buffer);
|
|
179
|
+
eocdView.setUint32(0, EOCD_SIG, true);
|
|
180
|
+
eocdView.setUint16(4, 0, true); // disk number
|
|
181
|
+
eocdView.setUint16(6, 0, true); // disk where central-dir starts
|
|
182
|
+
eocdView.setUint16(8, centralDirRecords.length, true); // entries on this disk
|
|
183
|
+
eocdView.setUint16(10, centralDirRecords.length, true); // total entries
|
|
184
|
+
eocdView.setUint32(12, centralDirSize, true);
|
|
185
|
+
eocdView.setUint32(16, centralDirStart, true);
|
|
186
|
+
eocdView.setUint16(20, 0, true); // .zip comment length
|
|
187
|
+
yield eocd;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function collectBody(source: AsyncIterable<Uint8Array>): Promise<Uint8Array> {
|
|
191
|
+
const chunks: Uint8Array[] = [];
|
|
192
|
+
let total = 0;
|
|
193
|
+
for await (const chunk of source) {
|
|
194
|
+
chunks.push(chunk);
|
|
195
|
+
total += chunk.byteLength;
|
|
196
|
+
}
|
|
197
|
+
const out = new Uint8Array(total);
|
|
198
|
+
let off = 0;
|
|
199
|
+
for (const c of chunks) {
|
|
200
|
+
out.set(c, off);
|
|
201
|
+
off += c.byteLength;
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// CRC32 — IEEE-802.3-Polynom (0xEDB88320, reversed). Lookup-Table einmal.
|
|
207
|
+
let CRC32_TABLE: Uint32Array | null = null;
|
|
208
|
+
function buildCrcTable(): Uint32Array {
|
|
209
|
+
const table = new Uint32Array(256);
|
|
210
|
+
for (let i = 0; i < 256; i++) {
|
|
211
|
+
let c = i;
|
|
212
|
+
for (let k = 0; k < 8; k++) {
|
|
213
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
214
|
+
}
|
|
215
|
+
table[i] = c >>> 0;
|
|
216
|
+
}
|
|
217
|
+
return table;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function crc32(data: Uint8Array): number {
|
|
221
|
+
if (!CRC32_TABLE) CRC32_TABLE = buildCrcTable();
|
|
222
|
+
let crc = 0xffffffff;
|
|
223
|
+
for (let i = 0; i < data.byteLength; i++) {
|
|
224
|
+
const byte = data[i] ?? 0;
|
|
225
|
+
const idx = (crc ^ byte) & 0xff;
|
|
226
|
+
crc = ((CRC32_TABLE[idx] ?? 0) ^ (crc >>> 8)) >>> 0;
|
|
227
|
+
}
|
|
228
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// MS-DOS Date+Time encoding fuer ZIP-Header.
|
|
232
|
+
// Date: bits 9-15=year-1980, 5-8=month, 0-4=day.
|
|
233
|
+
// Time: bits 11-15=hour, 5-10=minute, 0-4=second/2.
|
|
234
|
+
//
|
|
235
|
+
// **UTC** statt lokaler Zeitzone: in einem DSGVO-Kontext ist der
|
|
236
|
+
// ZIP-mtime Teil des Auskunfts-Artefakts. Verschiedene Server-Zeitzonen
|
|
237
|
+
// wuerden sonst verschiedene mtime-Werte fuer denselben Generation-
|
|
238
|
+
// Instant produzieren — Audit-Drift. UTC ist der Standard fuer alle
|
|
239
|
+
// server-side-Timestamps im Repo (Temporal.Instant ueberall).
|
|
240
|
+
function toDosTime(i: InstanceType<ReturnType<typeof getTemporal>["Instant"]>): {
|
|
241
|
+
date: number;
|
|
242
|
+
time: number;
|
|
243
|
+
} {
|
|
244
|
+
const dt = i.toZonedDateTimeISO("UTC");
|
|
245
|
+
const year = dt.year;
|
|
246
|
+
const date =
|
|
247
|
+
(((Math.max(year - 1980, 0) & 0x7f) << 9) | ((dt.month & 0x0f) << 5) | (dt.day & 0x1f)) >>> 0;
|
|
248
|
+
const time =
|
|
249
|
+
(((dt.hour & 0x1f) << 11) | ((dt.minute & 0x3f) << 5) | ((dt.second >> 1) & 0x1f)) >>> 0;
|
|
250
|
+
return { date, time };
|
|
251
|
+
}
|
package/src/jobs/job-runner.ts
CHANGED
|
@@ -104,7 +104,7 @@ const TRACE_CONTEXT_KEY = "_traceContext";
|
|
|
104
104
|
function readTraceContext(data: Record<string, unknown>): SerializedTraceContext | undefined {
|
|
105
105
|
const raw = data[TRACE_CONTEXT_KEY];
|
|
106
106
|
if (!raw || typeof raw !== "object") return undefined;
|
|
107
|
-
const ctx = raw as Partial<SerializedTraceContext>;
|
|
107
|
+
const ctx = raw as Partial<SerializedTraceContext>; // @cast-boundary engine-payload
|
|
108
108
|
if (!ctx.traceId || !ctx.spanId) return undefined;
|
|
109
109
|
return { traceId: ctx.traceId, spanId: ctx.spanId };
|
|
110
110
|
}
|
|
@@ -198,7 +198,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
198
198
|
for (const list of results) {
|
|
199
199
|
for (const j of list) {
|
|
200
200
|
if (j.name !== jobName) continue;
|
|
201
|
-
const t = (j.data as { _tenantId?: string } | undefined)?._tenantId;
|
|
201
|
+
const t = (j.data as { _tenantId?: string } | undefined)?._tenantId; // @cast-boundary dynamic-key
|
|
202
202
|
if (t === tenantId) {
|
|
203
203
|
count += 1;
|
|
204
204
|
if (count >= max) return true;
|
|
@@ -274,8 +274,8 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
274
274
|
// without peeking at BullMQ internals.
|
|
275
275
|
const rawData = bullJob.data as DbRow;
|
|
276
276
|
const meta: JobMeta = {
|
|
277
|
-
triggeredById: rawData["_triggeredById"] as string | undefined,
|
|
278
|
-
payload: rawData["_payload"] as string | undefined,
|
|
277
|
+
triggeredById: rawData["_triggeredById"] as string | undefined, // @cast-boundary dynamic-key
|
|
278
|
+
payload: rawData["_payload"] as string | undefined, // @cast-boundary dynamic-key
|
|
279
279
|
attempt: bullJob.attemptsMade + 1,
|
|
280
280
|
};
|
|
281
281
|
|
|
@@ -287,16 +287,16 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
287
287
|
|
|
288
288
|
// Determine tenantId and triggeredBy from meta
|
|
289
289
|
const tenantId =
|
|
290
|
-
(rawData["_tenantId"] as string | undefined) ??
|
|
291
|
-
(payload["tenantId"] as string | undefined) ??
|
|
290
|
+
(rawData["_tenantId"] as string | undefined) ?? // @cast-boundary dynamic-key
|
|
291
|
+
(payload["tenantId"] as string | undefined) ?? // @cast-boundary dynamic-key
|
|
292
292
|
SYSTEM_TENANT_ID;
|
|
293
|
-
const triggeredById = (rawData["_triggeredById"] as string | undefined) ?? null;
|
|
293
|
+
const triggeredById = (rawData["_triggeredById"] as string | undefined) ?? null; // @cast-boundary dynamic-key
|
|
294
294
|
|
|
295
295
|
// _triggerName aus rawData übernehmen falls gesetzt — handleEvent
|
|
296
296
|
// packt das beim Multi-Trigger-Dispatch rein (siehe unten). Über
|
|
297
297
|
// jobContext.triggerName freigegeben damit der Handler nicht selbst
|
|
298
298
|
// im rohen Payload kramen muss.
|
|
299
|
-
const triggerName = rawData["_triggerName"] as string | undefined;
|
|
299
|
+
const triggerName = rawData["_triggerName"] as string | undefined; // @cast-boundary dynamic-key
|
|
300
300
|
const jobContext: AppContext = {
|
|
301
301
|
...context,
|
|
302
302
|
systemUser: createSystemUser(tenantId),
|
|
@@ -317,7 +317,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
317
317
|
// so event writes during this job stamp the same correlation as the
|
|
318
318
|
// request that scheduled it. Cron/boot jobs (no scheduler) start fresh
|
|
319
319
|
// — correlationId = new requestId, no parent causation.
|
|
320
|
-
const inheritedCorrelationId = (rawData["_correlationId"] as string | undefined) ?? undefined;
|
|
320
|
+
const inheritedCorrelationId = (rawData["_correlationId"] as string | undefined) ?? undefined; // @cast-boundary dynamic-key
|
|
321
321
|
const jobRequestId = requestContext.generateId();
|
|
322
322
|
const jobCorrelationId = inheritedCorrelationId ?? jobRequestId;
|
|
323
323
|
|
|
@@ -460,7 +460,7 @@ export function createJobRunner(options: JobRunnerOptions): JobRunner {
|
|
|
460
460
|
// dispatch). Fan-out children of perTenant jobs land here on their
|
|
461
461
|
// recursive queue.add and DO carry _tenantId.
|
|
462
462
|
if (jobDef.maxPerTenant !== undefined) {
|
|
463
|
-
const tenantId = (payload as { _tenantId?: string } | undefined)?._tenantId;
|
|
463
|
+
const tenantId = (payload as { _tenantId?: string } | undefined)?._tenantId; // @cast-boundary dynamic-key
|
|
464
464
|
if (
|
|
465
465
|
tenantId !== undefined &&
|
|
466
466
|
(await isOverPerTenantLimit(jobName, tenantId, jobDef.maxPerTenant))
|
|
@@ -146,9 +146,6 @@ export function createLifecycle(opts: LifecycleOptions = {}): Lifecycle {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// Builds a single error-log closure once per lifecycle instance. Structured
|
|
150
|
-
// logger wins when present; otherwise plain stderr via console.error so we
|
|
151
|
-
// never eat a failure silently.
|
|
152
149
|
function makeErrorLogger(
|
|
153
150
|
logger: Pick<Logger, "error"> | undefined,
|
|
154
151
|
): (msg: string, err: unknown) => void {
|
package/src/logging/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type LoggerOptions = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export function createLogger(options: LoggerOptions = {}): Logger {
|
|
14
|
-
const level = options.level ?? (process.env["LOG_LEVEL"] as LoggerOptions["level"]) ?? "info";
|
|
14
|
+
const level = options.level ?? (process.env["LOG_LEVEL"] as LoggerOptions["level"]) ?? "info"; // @cast-boundary dynamic-key
|
|
15
15
|
const pretty = options.pretty ?? process.env["LOG_FORMAT"] === "pretty";
|
|
16
16
|
|
|
17
17
|
const pinoConfig = {
|
|
@@ -43,22 +43,26 @@ function wrapPino(p: pino.Logger): Logger {
|
|
|
43
43
|
return {
|
|
44
44
|
info(msg, data) {
|
|
45
45
|
const merged = mergeTraceFields(data);
|
|
46
|
-
merged
|
|
46
|
+
if (merged) p.info(merged, msg);
|
|
47
|
+
else p.info(msg);
|
|
47
48
|
},
|
|
48
49
|
warn(msg, data) {
|
|
49
50
|
const merged = mergeTraceFields(data);
|
|
50
|
-
merged
|
|
51
|
+
if (merged) p.warn(merged, msg);
|
|
52
|
+
else p.warn(msg);
|
|
51
53
|
},
|
|
52
54
|
error(msg, data) {
|
|
53
55
|
const merged = mergeTraceFields(data);
|
|
54
|
-
merged
|
|
56
|
+
if (merged) p.error(merged, msg);
|
|
57
|
+
else p.error(msg);
|
|
55
58
|
},
|
|
56
59
|
debug(msg, data) {
|
|
57
60
|
const merged = mergeTraceFields(data);
|
|
58
|
-
merged
|
|
61
|
+
if (merged) p.debug(merged, msg);
|
|
62
|
+
else p.debug(msg);
|
|
59
63
|
},
|
|
60
|
-
child(
|
|
61
|
-
return wrapPino(p.child(
|
|
64
|
+
child(ctx) {
|
|
65
|
+
return wrapPino(p.child(ctx));
|
|
62
66
|
},
|
|
63
67
|
};
|
|
64
68
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Logger } from "./types";
|
|
2
|
+
|
|
3
|
+
type FallbackLogger = {
|
|
4
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function createFallbackLogger(
|
|
8
|
+
namespace: string,
|
|
9
|
+
logger?: Pick<Logger, "error"> | undefined,
|
|
10
|
+
): FallbackLogger {
|
|
11
|
+
if (logger) {
|
|
12
|
+
return {
|
|
13
|
+
error(msg, data) {
|
|
14
|
+
logger.error(`[${namespace}] ${msg}`, data);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
error(msg, data) {
|
|
20
|
+
// biome-ignore lint/suspicious/noConsole: ops-visible fallback when no logger is wired
|
|
21
|
+
console.error(`[${namespace}] ${msg}:`, data);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -244,21 +244,23 @@ export function serializeOpenMetrics(meter: PrometheusMeter): string {
|
|
|
244
244
|
for (const name of names) {
|
|
245
245
|
const entry = snap.get(name);
|
|
246
246
|
if (!entry) continue;
|
|
247
|
-
const { def
|
|
247
|
+
const { def } = entry;
|
|
248
248
|
if (def.description) lines.push(`# HELP ${name} ${def.description}`);
|
|
249
249
|
lines.push(`# TYPE ${name} ${def.type}`);
|
|
250
250
|
|
|
251
|
-
// @cast-boundary engine-bridge — slots union narrows by def.type
|
|
252
251
|
if (def.type === "counter") {
|
|
253
|
-
|
|
252
|
+
const counterSlots = entry.slots as CounterState[]; // @cast-boundary engine-bridge
|
|
253
|
+
for (const s of counterSlots) {
|
|
254
254
|
lines.push(`${name}${renderLabels(s.labels)} ${s.value}`);
|
|
255
255
|
}
|
|
256
256
|
} else if (def.type === "gauge") {
|
|
257
|
-
|
|
257
|
+
const gaugeSlots = entry.slots as GaugeState[]; // @cast-boundary engine-bridge
|
|
258
|
+
for (const s of gaugeSlots) {
|
|
258
259
|
lines.push(`${name}${renderLabels(s.labels)} ${s.value}`);
|
|
259
260
|
}
|
|
260
261
|
} else {
|
|
261
|
-
|
|
262
|
+
const histSlots = entry.slots as HistogramState[]; // @cast-boundary engine-bridge
|
|
263
|
+
for (const s of histSlots) {
|
|
262
264
|
// Cumulative bucket counts + +Inf terminator + sum/count suffixes.
|
|
263
265
|
let cumulative = 0;
|
|
264
266
|
for (let i = 0; i < s.boundaries.length; i++) {
|
|
@@ -49,7 +49,7 @@ const archFeature = defineFeature("archtest", (r) => {
|
|
|
49
49
|
"item:relabel",
|
|
50
50
|
z.object({ id: z.uuid(), label: z.string() }),
|
|
51
51
|
async (event, ctx) => {
|
|
52
|
-
await ctx.
|
|
52
|
+
await ctx.unsafeAppendEvent({
|
|
53
53
|
aggregateId: event.payload.id,
|
|
54
54
|
aggregateType: "arch-item",
|
|
55
55
|
type: labelChanged.name,
|
|
@@ -65,7 +65,7 @@ const causationFeature = defineFeature("causation", (r) => {
|
|
|
65
65
|
async (event, ctx) => {
|
|
66
66
|
const created = await orderExecutor.create({ item: event.payload.item }, event.user, ctx.db);
|
|
67
67
|
if (!created.isSuccess) return created;
|
|
68
|
-
await ctx.
|
|
68
|
+
await ctx.unsafeAppendEvent({
|
|
69
69
|
aggregateId: String(created.data.id),
|
|
70
70
|
aggregateType: "causation-order",
|
|
71
71
|
type: placed.name,
|
|
@@ -105,7 +105,7 @@ const shippingFeature = defineFeature("shipping", (r) => {
|
|
|
105
105
|
"shipment:bill",
|
|
106
106
|
z.object({ id: z.uuid(), cost: z.number() }),
|
|
107
107
|
async (event, ctx) => {
|
|
108
|
-
await ctx.
|
|
108
|
+
await ctx.unsafeAppendEvent({
|
|
109
109
|
aggregateId: event.payload.id,
|
|
110
110
|
aggregateType: "domain-shipment",
|
|
111
111
|
type: shipmentBilled.name,
|
|
@@ -137,7 +137,7 @@ const shippingFeature = defineFeature("shipping", (r) => {
|
|
|
137
137
|
"shipment:bill-unregistered",
|
|
138
138
|
z.object({ id: z.uuid() }),
|
|
139
139
|
async (event, ctx) => {
|
|
140
|
-
await ctx.
|
|
140
|
+
await ctx.unsafeAppendEvent({
|
|
141
141
|
aggregateId: event.payload.id,
|
|
142
142
|
aggregateType: "domain-shipment",
|
|
143
143
|
type: "shipping:event:ghost", // never defined via r.defineEvent
|
|
@@ -152,7 +152,7 @@ const shippingFeature = defineFeature("shipping", (r) => {
|
|
|
152
152
|
"shipment:bill-bad-payload",
|
|
153
153
|
z.object({ id: z.uuid() }),
|
|
154
154
|
async (event, ctx) => {
|
|
155
|
-
await ctx.
|
|
155
|
+
await ctx.unsafeAppendEvent({
|
|
156
156
|
aggregateId: event.payload.id,
|
|
157
157
|
aggregateType: "domain-shipment",
|
|
158
158
|
type: shipmentBilled.name,
|
|
@@ -49,7 +49,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
|
|
|
49
49
|
"emit:valid",
|
|
50
50
|
z.object({ userId: z.uuid(), email: z.email() }),
|
|
51
51
|
async (cmd, ctx) => {
|
|
52
|
-
await ctx.
|
|
52
|
+
await ctx.unsafeAppendEvent({
|
|
53
53
|
aggregateId: cmd.payload.userId,
|
|
54
54
|
aggregateType: "user",
|
|
55
55
|
type: welcome.name,
|
|
@@ -66,7 +66,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
|
|
|
66
66
|
async (cmd, ctx) => {
|
|
67
67
|
// Deliberately NOT passing welcome.name — "emitter:event:not-registered"
|
|
68
68
|
// was never registered. ctx.appendEvent must reject at the append site.
|
|
69
|
-
await ctx.
|
|
69
|
+
await ctx.unsafeAppendEvent({
|
|
70
70
|
aggregateId: cmd.payload.userId,
|
|
71
71
|
aggregateType: "user",
|
|
72
72
|
type: "emitter:event:not-registered",
|
|
@@ -82,7 +82,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
|
|
|
82
82
|
z.object({ userId: z.uuid() }),
|
|
83
83
|
async (cmd, ctx) => {
|
|
84
84
|
// userId is correct but email is missing / not an email string.
|
|
85
|
-
await ctx.
|
|
85
|
+
await ctx.unsafeAppendEvent({
|
|
86
86
|
aggregateId: cmd.payload.userId,
|
|
87
87
|
aggregateType: "user",
|
|
88
88
|
type: welcome.name,
|
|
@@ -100,7 +100,7 @@ const emitterFeature = defineFeature("emitter", (r) => {
|
|
|
100
100
|
// "neighbor:event:neighbor-signal" is owned by the neighbor feature.
|
|
101
101
|
// The ownership guard in appendDomainEventCore must reject this append
|
|
102
102
|
// at emit-site — cross-feature emission silently breaks encapsulation.
|
|
103
|
-
await ctx.
|
|
103
|
+
await ctx.unsafeAppendEvent({
|
|
104
104
|
aggregateId: cmd.payload.userId,
|
|
105
105
|
aggregateType: "user",
|
|
106
106
|
type: foreignEventName,
|
|
@@ -66,7 +66,7 @@ const asOfFeature = defineFeature("asoftest", (r) => {
|
|
|
66
66
|
"invoice:approve",
|
|
67
67
|
z.object({ id: z.uuid(), amount: z.number().int(), approvedBy: z.string() }),
|
|
68
68
|
async (event, ctx) => {
|
|
69
|
-
await ctx.
|
|
69
|
+
await ctx.unsafeAppendEvent({
|
|
70
70
|
aggregateId: event.payload.id,
|
|
71
71
|
aggregateType: "asof-invoice",
|
|
72
72
|
type: approved.name,
|
|
@@ -55,7 +55,7 @@ const mmhFeature = defineFeature("mmh", (r) => {
|
|
|
55
55
|
async (event, ctx) => {
|
|
56
56
|
const created = await orderExecutor.create({ item: event.payload.item }, event.user, ctx.db);
|
|
57
57
|
if (!created.isSuccess) return created;
|
|
58
|
-
await ctx.
|
|
58
|
+
await ctx.unsafeAppendEvent({
|
|
59
59
|
aggregateId: String(created.data.id),
|
|
60
60
|
aggregateType: "mmh-order",
|
|
61
61
|
type: placed.name,
|
|
@@ -75,7 +75,7 @@ const mmhFeature = defineFeature("mmh", (r) => {
|
|
|
75
75
|
if (!ctx) throw new Error("MSP-apply ctx missing — regression of C.2b wiring");
|
|
76
76
|
const history = await ctx.loadAggregate(event.aggregateId);
|
|
77
77
|
confirmLoadCounts.push(history.length);
|
|
78
|
-
await ctx.
|
|
78
|
+
await ctx.unsafeAppendEvent({
|
|
79
79
|
aggregateId: event.aggregateId,
|
|
80
80
|
aggregateType: "mmh-order",
|
|
81
81
|
type: confirmed.name,
|
|
@@ -91,7 +91,7 @@ const mmhFeature = defineFeature("mmh", (r) => {
|
|
|
91
91
|
apply: {
|
|
92
92
|
[confirmed.name]: async (event, _tx, ctx) => {
|
|
93
93
|
if (!ctx) throw new Error("MSP-apply ctx missing — regression of C.2b wiring");
|
|
94
|
-
await ctx.
|
|
94
|
+
await ctx.unsafeAppendEvent({
|
|
95
95
|
aggregateId: event.aggregateId,
|
|
96
96
|
aggregateType: "mmh-order",
|
|
97
97
|
type: shipped.name,
|
|
@@ -139,7 +139,7 @@ const feature = defineFeature("mspreb", (r) => {
|
|
|
139
139
|
apply: {
|
|
140
140
|
[invoiceBilled.name]: async (event, _tx, ctx) => {
|
|
141
141
|
const p = event.payload as { customer: string };
|
|
142
|
-
await ctx.
|
|
142
|
+
await ctx.unsafeAppendEvent({
|
|
143
143
|
aggregateId: p.customer,
|
|
144
144
|
aggregateType: "msp-reb-invoice",
|
|
145
145
|
type: escalationTriggered.name,
|
|
@@ -166,7 +166,7 @@ const feature = defineFeature("mspreb", (r) => {
|
|
|
166
166
|
ctx.db,
|
|
167
167
|
);
|
|
168
168
|
if (!res.isSuccess) return res;
|
|
169
|
-
await ctx.
|
|
169
|
+
await ctx.unsafeAppendEvent({
|
|
170
170
|
aggregateId: String(res.data.id),
|
|
171
171
|
aggregateType: "msp-reb-invoice",
|
|
172
172
|
type: invoiceBilled.name,
|
|
@@ -187,7 +187,7 @@ const feature = defineFeature("mspreb", (r) => {
|
|
|
187
187
|
ctx.db,
|
|
188
188
|
);
|
|
189
189
|
if (!res.isSuccess) return res;
|
|
190
|
-
await ctx.
|
|
190
|
+
await ctx.unsafeAppendEvent({
|
|
191
191
|
aggregateId: String(res.data.id),
|
|
192
192
|
aggregateType: "msp-reb-payment",
|
|
193
193
|
type: paymentReceived.name,
|
|
@@ -124,7 +124,7 @@ const mspFeature = defineFeature("msptest", (r) => {
|
|
|
124
124
|
ctx.db,
|
|
125
125
|
);
|
|
126
126
|
if (!res.isSuccess) return res;
|
|
127
|
-
await ctx.
|
|
127
|
+
await ctx.unsafeAppendEvent({
|
|
128
128
|
aggregateId: String(res.data.id),
|
|
129
129
|
aggregateType: "msp-shipment",
|
|
130
130
|
type: shipmentBilled.name,
|
|
@@ -145,7 +145,7 @@ const mspFeature = defineFeature("msptest", (r) => {
|
|
|
145
145
|
ctx.db,
|
|
146
146
|
);
|
|
147
147
|
if (!res.isSuccess) return res;
|
|
148
|
-
await ctx.
|
|
148
|
+
await ctx.unsafeAppendEvent({
|
|
149
149
|
aggregateId: String(res.data.id),
|
|
150
150
|
aggregateType: "msp-refund",
|
|
151
151
|
type: refundIssued.name,
|
|
@@ -96,10 +96,10 @@ const qpFeature = defineFeature("qp", (r) => {
|
|
|
96
96
|
|
|
97
97
|
r.queryHandler(
|
|
98
98
|
"widget:list-system",
|
|
99
|
-
z.object({
|
|
99
|
+
z.object({ unsafeAllTenants: z.boolean().optional() }),
|
|
100
100
|
async (query, ctx) =>
|
|
101
101
|
ctx.queryProjection("qp:projection:widget-audit", {
|
|
102
|
-
|
|
102
|
+
unsafeAllTenants: query.payload.unsafeAllTenants ?? false,
|
|
103
103
|
}),
|
|
104
104
|
{ access: { openToAll: true } },
|
|
105
105
|
);
|
|
@@ -172,8 +172,8 @@ describe("ctx.queryProjection", () => {
|
|
|
172
172
|
expect(rows.map((r) => r.label).sort()).toEqual(["X", "Y"]);
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
test("
|
|
176
|
-
// Repurpose list-system by passing
|
|
175
|
+
test("unsafeAllTenants=true bypasses tenant filter on tenant-scoped projection", async () => {
|
|
176
|
+
// Repurpose list-system by passing unsafeAllTenants=true — but list-system is
|
|
177
177
|
// already no-tenant-column. The semantic matters when a projection HAS
|
|
178
178
|
// tenant_id but the handler wants a cross-tenant sweep (audit). We
|
|
179
179
|
// exercise that contract via a direct queryProjection call here.
|
|
@@ -186,7 +186,7 @@ describe("ctx.queryProjection", () => {
|
|
|
186
186
|
// surface small — assert against the two query handlers we have.)
|
|
187
187
|
const sys = await stack.http.queryOk<Array<{ label: string }>>(
|
|
188
188
|
"qp:query:widget:list-system",
|
|
189
|
-
{
|
|
189
|
+
{ unsafeAllTenants: true },
|
|
190
190
|
admin,
|
|
191
191
|
);
|
|
192
192
|
expect(sys).toHaveLength(2);
|