@cosmicdrift/kumiko-framework 0.219.0 → 0.220.1
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/api/__tests__/request-locale.integration.test.ts +2 -2
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.test.ts +8 -8
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +8 -6
- package/src/scripts/codemod/pii-personal-migration.ts +30 -14
- package/src/stack/test-stack.ts +3 -0
- package/src/upgrade-cli.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.220.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
"./package.json": "./package.json"
|
|
195
195
|
},
|
|
196
196
|
"dependencies": {
|
|
197
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
197
|
+
"@cosmicdrift/kumiko-types": "0.220.1",
|
|
198
198
|
"bullmq": "^5.76.7",
|
|
199
199
|
"bun-types": "^1.3.13",
|
|
200
200
|
"hono": "^4.13.1",
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
"zod": "^4.4.3"
|
|
211
211
|
},
|
|
212
212
|
"devDependencies": {
|
|
213
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
213
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.220.1",
|
|
214
214
|
"bun-types": "^1.3.13",
|
|
215
215
|
"pino-pretty": "^13.1.3"
|
|
216
216
|
},
|
|
@@ -85,7 +85,7 @@ describe("ctx.locale falls back to the app's boot-configured defaultLocale", ()
|
|
|
85
85
|
beforeAll(async () => {
|
|
86
86
|
stack = await setupTestStack({
|
|
87
87
|
features: [localeProbe],
|
|
88
|
-
|
|
88
|
+
defaultLocale: "ja",
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
@@ -108,7 +108,7 @@ describe("ctx.locale falls back to SessionUser.locale ahead of the boot default"
|
|
|
108
108
|
beforeAll(async () => {
|
|
109
109
|
stack = await setupTestStack({
|
|
110
110
|
features: [localeProbe],
|
|
111
|
-
|
|
111
|
+
defaultLocale: "ja",
|
|
112
112
|
});
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -38,7 +38,7 @@ const executor = createEventStoreExecutor(sharedWidgetTable, sharedWidgetEntity,
|
|
|
38
38
|
entityName: "widget",
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
let deliveryCount = 0;
|
|
42
42
|
|
|
43
43
|
const listenFeature = defineFeature("listen", (r) => {
|
|
44
44
|
r.entity("widget", sharedWidgetEntity);
|
|
@@ -47,7 +47,7 @@ const listenFeature = defineFeature("listen", (r) => {
|
|
|
47
47
|
name: "latency-probe",
|
|
48
48
|
apply: {
|
|
49
49
|
"widget.created": async () => {
|
|
50
|
-
|
|
50
|
+
deliveryCount += 1;
|
|
51
51
|
},
|
|
52
52
|
},
|
|
53
53
|
});
|
|
@@ -79,17 +79,17 @@ afterAll(async () => {
|
|
|
79
79
|
|
|
80
80
|
describe("E.4 — PG NOTIFY/LISTEN wake-up", () => {
|
|
81
81
|
test("NOTIFY on commit triggers runOnce without waiting for the poll timer", async () => {
|
|
82
|
-
|
|
82
|
+
deliveryCount = 0;
|
|
83
83
|
|
|
84
84
|
await stack.eventDispatcher?.start();
|
|
85
85
|
try {
|
|
86
86
|
await executor.create({ name: "latency-test" }, admin, tdb);
|
|
87
87
|
|
|
88
88
|
const deadline = Date.now() + 5000;
|
|
89
|
-
while (Date.now() < deadline &&
|
|
89
|
+
while (Date.now() < deadline && deliveryCount === 0) {
|
|
90
90
|
await new Promise((r) => setTimeout(r, 5));
|
|
91
91
|
}
|
|
92
|
-
expect(
|
|
92
|
+
expect(deliveryCount).toBe(1);
|
|
93
93
|
} finally {
|
|
94
94
|
await stack.eventDispatcher?.stop();
|
|
95
95
|
}
|
|
@@ -105,16 +105,16 @@ describe("E.4 — PG NOTIFY/LISTEN wake-up", () => {
|
|
|
105
105
|
await stack.eventDispatcher?.stop();
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
deliveryCount = 0;
|
|
109
109
|
await stack.eventDispatcher?.start();
|
|
110
110
|
try {
|
|
111
111
|
await executor.create({ name: "restart-probe" }, admin, tdb);
|
|
112
112
|
|
|
113
113
|
const deadline = Date.now() + 5000;
|
|
114
|
-
while (Date.now() < deadline &&
|
|
114
|
+
while (Date.now() < deadline && deliveryCount === 0) {
|
|
115
115
|
await new Promise((r) => setTimeout(r, 5));
|
|
116
116
|
}
|
|
117
|
-
expect(
|
|
117
|
+
expect(deliveryCount).toBe(1);
|
|
118
118
|
} finally {
|
|
119
119
|
await stack.eventDispatcher?.stop();
|
|
120
120
|
}
|
|
@@ -190,10 +190,12 @@ describe("ctx.loadAggregate via queryHandler — Marten AggregateStreamAsync equ
|
|
|
190
190
|
{ customer: "TimeTraveler", status: "draft" },
|
|
191
191
|
admin,
|
|
192
192
|
);
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
const
|
|
193
|
+
// Cutoff must come from the event row's created_at (Postgres now()), not
|
|
194
|
+
// host wall-clock — Docker clock skew made Date()-based cutoffs exclude
|
|
195
|
+
// the create event (fw#2425).
|
|
196
|
+
const beforeApprove = await loadAggregateRaw(stack.db, created.id, admin.tenantId);
|
|
197
|
+
expect(beforeApprove).toHaveLength(1);
|
|
198
|
+
const preApprove = beforeApprove[0]!.createdAt;
|
|
197
199
|
// Make sure the next event's createdAt is strictly after the cutoff.
|
|
198
200
|
await new Promise((r) => setTimeout(r, 10));
|
|
199
201
|
await stack.http.writeOk(
|
|
@@ -210,10 +212,10 @@ describe("ctx.loadAggregate via queryHandler — Marten AggregateStreamAsync equ
|
|
|
210
212
|
);
|
|
211
213
|
expect(now.approved).toBe(true);
|
|
212
214
|
|
|
213
|
-
// asOf
|
|
215
|
+
// asOf create.createdAt: the approval is in the future, not yet visible.
|
|
214
216
|
const past = await stack.http.queryOk<{ approved: boolean; status: string }>(
|
|
215
217
|
"asoftest:query:invoice:state",
|
|
216
|
-
{ id: created.id, asOf: preApprove.
|
|
218
|
+
{ id: created.id, asOf: preApprove.toString() },
|
|
217
219
|
admin,
|
|
218
220
|
);
|
|
219
221
|
expect(past.approved).toBe(false);
|
|
@@ -204,15 +204,22 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
if (subjectProps.length > 1) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
const first = subjectProps[0];
|
|
208
|
+
if (first) {
|
|
209
|
+
report(
|
|
210
|
+
first.prop,
|
|
211
|
+
`multiple subject annotations on one field (${subjectProps.map((s) => s.name).join(", ")}) — needs a human decision on which subject is correct`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
211
214
|
// skip: reported above — multiple subject annotations need a human call on which one wins
|
|
212
215
|
return;
|
|
213
216
|
}
|
|
214
217
|
|
|
215
|
-
const subject = subjectProps[0]
|
|
218
|
+
const subject = subjectProps[0];
|
|
219
|
+
if (!subject) {
|
|
220
|
+
// skip: length===0 and length>1 already returned — empty [0] is unreachable
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
216
223
|
|
|
217
224
|
if (NO_PERSONAL_FACTORIES.has(factoryName)) {
|
|
218
225
|
report(
|
|
@@ -276,12 +283,13 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
|
|
|
276
283
|
return;
|
|
277
284
|
}
|
|
278
285
|
const ownerFieldProp = findProp(init, "ownerField");
|
|
279
|
-
|
|
286
|
+
const ownerFieldInit = ownerFieldProp?.getInitializer();
|
|
287
|
+
if (!ownerFieldInit) {
|
|
280
288
|
report(subject.prop, 'userOwned is missing an "ownerField" property');
|
|
281
289
|
// skip: reported above — userOwned is missing its ownerField property
|
|
282
290
|
return;
|
|
283
291
|
}
|
|
284
|
-
personalInit = `{ of: ${
|
|
292
|
+
personalInit = `{ of: ${ownerFieldInit.getText()} }`;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
const subjectIsRefOrPlaintext =
|
|
@@ -331,9 +339,9 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
|
|
|
331
339
|
const hasSearchable = !!searchableProp;
|
|
332
340
|
const hasSensitive = !!sensitiveProp;
|
|
333
341
|
|
|
334
|
-
if (
|
|
342
|
+
if (sensitiveProp && (hasLookupable || hasSearchable)) {
|
|
335
343
|
report(
|
|
336
|
-
sensitiveProp
|
|
344
|
+
sensitiveProp,
|
|
337
345
|
'sensitive combined with lookupable/searchable — two find values ("secret" vs "exact"/"fuzzy"), needs a human decision',
|
|
338
346
|
);
|
|
339
347
|
// skip: reported above — sensitive plus lookupable/searchable is an ambiguous find value
|
|
@@ -348,10 +356,13 @@ function processObjectLiteral(obj: ObjectLiteralExpression, factoryName: string)
|
|
|
348
356
|
else find = "none";
|
|
349
357
|
|
|
350
358
|
if (isLongTextFind && (find === "exact" || find === "fuzzy")) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
359
|
+
const findSite = lookupableProp ?? searchableProp;
|
|
360
|
+
if (findSite) {
|
|
361
|
+
report(
|
|
362
|
+
findSite,
|
|
363
|
+
'lookupable/searchable on createLongTextField — only "none"/"secret" are valid find values on longText',
|
|
364
|
+
);
|
|
365
|
+
}
|
|
355
366
|
// skip: reported above — exact/fuzzy find isn't valid on createLongTextField
|
|
356
367
|
return;
|
|
357
368
|
}
|
|
@@ -381,7 +392,12 @@ function applyTransform(
|
|
|
381
392
|
next: { personal: string; reason: string | undefined; find: string | undefined },
|
|
382
393
|
): void {
|
|
383
394
|
const properties = obj.getProperties();
|
|
384
|
-
|
|
395
|
+
// Call sites always pass at least subject.prop; without an anchor there is nowhere to insert.
|
|
396
|
+
const anchor = removedProps[0];
|
|
397
|
+
if (!anchor) {
|
|
398
|
+
// skip: no removed prop to anchor inserts on — nothing to transform
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
385
401
|
const anchorIndex = properties.indexOf(anchor);
|
|
386
402
|
const removedSet = new Set<PropertyAssignment>(removedProps);
|
|
387
403
|
let insertIndex = 0;
|
package/src/stack/test-stack.ts
CHANGED
|
@@ -72,6 +72,8 @@ export type TestStackOptions = {
|
|
|
72
72
|
jwtSecret?: string;
|
|
73
73
|
/** Extra fields merged into the AppContext (e.g. _notifyFactory, configResolver).
|
|
74
74
|
* Can be a function receiving (registry, db, sseBroker) for late binding. */
|
|
75
|
+
/** Boot default locale (BCP-47) merged into AppContext before extraContext. */
|
|
76
|
+
defaultLocale?: string;
|
|
75
77
|
extraContext?:
|
|
76
78
|
| Record<string, unknown>
|
|
77
79
|
| ((deps: {
|
|
@@ -294,6 +296,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
294
296
|
registry,
|
|
295
297
|
...(options.masterKeyProvider ? { masterKeyProvider: options.masterKeyProvider } : {}),
|
|
296
298
|
...(fileProviderResolver ? { _fileProviderResolver: fileProviderResolver } : {}),
|
|
299
|
+
...(options.defaultLocale !== undefined && { defaultLocale: options.defaultLocale }),
|
|
297
300
|
...(typeof options.extraContext === "function"
|
|
298
301
|
? options.extraContext({ registry, db: testDb.db, sseBroker, redis: testRedis.redis })
|
|
299
302
|
: options.extraContext),
|
package/src/upgrade-cli.ts
CHANGED
|
@@ -325,9 +325,11 @@ async function applyCodemods(
|
|
|
325
325
|
return 0;
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
+
const firstPending = pending[0];
|
|
329
|
+
if (!firstPending) return 0;
|
|
328
330
|
const latestVersion = pending.reduce(
|
|
329
331
|
(max, e) => (compareVersions(e.version, max) > 0 ? e.version : max),
|
|
330
|
-
|
|
332
|
+
firstPending.version,
|
|
331
333
|
);
|
|
332
334
|
writeUpgradeMarker(targetDir, {
|
|
333
335
|
version: latestVersion,
|