@cosmicdrift/kumiko-framework 0.248.0 → 0.250.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/package.json +3 -3
- package/src/__tests__/full-stack.integration.test.ts +3 -1
- package/src/crypto/__tests__/event-pii.test.ts +25 -3
- package/src/crypto/__tests__/kms-wiring.test.ts +15 -2
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +18 -0
- package/src/crypto/event-pii.ts +7 -1
- package/src/crypto/index.ts +1 -0
- package/src/crypto/kms-wiring.ts +23 -10
- package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +258 -0
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +33 -0
- package/src/db/entity-table-meta.ts +9 -2
- package/src/db/event-store-executor-read.ts +124 -7
- package/src/db/table-builder.ts +10 -0
- package/src/engine/__tests__/engine.test.ts +132 -2
- package/src/engine/__tests__/event-migration-declarative.test.ts +2 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +15 -5
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
- package/src/engine/__tests__/screen-helpers.test.ts +52 -0
- package/src/engine/boot-validator/action-wiring.ts +6 -5
- package/src/engine/entity-handlers.ts +4 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +27 -1
- package/src/engine/feature-ast/__tests__/patch.test.ts +1 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +56 -3
- package/src/engine/feature-ast/extractors/events.ts +24 -0
- package/src/engine/feature-ast/patcher.ts +6 -1
- package/src/engine/feature-ast/patterns.ts +6 -0
- package/src/engine/feature-ast/render.ts +3 -4
- package/src/engine/feature-config-events-jobs.ts +33 -9
- package/src/engine/pattern-library/__tests__/library.test.ts +1 -0
- package/src/engine/registry-facade.ts +10 -1
- package/src/engine/registry-state.ts +8 -0
- package/src/engine/registry-validate.ts +54 -3
- package/src/engine/screen-helpers.ts +2 -0
- package/src/engine/types/index.ts +1 -0
- package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +4 -1
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +10 -3
- package/src/event-store/__tests__/upcaster.integration.test.ts +8 -1
- package/src/jobs/__tests__/job-write-as.integration.test.ts +206 -0
- package/src/jobs/job-runner.ts +12 -4
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +3 -1
- package/src/pipeline/__tests__/causation-chain.integration.test.ts +3 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +3 -1
- package/src/pipeline/__tests__/event-define-event-strict.integration.test.ts +6 -2
- package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +6 -2
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +3 -1
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +5 -3
- package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +3 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +2 -0
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +3 -1
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +260 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +3 -1
- package/src/pipeline/system-hooks.ts +94 -25
|
@@ -6,9 +6,18 @@ import {
|
|
|
6
6
|
isPiiCiphertext,
|
|
7
7
|
PII_ERASED_SENTINEL,
|
|
8
8
|
} from "../crypto";
|
|
9
|
-
import
|
|
9
|
+
import { createTenantDb, entityTableFromRegistry, rehydrateCompoundTypes } from "../db";
|
|
10
|
+
import type { DbConnection, DbRow } from "../db/connection";
|
|
10
11
|
import { tenantChannel } from "../engine/constants";
|
|
11
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
AppContext,
|
|
14
|
+
EntityDefinition,
|
|
15
|
+
EntityId,
|
|
16
|
+
JobRunnerRef,
|
|
17
|
+
Registry,
|
|
18
|
+
SessionUser,
|
|
19
|
+
TenantId,
|
|
20
|
+
} from "../engine/types";
|
|
12
21
|
import type { SearchAdapter, SearchDocument } from "../search/types";
|
|
13
22
|
import type { EventConsumer } from "./event-dispatcher";
|
|
14
23
|
|
|
@@ -21,25 +30,29 @@ import type { EventConsumer } from "./event-dispatcher";
|
|
|
21
30
|
//
|
|
22
31
|
// Event → Search-Op Mapping:
|
|
23
32
|
//
|
|
24
|
-
// <entity>.created → index(tenantId, doc)
|
|
25
|
-
// <entity>.updated → index(tenantId, doc) //
|
|
26
|
-
// <entity>.restored → index(tenantId, doc) //
|
|
33
|
+
// <entity>.created → index(tenantId, doc) // state = event.payload
|
|
34
|
+
// <entity>.updated → index(tenantId, doc) // state = { ...previous, ...changes }
|
|
35
|
+
// <entity>.restored → index(tenantId, doc) // state = event.payload.previous
|
|
27
36
|
// <entity>.deleted → remove(tenantId, type, id)
|
|
37
|
+
// <entity>.forgotten → remove(tenantId, type, id)
|
|
38
|
+
// <entity>.<named> → index or remove, state read from the live
|
|
39
|
+
// projection row (#2765) — a named domain event
|
|
40
|
+
// carries only a payload slice, not the full
|
|
41
|
+
// entity, so there's no fixed shape to reconstruct
|
|
42
|
+
// state from the way created/updated/restored do.
|
|
43
|
+
// Only queried when the entity declares searchable
|
|
44
|
+
// fields/extensions; no row (hard/soft-deleted) →
|
|
45
|
+
// remove, same as the deleted branch.
|
|
28
46
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
47
|
+
// Sensitive fields never reach this consumer for created/updated/restored —
|
|
48
|
+
// event-store-executor.ts strips them before they land in the event log.
|
|
49
|
+
// The projection-row path (named events) reads them from the live table
|
|
50
|
+
// instead, so the same sensitive+searchable boot-guard (entity-handler.ts)
|
|
51
|
+
// keeps them out there too.
|
|
31
52
|
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// Sensitive fields sind aus dem event log bereits gestrippt (event-store-
|
|
37
|
-
// executor.ts), also kriegt der Search-Index sie ebenfalls nicht — das ist
|
|
38
|
-
// die gleiche Garantie wie vorher beim postSave-hook.
|
|
39
|
-
//
|
|
40
|
-
// Batch-Variante gibt's aktuell nicht mehr — jeder Event triggert einen
|
|
41
|
-
// eigenen index()-call. Wenn Performance nach Scale-Messung das erfordert,
|
|
42
|
-
// kann der event-dispatcher später eine Batch-Handler-Variante bekommen.
|
|
53
|
+
// No batch variant right now — every event triggers its own index() call.
|
|
54
|
+
// If a scale measurement later calls for it, the event-dispatcher could
|
|
55
|
+
// grow a batch-handler variant.
|
|
43
56
|
export const SEARCH_CONSUMER_NAME = "system:consumer:search";
|
|
44
57
|
|
|
45
58
|
export function createSearchEventConsumer(
|
|
@@ -55,7 +68,7 @@ export function createSearchEventConsumer(
|
|
|
55
68
|
// killing the consumer near-instantly). Upgrade path: time-based
|
|
56
69
|
// dead-lettering in event-dispatcher-delivery.ts if 2min isn't enough.
|
|
57
70
|
errorPolicy: { maxAttempts: 1200 },
|
|
58
|
-
handler: async (event) => {
|
|
71
|
+
handler: async (event, ctx) => {
|
|
59
72
|
const entityName = event.aggregateType;
|
|
60
73
|
const verb = event.type.split(".").pop();
|
|
61
74
|
const tenantId = event.tenantId;
|
|
@@ -67,14 +80,41 @@ export function createSearchEventConsumer(
|
|
|
67
80
|
return;
|
|
68
81
|
}
|
|
69
82
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
let state: Record<string, unknown>;
|
|
84
|
+
if (verb === "created" || verb === "updated" || verb === "restored") {
|
|
85
|
+
state = reconstructStateForSearch(event.payload, verb);
|
|
86
|
+
} else {
|
|
87
|
+
// #2765 — named domain event: no fixed payload shape to reconstruct
|
|
88
|
+
// state from. Only worth a projection read when the entity actually
|
|
89
|
+
// declared searchable fields/extensions — same check
|
|
90
|
+
// buildSearchDocument makes below, done here first so a non-
|
|
91
|
+
// searchable entity's named events stay a no-op like before.
|
|
92
|
+
const entity = registry.getEntity(entityName);
|
|
93
|
+
const isSearchable =
|
|
94
|
+
entity !== undefined &&
|
|
95
|
+
(registry.getSearchableFields(entityName).length > 0 ||
|
|
96
|
+
registry.getSearchPayloadExtensions(entityName).length > 0);
|
|
97
|
+
// skip: entity declares no searchable fields/extensions — nothing to
|
|
98
|
+
// index, stays a no-op with no extra query, same as before #2765.
|
|
99
|
+
if (!entity || !isSearchable) return;
|
|
100
|
+
|
|
101
|
+
const row = await readProjectionRowForSearch(
|
|
102
|
+
ctx,
|
|
103
|
+
registry,
|
|
104
|
+
entityName,
|
|
105
|
+
entity,
|
|
106
|
+
tenantId,
|
|
107
|
+
event.aggregateId,
|
|
108
|
+
);
|
|
109
|
+
// skip: no live row (hard-deleted stream, or soft-deleted) — same
|
|
110
|
+
// outcome as the deleted/forgotten branch above.
|
|
111
|
+
if (!row) {
|
|
112
|
+
await searchAdapter.remove(tenantId, entityName, event.aggregateId);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
state = row;
|
|
75
116
|
}
|
|
76
117
|
|
|
77
|
-
let state = reconstructStateForSearch(event.payload, verb);
|
|
78
118
|
state = await decryptSearchableSubjectFields(entityName, state, registry);
|
|
79
119
|
// skip: erased subject — drop the doc so a rebuild cannot resurrect plaintext.
|
|
80
120
|
if (hasErasedSearchableSubjectField(entityName, state, registry)) {
|
|
@@ -91,6 +131,35 @@ export function createSearchEventConsumer(
|
|
|
91
131
|
};
|
|
92
132
|
}
|
|
93
133
|
|
|
134
|
+
// #2765 — reads the live projection row for a named domain event, tenant-
|
|
135
|
+
// scoped through createTenantDb the same way MultiStreamProjection appliers
|
|
136
|
+
// scope their apply()-writes (see api/server.ts), on the table the registry
|
|
137
|
+
// actually booted (entityTableFromRegistry) rather than one freshly derived
|
|
138
|
+
// from the entity definition alone. undefined means "no live row" — a hard
|
|
139
|
+
// delete or a soft-delete — which the caller treats as a remove.
|
|
140
|
+
async function readProjectionRowForSearch(
|
|
141
|
+
ctx: AppContext,
|
|
142
|
+
registry: Registry,
|
|
143
|
+
entityName: string,
|
|
144
|
+
entity: EntityDefinition,
|
|
145
|
+
tenantId: TenantId,
|
|
146
|
+
entityId: EntityId,
|
|
147
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
148
|
+
// @cast-boundary engine-bridge — consumer ctx.db is always the outer
|
|
149
|
+
// DbConnection: event-dispatcher creation is gated on it being set
|
|
150
|
+
// (api/server.ts), never a HandlerContext's already-tenant-scoped TenantDb.
|
|
151
|
+
const baseDb = ctx.db as DbConnection | undefined;
|
|
152
|
+
if (!baseDb) return undefined;
|
|
153
|
+
const table = entityTableFromRegistry(registry, entityName, entity);
|
|
154
|
+
const where: Record<string, unknown> = { id: entityId };
|
|
155
|
+
if (entity.softDelete && table["isDeleted"]) {
|
|
156
|
+
where["isDeleted"] = false;
|
|
157
|
+
}
|
|
158
|
+
const row = await createTenantDb(baseDb, tenantId).fetchOne(table, where);
|
|
159
|
+
if (!row) return undefined;
|
|
160
|
+
return rehydrateCompoundTypes(row, entity);
|
|
161
|
+
}
|
|
162
|
+
|
|
94
163
|
// #1610 — subject-annotated searchable fields are ciphertext in the event
|
|
95
164
|
// payload; decrypt into the derived index only. No KMS → omit ciphertext
|
|
96
165
|
// values rather than indexing blobs.
|