@cosmicdrift/kumiko-framework 0.154.0 → 0.154.2
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 +2 -2
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/api/__tests__/batch.integration.test.ts +9 -9
- package/src/engine/__tests__/engine.test.ts +16 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +9 -2
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/define-feature.ts +22 -4
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +15 -28
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +55 -14
- package/src/engine/feature-ast/__tests__/patch.test.ts +8 -13
- package/src/engine/feature-ast/__tests__/patcher.test.ts +12 -22
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +247 -5
- package/src/engine/feature-ast/extractors/index.ts +0 -3
- package/src/engine/feature-ast/extractors/round4.ts +113 -287
- package/src/engine/feature-ast/index.ts +0 -4
- package/src/engine/feature-ast/parse.ts +0 -6
- package/src/engine/feature-ast/patch.ts +35 -45
- package/src/engine/feature-ast/patcher.ts +18 -40
- package/src/engine/feature-ast/patterns.ts +20 -41
- package/src/engine/feature-ast/render.ts +17 -27
- package/src/engine/feature-config-events-jobs.ts +150 -74
- package/src/engine/feature-entity-handlers.ts +24 -6
- package/src/engine/feature-ui-extensions.ts +116 -45
- package/src/engine/object-form.ts +12 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +0 -19
- package/src/engine/pattern-library/library.ts +0 -4
- package/src/engine/pattern-library/mixed-schemas.ts +9 -80
- package/src/engine/pattern-library/shared-fields.ts +1 -6
- package/src/engine/registry-ingest.ts +7 -2
- package/src/engine/registry-validate.ts +6 -6
- package/src/engine/types/feature.ts +76 -45
- package/src/engine/types/handlers.ts +6 -3
- package/src/engine/types/nav.ts +4 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/jobs/__tests__/jobs.integration.test.ts +66 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +1 -1
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +200 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/meilisearch-adapter.ts +13 -12
|
@@ -301,21 +301,16 @@ describe("patch coverage for the remaining pattern-kinds", () => {
|
|
|
301
301
|
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "authClaims")).toBeUndefined();
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
-
test("add + remove via PatternId for
|
|
304
|
+
test("add + remove via PatternId for defineEvent with migrations", () => {
|
|
305
305
|
const sf = makeSourceFile(STARTER);
|
|
306
|
-
createFeaturePatcher(sf).
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
312
|
-
removePattern(sf, {
|
|
313
|
-
kind: "eventMigration",
|
|
314
|
-
eventName: "itemCreated",
|
|
315
|
-
fromVersion: 1,
|
|
316
|
-
toVersion: 2,
|
|
306
|
+
createFeaturePatcher(sf).addDefineEvent({
|
|
307
|
+
name: "itemCreated",
|
|
308
|
+
schemaSource: "z.object({ id: z.string() })",
|
|
309
|
+
version: 2,
|
|
310
|
+
migrations: { "1": "(old) => old" },
|
|
317
311
|
});
|
|
318
|
-
|
|
312
|
+
removePattern(sf, { kind: "defineEvent", eventName: "itemCreated" });
|
|
313
|
+
expect(parseSourceFile(sf).patterns.find((p) => p.kind === "defineEvent")).toBeUndefined();
|
|
319
314
|
});
|
|
320
315
|
});
|
|
321
316
|
|
|
@@ -157,48 +157,38 @@ describe("FeaturePatcher — typed add helpers for mixed (closure-bearing) patte
|
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test("
|
|
160
|
+
test("addHook routes entity-wide { allOf } target correctly", () => {
|
|
161
161
|
const sf = makeSourceFile(STARTER);
|
|
162
|
-
createFeaturePatcher(sf).
|
|
162
|
+
createFeaturePatcher(sf).addHook({
|
|
163
163
|
type: "postDelete",
|
|
164
|
-
|
|
164
|
+
target: { allOf: "task" },
|
|
165
165
|
handlerSource: "async (event, ctx) => { /* cleanup */ }",
|
|
166
166
|
});
|
|
167
167
|
const result = parseSourceFile(sf);
|
|
168
168
|
expect(result.patterns[0]).toMatchObject({
|
|
169
|
-
kind: "
|
|
169
|
+
kind: "hook",
|
|
170
170
|
hookType: "postDelete",
|
|
171
|
-
|
|
171
|
+
target: { allOf: "task" },
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
test("addDefineEvent
|
|
175
|
+
test("addDefineEvent with migrations forms a complete event-versioning chain", () => {
|
|
176
176
|
const sf = makeSourceFile(STARTER);
|
|
177
177
|
const p = createFeaturePatcher(sf);
|
|
178
178
|
p.addDefineEvent({
|
|
179
179
|
name: "stepCompleted",
|
|
180
180
|
schemaSource: "z.object({ id: z.string() })",
|
|
181
181
|
version: 2,
|
|
182
|
-
|
|
183
|
-
p.addEventMigration({
|
|
184
|
-
event: "stepCompleted",
|
|
185
|
-
fromVersion: 1,
|
|
186
|
-
toVersion: 2,
|
|
187
|
-
transformSource: '(old) => ({ id: old.id ?? "" })',
|
|
182
|
+
migrations: { "1": '(old) => ({ id: old.id ?? "" })' },
|
|
188
183
|
});
|
|
189
184
|
const result = parseSourceFile(sf);
|
|
190
185
|
expect(result.errors).toEqual([]);
|
|
191
|
-
expect(result.patterns).toHaveLength(
|
|
186
|
+
expect(result.patterns).toHaveLength(1);
|
|
192
187
|
expect(result.patterns[0]).toMatchObject({
|
|
193
188
|
kind: "defineEvent",
|
|
194
189
|
eventName: "stepCompleted",
|
|
195
190
|
version: 2,
|
|
196
|
-
|
|
197
|
-
expect(result.patterns[1]).toMatchObject({
|
|
198
|
-
kind: "eventMigration",
|
|
199
|
-
eventName: "stepCompleted",
|
|
200
|
-
fromVersion: 1,
|
|
201
|
-
toVersion: 2,
|
|
191
|
+
migrations: { "1": expect.anything() },
|
|
202
192
|
});
|
|
203
193
|
});
|
|
204
194
|
});
|
|
@@ -452,9 +442,9 @@ defineFeature("tasks", (r) => {
|
|
|
452
442
|
handlerSource: "async (q, ctx) => []",
|
|
453
443
|
access: { openToAll: true },
|
|
454
444
|
});
|
|
455
|
-
p.
|
|
445
|
+
p.addHook({
|
|
456
446
|
type: "postDelete",
|
|
457
|
-
|
|
447
|
+
target: { allOf: "task" },
|
|
458
448
|
handlerSource: "async (event, ctx) => { /* cascade-clean */ }",
|
|
459
449
|
});
|
|
460
450
|
const result = parseSourceFile(sf);
|
|
@@ -463,7 +453,7 @@ defineFeature("tasks", (r) => {
|
|
|
463
453
|
"entity",
|
|
464
454
|
"writeHandler",
|
|
465
455
|
"queryHandler",
|
|
466
|
-
"
|
|
456
|
+
"hook",
|
|
467
457
|
]);
|
|
468
458
|
});
|
|
469
459
|
});
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// up at canonical positions). We compare every other field.
|
|
10
10
|
|
|
11
11
|
import { describe, expect, test } from "bun:test";
|
|
12
|
-
import
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { Project, ts } from "ts-morph";
|
|
13
14
|
import { parseSourceFile } from "../parse";
|
|
14
15
|
import type { FeaturePattern } from "../patterns";
|
|
15
16
|
import { renderFeatureFile, renderPattern } from "../render";
|
|
@@ -29,10 +30,10 @@ defineFeature("inventory", (r) => {
|
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
32
|
|
|
32
|
-
r.relation("item", "supplier", {
|
|
33
|
+
r.relation("item", "supplier", { type: "belongsTo", target: "user", foreignKey: "supplierId" });
|
|
33
34
|
|
|
34
35
|
r.metric("created", { type: "counter" });
|
|
35
|
-
r.secret("apiKey", {
|
|
36
|
+
r.secret("apiKey", { label: { en: "Stripe API Key" }, scope: "tenant" });
|
|
36
37
|
r.claimKey("teamId", { type: "string" });
|
|
37
38
|
|
|
38
39
|
r.referenceData(
|
|
@@ -44,7 +45,16 @@ defineFeature("inventory", (r) => {
|
|
|
44
45
|
{ upsertKey: "id" },
|
|
45
46
|
);
|
|
46
47
|
|
|
47
|
-
r.config({
|
|
48
|
+
r.config({
|
|
49
|
+
keys: {
|
|
50
|
+
maxRows: {
|
|
51
|
+
type: "number",
|
|
52
|
+
default: 50,
|
|
53
|
+
scope: "tenant",
|
|
54
|
+
access: { read: ["user"], write: ["admin"] },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
48
58
|
});
|
|
49
59
|
`;
|
|
50
60
|
|
|
@@ -114,7 +124,7 @@ function stripLocations(value: unknown): unknown {
|
|
|
114
124
|
out[k] = { raw: normalizeBodyRaw((v as { raw: string }).raw) };
|
|
115
125
|
continue;
|
|
116
126
|
}
|
|
117
|
-
if (k === "opaqueProps" && v && typeof v === "object") {
|
|
127
|
+
if ((k === "opaqueProps" || k === "migrations") && v && typeof v === "object") {
|
|
118
128
|
const op: Record<string, unknown> = {};
|
|
119
129
|
for (const [pk, pv] of Object.entries(v as Record<string, unknown>)) {
|
|
120
130
|
if (pv && typeof pv === "object" && "raw" in pv) {
|
|
@@ -359,6 +369,41 @@ describe("renderPattern — single-pattern shape", () => {
|
|
|
359
369
|
});
|
|
360
370
|
});
|
|
361
371
|
|
|
372
|
+
// Regression guard for the class of bug the r.exposesApi/r.usesApi fold
|
|
373
|
+
// hit: a registrar-shape change must survive the Designer's parse→render→
|
|
374
|
+
// parse cycle, not just a TS compile of the framework itself. r.hook()'s
|
|
375
|
+
// `{ allOf: entity }` target (replacing r.entityHook) reuses HookPattern's
|
|
376
|
+
// already-generic target field, so this is the cheap case — but proving it
|
|
377
|
+
// beats assuming it.
|
|
378
|
+
const HOOK_ALL_OF_FEATURE = `
|
|
379
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
380
|
+
|
|
381
|
+
defineFeature("shop", (r) => {
|
|
382
|
+
r.entity("product", { fields: { name: { type: "text" } } });
|
|
383
|
+
r.hook("postSave", { allOf: "product" }, async (result) => {
|
|
384
|
+
console.log(result);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
`;
|
|
388
|
+
|
|
389
|
+
describe("render → parse roundtrip — r.hook({ allOf }) entity-wide target", () => {
|
|
390
|
+
test("allOf target survives parse → render → parse unchanged", () => {
|
|
391
|
+
const initial = parse(HOOK_ALL_OF_FEATURE);
|
|
392
|
+
const rendered = renderFeatureFile({
|
|
393
|
+
featureName: initial.featureName ?? "",
|
|
394
|
+
patterns: initial.patterns,
|
|
395
|
+
});
|
|
396
|
+
const reparsed = parse(rendered);
|
|
397
|
+
expect(reparsed.patterns.map(stripLocations)).toEqual(initial.patterns.map(stripLocations));
|
|
398
|
+
|
|
399
|
+
const hookPattern = reparsed.patterns.find((p) => p.kind === "hook");
|
|
400
|
+
expect(hookPattern?.kind).toBe("hook");
|
|
401
|
+
if (hookPattern?.kind === "hook") {
|
|
402
|
+
expect(hookPattern.target).toEqual({ allOf: "product" });
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
362
407
|
// Regression guard for the class of bug the r.exposesApi/r.usesApi fold
|
|
363
408
|
// hit: a registrar-shape change (there, removing a method; here, adding a
|
|
364
409
|
// nested optional field) must survive the Designer's parse→render→parse
|
|
@@ -400,3 +445,200 @@ describe("render → parse roundtrip — r.screen({ nav }) inline sugar", () =>
|
|
|
400
445
|
}
|
|
401
446
|
});
|
|
402
447
|
});
|
|
448
|
+
|
|
449
|
+
const DEFINE_EVENT_WITH_MIGRATIONS_FEATURE = `
|
|
450
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
451
|
+
|
|
452
|
+
defineFeature("billing", (r) => {
|
|
453
|
+
r.defineEvent("invoicePaid", z.object({ totalCents: z.number() }), {
|
|
454
|
+
version: 2,
|
|
455
|
+
migrations: [
|
|
456
|
+
{
|
|
457
|
+
fromVersion: 1,
|
|
458
|
+
toVersion: 2,
|
|
459
|
+
transform: (payload) => ({ totalCents: Math.round(payload.total * 100) }),
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
`;
|
|
465
|
+
|
|
466
|
+
describe("render → parse roundtrip — r.defineEvent({ migrations }) fold", () => {
|
|
467
|
+
test("migrations array survives parse → render → parse unchanged", () => {
|
|
468
|
+
const initial = parse(DEFINE_EVENT_WITH_MIGRATIONS_FEATURE);
|
|
469
|
+
const rendered = renderFeatureFile({
|
|
470
|
+
featureName: initial.featureName ?? "",
|
|
471
|
+
patterns: initial.patterns,
|
|
472
|
+
});
|
|
473
|
+
const reparsed = parse(rendered);
|
|
474
|
+
expect(reparsed.patterns.map(stripLocations)).toEqual(initial.patterns.map(stripLocations));
|
|
475
|
+
|
|
476
|
+
const eventPattern = reparsed.patterns.find((p) => p.kind === "defineEvent");
|
|
477
|
+
expect(eventPattern?.kind).toBe("defineEvent");
|
|
478
|
+
if (eventPattern?.kind === "defineEvent") {
|
|
479
|
+
expect(eventPattern.version).toBe(2);
|
|
480
|
+
expect(Object.keys(eventPattern.migrations ?? {})).toEqual(["1"]);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// --- Real-compile check ---------------------------------------------------
|
|
486
|
+
//
|
|
487
|
+
// Everything above proves parse(render(patterns)) is structurally faithful.
|
|
488
|
+
// It does NOT prove the rendered code actually compiles against the real
|
|
489
|
+
// FeatureRegistrar type — parse() runs against a bare in-memory Project with
|
|
490
|
+
// no lib/module resolution, so a rendered call can be well-formed AST and
|
|
491
|
+
// still be a type error against the real registrar (e.g. a removed method).
|
|
492
|
+
// That gap let the r.usesApi removal (folded into r.requires({apis})) pass
|
|
493
|
+
// the full 4960-test suite silently; only the visual Designer would have hit
|
|
494
|
+
// the broken shape.
|
|
495
|
+
//
|
|
496
|
+
// This Project resolves `@cosmicdrift/kumiko-framework/*` against the real
|
|
497
|
+
// framework source (unlike parse()'s Project, dependency resolution is NOT
|
|
498
|
+
// skipped), so `r` in a compiled fixture is the real FeatureRegistrar, not a
|
|
499
|
+
// structural stand-in.
|
|
500
|
+
const FRAMEWORK_TSCONFIG_PATH = path.join(import.meta.dir, "../../../../tsconfig.json");
|
|
501
|
+
|
|
502
|
+
const compileProject = new Project({
|
|
503
|
+
tsConfigFilePath: FRAMEWORK_TSCONFIG_PATH,
|
|
504
|
+
skipAddingFilesFromTsConfig: true,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
let compileCheckCounter = 0;
|
|
508
|
+
|
|
509
|
+
function compileAgainstRegistrar(source: string): readonly string[] {
|
|
510
|
+
compileCheckCounter += 1;
|
|
511
|
+
const filePath = path.join(
|
|
512
|
+
import.meta.dir,
|
|
513
|
+
`__generated__/compile-check-${compileCheckCounter}.gen.ts`,
|
|
514
|
+
);
|
|
515
|
+
const sourceFile = compileProject.createSourceFile(filePath, source, { overwrite: true });
|
|
516
|
+
const diagnostics = sourceFile.getPreEmitDiagnostics().map((d) => {
|
|
517
|
+
const text = ts.flattenDiagnosticMessageText(d.compilerObject.messageText, "\n");
|
|
518
|
+
return `${d.getLineNumber()}: ${text}`;
|
|
519
|
+
});
|
|
520
|
+
sourceFile.forget();
|
|
521
|
+
return diagnostics;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function renderAndCompile(source: string): readonly string[] {
|
|
525
|
+
const { featureName, patterns } = parse(source);
|
|
526
|
+
const rendered = renderFeatureFile({ featureName: featureName ?? "", patterns });
|
|
527
|
+
return compileAgainstRegistrar(rendered);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
describe("render → compiles against the real FeatureRegistrar type", () => {
|
|
531
|
+
test("compile check has teeth: an unknown registrar method is reported", () => {
|
|
532
|
+
const diagnostics = compileAgainstRegistrar(`
|
|
533
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
534
|
+
|
|
535
|
+
defineFeature("teeth-check", (r) => {
|
|
536
|
+
r.methodThatDoesNotExist({ nope: true });
|
|
537
|
+
});
|
|
538
|
+
`);
|
|
539
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
540
|
+
expect(diagnostics.join("\n")).toContain("methodThatDoesNotExist");
|
|
541
|
+
}, 20_000);
|
|
542
|
+
|
|
543
|
+
test("positive control: a minimal always-matched-the-real-shape fixture compiles clean", () => {
|
|
544
|
+
const diagnostics = compileAgainstRegistrar(`
|
|
545
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
546
|
+
|
|
547
|
+
defineFeature("teeth-check-positive", (r) => {
|
|
548
|
+
r.systemScope();
|
|
549
|
+
r.describe("Minimal fixture proving the harness returns [] on real success.");
|
|
550
|
+
r.toggleable({ default: true });
|
|
551
|
+
});
|
|
552
|
+
`);
|
|
553
|
+
expect(diagnostics).toEqual([]);
|
|
554
|
+
}, 20_000);
|
|
555
|
+
|
|
556
|
+
test("STATIC_FEATURE's rendered header-data patterns compile clean", () => {
|
|
557
|
+
expect(renderAndCompile(STATIC_FEATURE)).toEqual([]);
|
|
558
|
+
}, 20_000);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// One self-contained, compile-clean fixture per remaining pattern kind not
|
|
562
|
+
// already exercised above (STATIC_FEATURE / MIXED_FEATURE / HOOK_ALL_OF /
|
|
563
|
+
// SCREEN_WITH_NAV / DEFINE_EVENT_WITH_MIGRATIONS). Deliberately separate
|
|
564
|
+
// from the parse-roundtrip fixtures above: those were written to test
|
|
565
|
+
// parse/render symmetry, not to be type-correct (e.g. DEFINE_EVENT_WITH_
|
|
566
|
+
// MIGRATIONS_FEATURE uses `z` without importing it) — reusing them here
|
|
567
|
+
// would produce diagnostics unrelated to the registrar shape and defeat the
|
|
568
|
+
// point of an exact-match assertion.
|
|
569
|
+
//
|
|
570
|
+
// RAW_REF_FEATURE and the "unknown" pattern kind are excluded on purpose:
|
|
571
|
+
// raw-ref sentinels reference symbols that don't exist by design, and
|
|
572
|
+
// "unknown" is the parser's catch-all bucket for unrecognized r.calls, not
|
|
573
|
+
// a real user-authored pattern.
|
|
574
|
+
const MANIFEST_AND_CONFIG_FEATURE = `
|
|
575
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
576
|
+
import { z } from "zod";
|
|
577
|
+
|
|
578
|
+
defineFeature("catalog", (r) => {
|
|
579
|
+
r.systemScope();
|
|
580
|
+
r.describe("Product catalog and pricing.");
|
|
581
|
+
r.optionalRequires("promotions");
|
|
582
|
+
r.uiHints({ displayLabel: "Catalog", category: "Commerce", recommended: true });
|
|
583
|
+
r.readsConfig("auth.smtpHost");
|
|
584
|
+
r.translations({ keys: { en: { title: "Catalog" } } });
|
|
585
|
+
r.useExtension("audit-log", "item");
|
|
586
|
+
r.extendsRegistrar("audit-log", {});
|
|
587
|
+
r.envSchema(z.object({ CATALOG_API_KEY: z.string() }));
|
|
588
|
+
r.usesApi("compliance.forTenant");
|
|
589
|
+
r.exposesApi("catalog.pricingFor");
|
|
590
|
+
});
|
|
591
|
+
`;
|
|
592
|
+
|
|
593
|
+
// r.projection is deliberately excluded here: its `table` field (a Drizzle
|
|
594
|
+
// table reference) is dropped by the feature-ast renderer independent of
|
|
595
|
+
// the object-form/positional-form issue this fixture targets — a
|
|
596
|
+
// pre-existing, separately tracked gap (public-api-registrar-consolidation
|
|
597
|
+
// plan doc, kumiko-platform). multiStreamProjection below covers the
|
|
598
|
+
// `apply`-map shape without hitting it (its `table` is optional).
|
|
599
|
+
const INTEGRATION_FEATURE = `
|
|
600
|
+
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
601
|
+
|
|
602
|
+
defineFeature("fulfillment", (r) => {
|
|
603
|
+
r.job("reconcile-counts", { trigger: { manual: true } }, async (payload, context) => {
|
|
604
|
+
console.log(payload, context);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
r.notification("itemLowStock", {
|
|
608
|
+
trigger: { on: "item:lowStock" },
|
|
609
|
+
recipient: () => null,
|
|
610
|
+
data: () => ({}),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
r.authClaims(async () => ({}));
|
|
614
|
+
|
|
615
|
+
r.httpRoute({
|
|
616
|
+
method: "GET",
|
|
617
|
+
path: "/fulfillment/feed.xml",
|
|
618
|
+
handler: (c) => c.text("ok"),
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
r.multiStreamProjection({
|
|
622
|
+
name: "item-audit",
|
|
623
|
+
apply: {
|
|
624
|
+
"item.created": async (event, tx, ctx) => {
|
|
625
|
+
console.log(event, tx, ctx);
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
r.workspace({ id: "ops", label: "fulfillment:workspace.ops" });
|
|
631
|
+
|
|
632
|
+
r.treeActions({ list: {} });
|
|
633
|
+
});
|
|
634
|
+
`;
|
|
635
|
+
|
|
636
|
+
describe("render → compiles against the real FeatureRegistrar type — full pattern-kind coverage", () => {
|
|
637
|
+
test("manifest/config-only patterns compile clean", () => {
|
|
638
|
+
expect(renderAndCompile(MANIFEST_AND_CONFIG_FEATURE)).toEqual([]);
|
|
639
|
+
}, 20_000);
|
|
640
|
+
|
|
641
|
+
test("job/notification/httpRoute/projection/workspace patterns compile clean", () => {
|
|
642
|
+
expect(renderAndCompile(INTEGRATION_FEATURE)).toEqual([]);
|
|
643
|
+
}, 20_000);
|
|
644
|
+
});
|
|
@@ -29,8 +29,6 @@ export {
|
|
|
29
29
|
collectScreenOpaqueProps,
|
|
30
30
|
extractAuthClaims,
|
|
31
31
|
extractDefineEvent,
|
|
32
|
-
extractEntityHook,
|
|
33
|
-
extractEventMigration,
|
|
34
32
|
extractHook,
|
|
35
33
|
extractHttpRoute,
|
|
36
34
|
extractJob,
|
|
@@ -40,7 +38,6 @@ export {
|
|
|
40
38
|
extractQueryHandler,
|
|
41
39
|
extractScreen,
|
|
42
40
|
extractWriteHandler,
|
|
43
|
-
isEntityHookType,
|
|
44
41
|
isHookType,
|
|
45
42
|
isHttpRouteMethod,
|
|
46
43
|
type ParsedHandlerCall,
|