@cosmicdrift/kumiko-framework 0.374.0 → 0.378.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/AGENTS.md +2 -2
- package/agents/examples/apex-landing.md +1 -1
- package/agents/examples/apex-surface-auth.md +3 -3
- package/agents/examples/compose-stacks.md +15 -8
- package/agents/examples/delivery-notifications.md +25 -2
- package/agents/examples/embedded-entity-form.md +1 -1
- package/agents/examples/embedded.md +191 -0
- package/agents/examples/notes-history-basic.md +1 -1
- package/agents/examples/tags-basic.md +1 -1
- package/agents/examples/user-profile.md +1 -1
- package/agents/examples/wizard-form.md +4 -4
- package/agents/features.md +2 -2
- package/agents/widgets.md +4 -3
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +2 -1
- package/dist/config/index.d.ts +4 -0
- package/dist/db/index.d.ts +2 -2
- package/dist/db/index.js +1 -1
- package/dist/db/migrate-runner.js +12 -4
- package/dist/db/money.d.ts +2 -1
- package/dist/db/money.js +3 -8
- package/dist/db/query.js +14 -14
- package/dist/db/replay-migration-sql.d.ts +1 -0
- package/dist/db/replay-migration-sql.js +115 -11
- package/dist/db/tenant-db.d.ts +7 -0
- package/dist/db/tenant-db.js +21 -5
- package/dist/engine/boot-validator/entity-handler.js +3 -0
- package/dist/engine/boot-validator/form-derivations.d.ts +4 -0
- package/dist/engine/boot-validator/form-derivations.js +82 -0
- package/dist/engine/boot-validator/screens.js +23 -4
- package/dist/engine/build-config-feature-schema.js +33 -10
- package/dist/engine/embedded-derived.d.ts +1 -1
- package/dist/engine/embedded-derived.js +7 -1
- package/dist/engine/form-derivations.d.ts +21 -0
- package/dist/engine/form-derivations.js +175 -0
- package/dist/engine/index.d.ts +5 -1
- package/dist/engine/index.js +3 -0
- package/dist/engine/minor-units.d.ts +2 -0
- package/dist/engine/minor-units.js +8 -0
- package/dist/engine/parse-ref-target.js +1 -1
- package/dist/engine/provider-plugin-context.d.ts +3 -0
- package/dist/engine/provider-plugin-context.js +28 -0
- package/dist/engine/schema-builder.js +7 -0
- package/dist/engine/template-variables.d.ts +2 -1
- package/dist/engine/template-variables.js +4 -0
- package/dist/engine/types/index.d.ts +3 -3
- package/dist/engine/with-form-derivations.d.ts +9 -0
- package/dist/engine/with-form-derivations.js +16 -0
- package/dist/env/index.js +1 -1
- package/dist/errors/zod-bridge.js +1 -1
- package/dist/files/file-ref-entity.d.ts +6 -6
- package/dist/files/file-ref-table.d.ts +6 -6
- package/dist/files/index.d.ts +1 -1
- package/dist/files/index.js +1 -1
- package/dist/files/provider-resolver.d.ts +17 -17
- package/dist/files/provider-resolver.js +1 -1
- package/dist/http/egress.d.ts +14 -0
- package/dist/http/egress.js +144 -0
- package/dist/http/index.d.ts +3 -2
- package/dist/http/index.js +2 -1
- package/dist/http/policy.d.ts +30 -0
- package/dist/http/policy.js +299 -0
- package/dist/i18n/required-surface-keys.js +2 -0
- package/dist/i18n/settings-hub-keys.js +1 -0
- package/dist/jobs/job-runner.js +4 -2
- package/dist/pipeline/config-accessor-factory.d.ts +3 -0
- package/dist/pipeline/config-accessor-factory.js +3 -0
- package/dist/pipeline/dispatch-shared.js +7 -5
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/search/configure-tenant-index.d.ts +4 -0
- package/dist/search/configure-tenant-index.js +8 -0
- package/dist/search/index.d.ts +1 -0
- package/dist/search/index.js +1 -0
- package/dist/ui-types/index.d.ts +3 -1
- package/dist/ui-types/index.js +1 -0
- package/dist/upgrade-changelog.d.ts +25 -0
- package/dist/upgrade-changelog.js +48 -0
- package/dist/upgrade-cli.js +78 -13
- package/package.json +3 -5
- package/presets/biome-app.json +8 -0
- package/src/changes.json +103 -0
- package/src/scripts/codemod/README.md +25 -0
- package/src/scripts/codemod/codemod-support.ts +64 -6
- package/src/scripts/codemod/package-merge.ts +260 -0
- package/src/scripts/codemod/s3-imports.ts +4 -42
- package/src/scripts/codemod/testing-injection.ts +135 -0
package/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ not exist in your version:
|
|
|
10
10
|
| File | What it holds |
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| `agents/features.md` | Every bundled feature: name, purpose, config keys, dependencies |
|
|
13
|
-
| `agents/widgets.md` | Every value export of `@cosmicdrift/kumiko-renderer
|
|
13
|
+
| `agents/widgets.md` | Every value export of `@cosmicdrift/kumiko-renderer/web` with its short description |
|
|
14
14
|
| `agents/examples.md` | Index of the worked examples, one per concept |
|
|
15
15
|
| `agents/examples/<name>.md` | One tested sample app: README plus every source and test file |
|
|
16
16
|
|
|
@@ -238,7 +238,7 @@ produces code that compiles and then fails in production.
|
|
|
238
238
|
- Never write your own JSX for a list, form, detail page or dashboard.
|
|
239
239
|
Declare a screen. Use `type: "custom"` only when no screen type fits.
|
|
240
240
|
- Inside a custom screen, never build your own card, table, badge, button or
|
|
241
|
-
chart. Use the widgets from `@cosmicdrift/kumiko-renderer
|
|
241
|
+
chart. Use the widgets from `@cosmicdrift/kumiko-renderer/web`
|
|
242
242
|
(`agents/widgets.md`) and the primitives from `usePrimitives()`.
|
|
243
243
|
- Never fetch data with `useEffect` plus `fetch`. Use `useQuery`
|
|
244
244
|
(`live: true` for server-sent updates) and `useMutation`.
|
|
@@ -238,7 +238,7 @@ import {
|
|
|
238
238
|
type ApexPage,
|
|
239
239
|
type ApexPricingTier,
|
|
240
240
|
renderApexPage,
|
|
241
|
-
} from "@cosmicdrift/kumiko-headless/apex";
|
|
241
|
+
} from "@cosmicdrift/kumiko-renderer/headless/apex";
|
|
242
242
|
import { HERO_SCREENSHOT } from "./constants";
|
|
243
243
|
|
|
244
244
|
// --- Inputs: shapes the surrounding features hand you -----------------------
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
import {
|
|
43
43
|
RequestAccountDeletionScreen, ConfirmAccountDeletionScreen,
|
|
44
44
|
} from "@cosmicdrift/kumiko-bundled-features/user-data-rights/web";
|
|
45
|
-
import { createPublicSurface } from "@cosmicdrift/kumiko-renderer
|
|
45
|
+
import { createPublicSurface } from "@cosmicdrift/kumiko-renderer/web";
|
|
46
46
|
|
|
47
47
|
// createLoginRoute, not a raw LoginScreen render — it owns the challenge-
|
|
48
48
|
// swap for a second factor, so mounting auth-mfa later doesn't need this
|
|
@@ -252,7 +252,7 @@ describe("apex-surface-auth recipe", () => {
|
|
|
252
252
|
// 2. Client (Kommentar unten + README): wie createPublicSurface + AuthShell
|
|
253
253
|
// die Screens in der Apex-Chrome mounten.
|
|
254
254
|
//
|
|
255
|
-
// CLIENT-WIRING (apex.tsx der App — renderer
|
|
255
|
+
// CLIENT-WIRING (apex.tsx der App — renderer/web, hier nur als Referenz, da
|
|
256
256
|
// Recipes keine Browser-Deps ziehen):
|
|
257
257
|
//
|
|
258
258
|
// import {
|
|
@@ -263,7 +263,7 @@ describe("apex-surface-auth recipe", () => {
|
|
|
263
263
|
// RequestAccountDeletionScreen, ConfirmAccountDeletionScreen,
|
|
264
264
|
// defaultTranslations as deletionI18n,
|
|
265
265
|
// } from "@cosmicdrift/kumiko-bundled-features/user-data-rights/web";
|
|
266
|
-
// import { createPublicSurface } from "@cosmicdrift/kumiko-renderer
|
|
266
|
+
// import { createPublicSurface } from "@cosmicdrift/kumiko-renderer/web";
|
|
267
267
|
//
|
|
268
268
|
// // AuthShell: die Auth-Card rendert in der Marketing-Chrome statt
|
|
269
269
|
// // Fullscreen. Default (ohne Provider) bleibt der Fullscreen-Wrapper.
|
|
@@ -44,22 +44,25 @@ the option.
|
|
|
44
44
|
| `composeGdprStack` | `kmsMode` | `kms` or `allowPlaintextPii` |
|
|
45
45
|
| `composeGdprStack({ selfService })`, `composeUserDataRightsStack` | `mail` | a mounted `channel-email` |
|
|
46
46
|
| `composeSecurityStack` | `kmsMode`, `rateLimit` not `false` | `kms` or `allowPlaintextPii` |
|
|
47
|
-
| `composeSaasStack({ billing: "stripe" })` | `billing` | a mounted subscription provider |
|
|
47
|
+
| `composeSaasStack({ billing: "stripe" })` | `billing` (warning only) | a mounted subscription provider |
|
|
48
48
|
|
|
49
49
|
A missing option fails the boot with:
|
|
50
50
|
|
|
51
51
|
```
|
|
52
52
|
[runProdApp] stacks in `features` need boot options that are not set:
|
|
53
|
-
-
|
|
53
|
+
- composeGdprStack needs the boot option `kmsMode`: set kmsMode: "required" (or { plaintextReason } for a demo)
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
A stripe `composeSaasStack` without `billing` boots and logs the same line as a warning, so an
|
|
57
|
+
app can go live before its prices exist.
|
|
58
|
+
|
|
56
59
|
## Flow
|
|
57
60
|
|
|
58
61
|
1. `src/app.ts` lists the stacks and the boot options they need.
|
|
59
62
|
2. The test boots with `KUMIKO_DRY_RUN_ENV=boot`, which runs the whole
|
|
60
63
|
validation and stops before any DB or Redis connection: with the options
|
|
61
|
-
the boot passes, without `
|
|
62
|
-
|
|
64
|
+
the boot passes, without `kmsMode` it fails and names the stacks, and
|
|
65
|
+
without `billing` it warns.
|
|
63
66
|
|
|
64
67
|
## Tests
|
|
65
68
|
|
|
@@ -122,11 +125,15 @@ describe("compose stacks recipe", () => {
|
|
|
122
125
|
expect(lines.some((line: string) => line.includes("boot validation OK"))).toBe(true);
|
|
123
126
|
});
|
|
124
127
|
|
|
125
|
-
test("a missing billing option
|
|
128
|
+
test("a missing billing option only warns and names composeSaasStack", async () => {
|
|
126
129
|
const { billing: _billing, ...withoutBilling } = bootOptions();
|
|
127
|
-
await
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
await bootValidate(withoutBilling);
|
|
131
|
+
const warnings = warnSpy.mock.calls.map((call: unknown[]) => String(call[0]));
|
|
132
|
+
expect(
|
|
133
|
+
warnings.some((line: string) =>
|
|
134
|
+
line.includes("composeSaasStack needs the boot option `billing`"),
|
|
135
|
+
),
|
|
136
|
+
).toBe(true);
|
|
130
137
|
});
|
|
131
138
|
|
|
132
139
|
test("a missing KMS decision names every stack that needs it", async () => {
|
|
@@ -16,7 +16,7 @@ Shows how a feature sends notifications to multiple channels (InApp + Email + Pu
|
|
|
16
16
|
|
|
17
17
|
**Mail text is stored data:** The boot seed (`seedNotificationTemplates`) writes the declared `template` and `meta` into the template store as system templates. The email channel renders them with `renderer-mail-html`. A notification routed to email without `template` and `meta` fails its delivery.
|
|
18
18
|
|
|
19
|
-
**Tenant override:**
|
|
19
|
+
**Tenant override:** Only a SystemAdmin edits templates unless the app unlocks them. The app lists the mail in `createTemplateResolverFeature({ tenantEditable: [{ kind: "mail", slug, access: { roles: ["TenantAdmin"] } }] })`; then a tenant admin overrides the text through the template-resolver handlers (`upsertTenant`, then `publish`) and every other template is refused with 403. The next mail for that tenant uses the override; the system text stays for everyone else.
|
|
20
20
|
|
|
21
21
|
**Sending:** The write handler calls `ctx.notify(type, { to, data })`. The `data` keys are the template variables; sending fails loudly when a required variable is missing.
|
|
22
22
|
|
|
@@ -137,7 +137,9 @@ const assignee = createTestUser({ id: 5, roles: ["Support"] });
|
|
|
137
137
|
const features = [
|
|
138
138
|
createConfigFeature(),
|
|
139
139
|
createTenantFeature(),
|
|
140
|
-
createTemplateResolverFeature(
|
|
140
|
+
createTemplateResolverFeature({
|
|
141
|
+
tenantEditable: [{ kind: "mail", slug: TICKET_ASSIGNED, access: { roles: ["TenantAdmin"] } }],
|
|
142
|
+
}),
|
|
141
143
|
createRendererFoundationFeature(),
|
|
142
144
|
createRendererMailHtmlFeature(),
|
|
143
145
|
createDeliveryFeature(),
|
|
@@ -275,6 +277,27 @@ describe("delivery-notifications sample", () => {
|
|
|
275
277
|
expect(email?.html).toContain("Tenant text: Printer on fire");
|
|
276
278
|
});
|
|
277
279
|
|
|
280
|
+
test("a tenant admin may not override a mail the app did not list in tenantEditable", async () => {
|
|
281
|
+
const tenantAdmin = { ...admin, roles: ["TenantAdmin"] };
|
|
282
|
+
|
|
283
|
+
const error = await stack.http.writeErr(
|
|
284
|
+
TemplateResolverHandlers.upsertTenant,
|
|
285
|
+
{
|
|
286
|
+
slug: "support:notify:ticket-closed",
|
|
287
|
+
kind: "mail",
|
|
288
|
+
locale: "en",
|
|
289
|
+
content: "<p>Closed</p>",
|
|
290
|
+
contentFormat: "html",
|
|
291
|
+
meta: { subject: "Closed" },
|
|
292
|
+
variableSchema: {},
|
|
293
|
+
},
|
|
294
|
+
tenantAdmin,
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(error.code).toBe("access_denied");
|
|
298
|
+
expect(error.i18nKey).toBe("templateResolver.errors.templateNotTenantEditable");
|
|
299
|
+
});
|
|
300
|
+
|
|
278
301
|
test("ticket without assignee skips notification (recipient returns null)", async () => {
|
|
279
302
|
emailTransport.sent.length = 0;
|
|
280
303
|
pushTransport.sent.length = 0;
|
|
@@ -471,7 +471,7 @@ export const prospectAcceptScreen: EntityEditScreenDefinition = {
|
|
|
471
471
|
```tsx
|
|
472
472
|
import type { RenderEditControls } from "@cosmicdrift/kumiko-renderer";
|
|
473
473
|
import { RenderEdit, useDispatcher, usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
474
|
-
import { Drawer } from "@cosmicdrift/kumiko-renderer
|
|
474
|
+
import { Drawer } from "@cosmicdrift/kumiko-renderer/web";
|
|
475
475
|
import { type ReactNode, useMemo, useRef, useState } from "react";
|
|
476
476
|
import { z } from "zod";
|
|
477
477
|
import { prospectEntity } from "../entities/prospect";
|
|
@@ -27,6 +27,14 @@ object and an embedded list rendered by a declarative `entityEdit` screen.
|
|
|
27
27
|
`createEmbeddedListField`; its renderer receives `embeddedListCells`, so no
|
|
28
28
|
custom screen component is required.
|
|
29
29
|
|
|
30
|
+
- **Derived head totals** (`src/screens/invoice-receive.screen.ts`): an
|
|
31
|
+
`actionForm` declares `derive` so net, VAT, and gross follow the lines
|
|
32
|
+
(`listSum`), or follow a typed net and VAT rate while the list is empty
|
|
33
|
+
(`whenEmpty`, `percent`). The renderer shows them read-only and live; the
|
|
34
|
+
handler wraps its schema with `withFormDerivations(schema, screen)`
|
|
35
|
+
(`src/handlers/invoice-receive.write.ts`), so totals sent by the client are
|
|
36
|
+
replaced on the server.
|
|
37
|
+
|
|
30
38
|
## When to reach for it
|
|
31
39
|
|
|
32
40
|
Use an embedded field or list when the nested data is created, changed, and
|
|
@@ -397,6 +405,74 @@ describe("invoice line items with select/reference/derived/totals metadata", ()
|
|
|
397
405
|
expect(error).toBeDefined();
|
|
398
406
|
});
|
|
399
407
|
});
|
|
408
|
+
|
|
409
|
+
// --- actionForm derive ---
|
|
410
|
+
|
|
411
|
+
describe("invoice receive form derives its totals on the server", () => {
|
|
412
|
+
type ReceiveTotals = {
|
|
413
|
+
netTotal: { amount: number; currency: string };
|
|
414
|
+
vatTotal: { amount: number; currency: string };
|
|
415
|
+
grossTotal: { amount: number; currency: string };
|
|
416
|
+
};
|
|
417
|
+
const eur = (amount: number) => ({ amount, currency: "EUR" });
|
|
418
|
+
const head = { supplier: "Papier AG", invoiceNumber: "RE-2026-0042" };
|
|
419
|
+
|
|
420
|
+
test("with lines, manipulated client totals are replaced by the line sums", async () => {
|
|
421
|
+
const totals = await stack.http.writeOk<ReceiveTotals>(
|
|
422
|
+
"contacts:write:invoice:receive",
|
|
423
|
+
{
|
|
424
|
+
...head,
|
|
425
|
+
lines: [
|
|
426
|
+
{ description: "Paper", amount: 1005, vatRate: 19, vatAmount: 5 },
|
|
427
|
+
{ description: "Toner", amount: 2000, vatRate: 7 },
|
|
428
|
+
],
|
|
429
|
+
netTotal: eur(1),
|
|
430
|
+
vatTotal: eur(2),
|
|
431
|
+
grossTotal: eur(3),
|
|
432
|
+
},
|
|
433
|
+
admin,
|
|
434
|
+
);
|
|
435
|
+
expect(totals).toEqual({
|
|
436
|
+
netTotal: eur(30.05),
|
|
437
|
+
vatTotal: eur(3.31),
|
|
438
|
+
grossTotal: eur(33.36),
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("without lines, the typed net stays and VAT and gross are computed from the rate", async () => {
|
|
443
|
+
const totals = await stack.http.writeOk<ReceiveTotals>(
|
|
444
|
+
"contacts:write:invoice:receive",
|
|
445
|
+
{ ...head, lines: [], netTotal: eur(100), vatRate: 19, vatTotal: eur(0), grossTotal: eur(0) },
|
|
446
|
+
admin,
|
|
447
|
+
);
|
|
448
|
+
expect(totals).toEqual({ netTotal: eur(100), vatTotal: eur(19), grossTotal: eur(119) });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("a stale gross total is overwritten", async () => {
|
|
452
|
+
const totals = await stack.http.writeOk<ReceiveTotals>(
|
|
453
|
+
"contacts:write:invoice:receive",
|
|
454
|
+
{
|
|
455
|
+
...head,
|
|
456
|
+
lines: [],
|
|
457
|
+
netTotal: eur(50),
|
|
458
|
+
vatRate: 20,
|
|
459
|
+
vatTotal: eur(0),
|
|
460
|
+
grossTotal: eur(999),
|
|
461
|
+
},
|
|
462
|
+
admin,
|
|
463
|
+
);
|
|
464
|
+
expect(totals.grossTotal).toEqual(eur(60));
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("a caller without the Admin role is rejected", async () => {
|
|
468
|
+
const error = await stack.http.writeErr(
|
|
469
|
+
"contacts:write:invoice:receive",
|
|
470
|
+
{ ...head, lines: [], netTotal: eur(50), vatRate: 20, vatTotal: eur(0), grossTotal: eur(0) },
|
|
471
|
+
viewer,
|
|
472
|
+
);
|
|
473
|
+
expect(error.code).toBe("access_denied");
|
|
474
|
+
});
|
|
475
|
+
});
|
|
400
476
|
```
|
|
401
477
|
|
|
402
478
|
### src/entities/contact.ts
|
|
@@ -529,6 +605,9 @@ export const productTable = buildEntityTable("product", productEntity);
|
|
|
529
605
|
// on its `lines` sub-fields (Issue #1835), plus a declarative
|
|
530
606
|
// entityEdit screen that renders the list end-to-end via
|
|
531
607
|
// EmbeddedListField, no custom-screen code required
|
|
608
|
+
// invoice-receive — actionForm whose net/VAT/gross head fields are derived
|
|
609
|
+
// from the lines (`derive`); the handler recomputes them with
|
|
610
|
+
// `withFormDerivations`, so client-sent totals are never trusted
|
|
532
611
|
|
|
533
612
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
534
613
|
import { contactEntity } from "./entities/contact";
|
|
@@ -536,6 +615,8 @@ import { invoiceEntity } from "./entities/invoice";
|
|
|
536
615
|
import { productEntity } from "./entities/product";
|
|
537
616
|
import { contactCreate } from "./handlers/contact-create.write";
|
|
538
617
|
import { contactDetail } from "./handlers/contact-detail.query";
|
|
618
|
+
import { invoiceReceive } from "./handlers/invoice-receive.write";
|
|
619
|
+
import { invoiceReceiveScreen } from "./screens/invoice-receive.screen";
|
|
539
620
|
|
|
540
621
|
export { contactEntity } from "./entities/contact";
|
|
541
622
|
export { invoiceEntity } from "./entities/invoice";
|
|
@@ -557,6 +638,7 @@ export const embeddedFeature = defineFeature("contacts", (r) => {
|
|
|
557
638
|
|
|
558
639
|
r.writeHandler(contactCreate);
|
|
559
640
|
r.queryHandler(contactDetail);
|
|
641
|
+
r.writeHandler(invoiceReceive);
|
|
560
642
|
|
|
561
643
|
r.crud("product", productEntity, { write: adminWrite, read: openRead });
|
|
562
644
|
r.crud("invoice", invoiceEntity, { write: adminWrite, read: openRead });
|
|
@@ -569,6 +651,8 @@ export const embeddedFeature = defineFeature("contacts", (r) => {
|
|
|
569
651
|
sections: [{ columns: 1, fields: ["customer", "lines"] }],
|
|
570
652
|
},
|
|
571
653
|
});
|
|
654
|
+
|
|
655
|
+
r.screen(invoiceReceiveScreen);
|
|
572
656
|
});
|
|
573
657
|
```
|
|
574
658
|
|
|
@@ -646,3 +730,110 @@ export const contactDetail = defineQueryHandler({
|
|
|
646
730
|
},
|
|
647
731
|
});
|
|
648
732
|
```
|
|
733
|
+
|
|
734
|
+
### src/handlers/invoice-receive.write.ts
|
|
735
|
+
|
|
736
|
+
```ts
|
|
737
|
+
import {
|
|
738
|
+
defineWriteHandler,
|
|
739
|
+
fieldToZod,
|
|
740
|
+
withFormDerivations,
|
|
741
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
742
|
+
import { z } from "zod";
|
|
743
|
+
import { invoiceReceiveLines, invoiceReceiveScreen } from "../screens/invoice-receive.screen";
|
|
744
|
+
|
|
745
|
+
const moneyPayload = z.object({ amount: z.number(), currency: z.string().min(1) });
|
|
746
|
+
|
|
747
|
+
// The preprocess recomputes net/VAT/gross before validation: totals sent by the
|
|
748
|
+
// client never reach the handler.
|
|
749
|
+
const receiveInput = withFormDerivations(
|
|
750
|
+
z.object({
|
|
751
|
+
supplier: z.string().min(1),
|
|
752
|
+
invoiceNumber: z.string().min(1),
|
|
753
|
+
lines: fieldToZod(invoiceReceiveLines, ["EUR"]),
|
|
754
|
+
netTotal: moneyPayload,
|
|
755
|
+
vatRate: z.number().optional(),
|
|
756
|
+
vatTotal: moneyPayload,
|
|
757
|
+
grossTotal: moneyPayload,
|
|
758
|
+
}),
|
|
759
|
+
invoiceReceiveScreen,
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
export const invoiceReceive = defineWriteHandler({
|
|
763
|
+
name: "invoice:receive",
|
|
764
|
+
schema: receiveInput,
|
|
765
|
+
access: { roles: ["Admin"] },
|
|
766
|
+
handler: async (event) => ({
|
|
767
|
+
isSuccess: true,
|
|
768
|
+
data: {
|
|
769
|
+
netTotal: event.payload.netTotal,
|
|
770
|
+
vatTotal: event.payload.vatTotal,
|
|
771
|
+
grossTotal: event.payload.grossTotal,
|
|
772
|
+
},
|
|
773
|
+
}),
|
|
774
|
+
});
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### src/screens/invoice-receive.screen.ts
|
|
778
|
+
|
|
779
|
+
```ts
|
|
780
|
+
import {
|
|
781
|
+
type ActionFormScreenDefinition,
|
|
782
|
+
createDecimalField,
|
|
783
|
+
createEmbeddedListField,
|
|
784
|
+
createTextField,
|
|
785
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
786
|
+
|
|
787
|
+
const euro = { kind: "literal", code: "EUR" } as const;
|
|
788
|
+
|
|
789
|
+
export const invoiceReceiveLines = createEmbeddedListField(
|
|
790
|
+
{
|
|
791
|
+
description: { type: "text", required: true },
|
|
792
|
+
amount: { type: "money", required: true },
|
|
793
|
+
vatRate: { type: "decimal", scale: 2, required: true },
|
|
794
|
+
vatAmount: { type: "money" },
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
derived: { vatAmount: { op: "percent", from: ["amount", "vatRate"] } },
|
|
798
|
+
totals: ["amount", "vatAmount"],
|
|
799
|
+
},
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
// Net/VAT/gross are derived: with line items they are the sum of the lines,
|
|
803
|
+
// without lines the net amount and the VAT rate are typed and VAT/gross follow.
|
|
804
|
+
export const invoiceReceiveScreen: ActionFormScreenDefinition = {
|
|
805
|
+
id: "invoice-receive",
|
|
806
|
+
type: "actionForm",
|
|
807
|
+
handler: "contacts:write:invoice:receive",
|
|
808
|
+
fields: {
|
|
809
|
+
supplier: createTextField({ required: true, personal: "tenant", find: "none" }),
|
|
810
|
+
invoiceNumber: createTextField({
|
|
811
|
+
required: true,
|
|
812
|
+
personal: false,
|
|
813
|
+
reason: "is_business_data",
|
|
814
|
+
}),
|
|
815
|
+
lines: invoiceReceiveLines,
|
|
816
|
+
netTotal: { type: "money", currency: euro },
|
|
817
|
+
vatRate: createDecimalField({ precision: 5, scale: 2 }),
|
|
818
|
+
vatTotal: { type: "money", currency: euro },
|
|
819
|
+
grossTotal: { type: "money", currency: euro },
|
|
820
|
+
},
|
|
821
|
+
derive: {
|
|
822
|
+
netTotal: { op: "listSum", list: "lines", cell: "amount" },
|
|
823
|
+
vatTotal: {
|
|
824
|
+
op: "listSum",
|
|
825
|
+
list: "lines",
|
|
826
|
+
cell: "vatAmount",
|
|
827
|
+
whenEmpty: { op: "percent", from: ["netTotal", "vatRate"] },
|
|
828
|
+
},
|
|
829
|
+
grossTotal: { op: "sum", from: ["netTotal", "vatTotal"] },
|
|
830
|
+
},
|
|
831
|
+
layout: {
|
|
832
|
+
sections: [
|
|
833
|
+
{ columns: 2, fields: ["supplier", "invoiceNumber"] },
|
|
834
|
+
{ columns: 1, fields: ["lines"] },
|
|
835
|
+
{ columns: 2, fields: ["netTotal", "vatRate", "vatTotal", "grossTotal"] },
|
|
836
|
+
],
|
|
837
|
+
},
|
|
838
|
+
};
|
|
839
|
+
```
|
|
@@ -55,7 +55,7 @@ handler as above. Register `notesHistoryClient()` once (for its component +
|
|
|
55
55
|
i18n), then mount it either way:
|
|
56
56
|
|
|
57
57
|
```tsx illustration
|
|
58
|
-
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer
|
|
58
|
+
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer/web";
|
|
59
59
|
import { notesHistoryClient, NotesSection, NOTES_SECTION_EXTENSION_NAME } from "@cosmicdrift/kumiko-bundled-features/notes-history/web";
|
|
60
60
|
|
|
61
61
|
// once, at app boot — required even for standalone use (registers i18n):
|
|
@@ -56,7 +56,7 @@ as above. Register `tagsClient()` once (for its component + i18n), then mount it
|
|
|
56
56
|
either way:
|
|
57
57
|
|
|
58
58
|
```tsx illustration
|
|
59
|
-
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer
|
|
59
|
+
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer/web";
|
|
60
60
|
import { tagsClient, TagSection, TAGS_SECTION_EXTENSION_NAME } from "@cosmicdrift/kumiko-bundled-features/tags/web";
|
|
61
61
|
|
|
62
62
|
// once, at app boot — required even for standalone use (registers i18n):
|
|
@@ -54,7 +54,7 @@ itself needs no client registration):
|
|
|
54
54
|
```tsx illustration
|
|
55
55
|
import { userProfileClient } from "@cosmicdrift/kumiko-bundled-features/user-profile/web";
|
|
56
56
|
import { emailPasswordClient } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";
|
|
57
|
-
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer
|
|
57
|
+
import { createKumikoApp } from "@cosmicdrift/kumiko-renderer/web";
|
|
58
58
|
|
|
59
59
|
createKumikoApp({
|
|
60
60
|
clientFeatures: [emailPasswordClient(), userProfileClient()],
|
|
@@ -338,11 +338,11 @@ export const listingsFeature = defineFeature("listings", (r) => {
|
|
|
338
338
|
// A `kind: "extension"` section only resolves through
|
|
339
339
|
// clientFeatures.extensionSectionComponents (createKumikoApp → ExtensionSectionsProvider)
|
|
340
340
|
// — never imported directly from feature.ts, which keeps the server file
|
|
341
|
-
// free of the renderer
|
|
341
|
+
// free of the renderer/web graph. Without registering this export, an app
|
|
342
342
|
// mounting this recipe gets RenderEdit's fallback placeholder instead of
|
|
343
343
|
// the real review step.
|
|
344
344
|
|
|
345
|
-
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer
|
|
345
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer/web";
|
|
346
346
|
import { ListingReviewSection } from "./listing-review-section";
|
|
347
347
|
|
|
348
348
|
export const listingsClient: ClientFeatureDefinition = {
|
|
@@ -355,13 +355,13 @@ export const listingsClient: ClientFeatureDefinition = {
|
|
|
355
355
|
|
|
356
356
|
```tsx
|
|
357
357
|
import type { ExtensionSectionProps } from "@cosmicdrift/kumiko-renderer";
|
|
358
|
-
import { DetailList } from "@cosmicdrift/kumiko-renderer
|
|
358
|
+
import { DetailList } from "@cosmicdrift/kumiko-renderer/web";
|
|
359
359
|
import type { ReactNode } from "react";
|
|
360
360
|
|
|
361
361
|
// Registered under the `__component` name the feature's review section
|
|
362
362
|
// points at (see feature.ts) — never imported by feature.ts itself, the
|
|
363
363
|
// string name is the only link so the server file stays free of the
|
|
364
|
-
// renderer
|
|
364
|
+
// renderer/web graph.
|
|
365
365
|
export function ListingReviewSection({ values }: ExtensionSectionProps): ReactNode {
|
|
366
366
|
return (
|
|
367
367
|
<DetailList
|
package/agents/features.md
CHANGED
|
@@ -228,7 +228,7 @@ Requires: `file-derivatives`
|
|
|
228
228
|
|
|
229
229
|
## document-designer
|
|
230
230
|
|
|
231
|
-
Template overview and layout editor: lists the mail, PDF and layout templates of a tenant (platform admins see the system templates and how many tenants customised each) and edits the mail layout and the PDF letterhead in the browser. Mount after template-resolver and, for the branding rail, managed-pages. Client: `documentDesignerClient()`; admin-shell lists the navs with `includeDocumentDesigner`.
|
|
231
|
+
Template overview and layout editor: lists the mail, PDF and layout templates of a tenant (platform admins see the system templates and how many tenants customised each) and edits the mail layout and the PDF letterhead in the browser. Mount after template-resolver and, for the branding rail, managed-pages. Client: `documentDesignerClient()`; admin-shell lists the navs with `includeDocumentDesigner`. The tenant overview and the editors are for SystemAdmin only; the `access` option adds roles (normally `templateEditAccess(tenantEditable)` of the template-resolver mount), and the server still enforces per template which of them may save.
|
|
232
232
|
|
|
233
233
|
Category: content
|
|
234
234
|
|
|
@@ -644,7 +644,7 @@ Category: data
|
|
|
644
644
|
|
|
645
645
|
## template-resolver
|
|
646
646
|
|
|
647
|
-
Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`). Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away. Text an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation. How a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning). A collection is tenant-wide by default. With `ownership: "user"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side. `validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only. `preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted. Images used by a template (`<img data-resource="logo">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: "attachment"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10. Replaces the former `text-content` feature; its blocks now live here as kind `text-block`.
|
|
647
|
+
Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`). Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away. Editing templates is for SystemAdmin only. A TenantAdmin edits only the templates the app unlocks at the mount site, `createTemplateResolverFeature({ tenantEditable: [{ kind: "mail", slug: "status-update", access: { roles: ["TenantAdmin"] } }] })`, and every write handler enforces that on the server (override, draft, publish, archive, reset, copy-locale, import, resources, `set`). Each entry carries the `access` roles allowed to edit it, because a bundled feature cannot know the host's roles. Reading and delivery do not depend on it: a tenant override that exists is served. A collection mounted without `access` is SystemAdmin only. Text an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation. How a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning). A collection is tenant-wide by default. With `ownership: "user"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side. `validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only. `preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted. `send-test-status` tells an editor beforehand whether that send can work for the caller (`canSend`, else `reason` `no_channel` or `no_address`), never returning the address. Images used by a template (`<img data-resource="logo">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: "attachment"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10. Replaces the former `text-content` feature; its blocks now live here as kind `text-block`.
|
|
648
648
|
|
|
649
649
|
Category: notifications
|
|
650
650
|
|
package/agents/widgets.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
<!-- Generated by `bun run gen:agent-docs` from packages/renderer
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from packages/renderer/src/web/index.ts. Do not edit. -->
|
|
2
2
|
|
|
3
3
|
# Widgets
|
|
4
4
|
|
|
5
|
-
Value exports of `@cosmicdrift/kumiko-renderer
|
|
5
|
+
Value exports of `@cosmicdrift/kumiko-renderer/web`.
|
|
6
6
|
|
|
7
7
|
## Components
|
|
8
8
|
|
|
@@ -55,6 +55,7 @@ Value exports of `@cosmicdrift/kumiko-renderer-web`.
|
|
|
55
55
|
| `NavTree` | |
|
|
56
56
|
| `NumberField` | Zahlenfeld = Field + Input(kind:"number") in einem — nimmt der Screen die wiederholte id/name/value/onChange-Verdrahtung ab. |
|
|
57
57
|
| `PageSection` | |
|
|
58
|
+
| `PdfPreview` | |
|
|
58
59
|
| `PercentField` | |
|
|
59
60
|
| `PhotoSlots` | Guided photo capture: one tile per requested shot in a two-column grid, each its own camera/gallery picker, filled tiles show the thumbnail. |
|
|
60
61
|
| `PlainContentEditor` | |
|
|
@@ -174,7 +175,7 @@ Value exports of `@cosmicdrift/kumiko-renderer-web`.
|
|
|
174
175
|
| --- | --- |
|
|
175
176
|
| `BROWSER_LOCALE_STORAGE_KEY` | |
|
|
176
177
|
| `STATUS_TONE_TEXT` | |
|
|
177
|
-
| `cssVarTokens` | Tokens
|
|
178
|
+
| `cssVarTokens` | Tokens with var-string values. |
|
|
178
179
|
| `defaultPrimitives` | |
|
|
179
180
|
| `defaultTokens` | Default-Tokens — identisch zu `cssVarTokens` (var-string-refs). |
|
|
180
181
|
| `lightTokens` | |
|
package/dist/api/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { deleteTenantCookie, setTenantCookie } from "./anonymous-cookie.ts";
|
|
|
3
3
|
export { collectAnonymousLiveEntities } from "./anonymous-live-entities.ts";
|
|
4
4
|
export { LOCALE_HEADER_NAME, NO_ROUTE_MATCH_HEADER_NAME } from "./api-constants.ts";
|
|
5
5
|
export type { AnonymousAccessConfig, AnonymousAccessResolved, AuthMiddlewareOptions, AuthSessionChecker, AuthSessionCheckResult, AuthSessionStatus, TenantExists, TenantLifecycleStatusResolver, TenantResolver, TokenVerifier, } from "./auth-middleware.ts";
|
|
6
|
-
export { authMiddleware, getUser, PAT_TOKEN_PREFIX, sessionCheckStatus, } from "./auth-middleware.ts";
|
|
6
|
+
export { authMiddleware, CSRF_HEADER_NAME, getUser, PAT_TOKEN_PREFIX, sessionCheckStatus, } from "./auth-middleware.ts";
|
|
7
7
|
export type { AuthRoutesConfig, LoginRateLimiter, SessionChecker, SessionCreator, SessionMassRevoker, SessionMetadata, SessionRevoker, } from "./auth-routes.ts";
|
|
8
8
|
export { createAuthRoutes, createInMemoryLoginRateLimiter, createRedisLoginRateLimiter, } from "./auth-routes.ts";
|
|
9
9
|
export type { ClientIpHeaderSource, ClientIpResolver } from "./client-ip.ts";
|
package/dist/api/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { deleteTenantCookie, setTenantCookie } from "./anonymous-cookie.js";
|
|
2
2
|
export { collectAnonymousLiveEntities } from "./anonymous-live-entities.js";
|
|
3
3
|
export { LOCALE_HEADER_NAME, NO_ROUTE_MATCH_HEADER_NAME } from "./api-constants.js";
|
|
4
|
-
export { authMiddleware, getUser, PAT_TOKEN_PREFIX, sessionCheckStatus, } from "./auth-middleware.js";
|
|
4
|
+
export { authMiddleware, CSRF_HEADER_NAME, getUser, PAT_TOKEN_PREFIX, sessionCheckStatus, } from "./auth-middleware.js";
|
|
5
5
|
export { createAuthRoutes, createInMemoryLoginRateLimiter, createRedisLoginRateLimiter, } from "./auth-routes.js";
|
|
6
6
|
export { assertValidTrustedProxyHops, clientIpSourceFromHonoContext, createClientIpResolver, extractSocketAddress, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, UNKNOWN_CLIENT_IP, } from "./client-ip.js";
|
|
7
7
|
export { ExtraRouteEntries, ExtraRouteRejection, signatureRoute } from "./extra-route.js";
|
package/dist/api/routes.js
CHANGED
|
@@ -11,8 +11,8 @@ import { patAllows } from "./pat-scope.js";
|
|
|
11
11
|
import { requestContext } from "./request-context.js";
|
|
12
12
|
import { SSE_HEARTBEAT_INTERVAL_MS } from "./sse-route.js";
|
|
13
13
|
// SSE frame event names for POST /api/stream. Parallel definition lives in
|
|
14
|
-
// @cosmicdrift/kumiko-headless (
|
|
15
|
-
// cannot depend on
|
|
14
|
+
// @cosmicdrift/kumiko-renderer/headless (the live dispatcher imports that one) — framework
|
|
15
|
+
// cannot depend on the renderer for four string literals.
|
|
16
16
|
export const StreamFrame = {
|
|
17
17
|
chunk: "chunk",
|
|
18
18
|
ping: "ping",
|
package/dist/api/server.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createRecordWriteCheck } from "../files/record-write-check.js";
|
|
|
18
18
|
import { createFallbackLogger } from "../logging/index.js";
|
|
19
19
|
import { createNoopProvider, registerStandardMetrics, wrapRedisClient, } from "../observability/index.js";
|
|
20
20
|
import { resolveTenantLifecyclePlugin } from "../pipeline/active-membership.js";
|
|
21
|
+
import { configAccessorFactoryOf } from "../pipeline/config-accessor-factory.js";
|
|
21
22
|
import { createDispatcher } from "../pipeline/dispatcher.js";
|
|
22
23
|
import { SHARED_INSTANCE_SENTINEL } from "../pipeline/event-consumer-state.js";
|
|
23
24
|
import { createEventDispatcher } from "../pipeline/event-dispatcher.js";
|
|
@@ -63,7 +64,7 @@ export function withFileProviderResolver(registry, context, cacheSync) {
|
|
|
63
64
|
...context,
|
|
64
65
|
_fileProviderResolver: makeFileProviderResolver({
|
|
65
66
|
registry,
|
|
66
|
-
_configAccessorFactory: context
|
|
67
|
+
_configAccessorFactory: configAccessorFactoryOf(context),
|
|
67
68
|
secrets: context.secrets,
|
|
68
69
|
db: context.db,
|
|
69
70
|
...(cacheSync ? { cacheSync } : {}),
|
package/dist/config/index.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type AppConfig = {
|
|
|
27
27
|
readonly features: readonly FeatureDefinition[];
|
|
28
28
|
/** Features mounted only in production on top of `features`. */
|
|
29
29
|
readonly prodFeatures?: readonly FeatureDefinition[];
|
|
30
|
+
/** Whether the app boots with the bundled auth features (config/user/tenant/auth-email-password)
|
|
31
|
+
* that composeFeatures adds; the schema tooling needs their tables. Unset: derived from the
|
|
32
|
+
* feature list. */
|
|
33
|
+
readonly includeBundled?: boolean;
|
|
30
34
|
readonly client: AppClientConfig;
|
|
31
35
|
readonly ports: AppPorts;
|
|
32
36
|
/** Passed to validateBoot by `kumiko check` and, via the app's bin files, by the runners. */
|
package/dist/db/index.d.ts
CHANGED
|
@@ -35,5 +35,5 @@ export { diffReplayAgainstSnapshot, type ReplayedSchema, type ReplayedTable, typ
|
|
|
35
35
|
export { findCommentedDropTables, isRetiredFrameworkTable, type RetiredFrameworkTable, retiredFrameworkTables, } from "./retired-framework-tables.ts";
|
|
36
36
|
export { tableExists } from "./schema-inspection.ts";
|
|
37
37
|
export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, type EntityRow, type EntityTable, physicalColumnName, toSnakeCase, toTableName, } from "./table-builder.ts";
|
|
38
|
-
export type { CreateTenantDbOptions, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.ts";
|
|
39
|
-
export { castTenantRows, createSystemDbView, createTenantDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.ts";
|
|
38
|
+
export type { CreateTenantDbOptions, OutsideTransactionContext, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.ts";
|
|
39
|
+
export { castTenantRows, createSystemDbView, createTenantDb, hasOutsideTransactionDb, requireOutsideTransactionDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.ts";
|
package/dist/db/index.js
CHANGED
|
@@ -28,4 +28,4 @@ export { diffReplayAgainstSnapshot, replayMigrationsDir, } from "./replay-migrat
|
|
|
28
28
|
export { findCommentedDropTables, isRetiredFrameworkTable, retiredFrameworkTables, } from "./retired-framework-tables.js";
|
|
29
29
|
export { tableExists } from "./schema-inspection.js";
|
|
30
30
|
export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, physicalColumnName, toSnakeCase, toTableName, } from "./table-builder.js";
|
|
31
|
-
export { castTenantRows, createSystemDbView, createTenantDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.js";
|
|
31
|
+
export { castTenantRows, createSystemDbView, createTenantDb, hasOutsideTransactionDb, requireOutsideTransactionDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.js";
|
|
@@ -111,8 +111,18 @@ export function splitSqlStatements(sqlText) {
|
|
|
111
111
|
current += ch;
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
|
-
if (ch === "$"
|
|
115
|
-
|
|
114
|
+
if (ch === "$") {
|
|
115
|
+
const opening = /^\$([A-Za-z_]\w*)?\$/.exec(sqlText.slice(i))?.[0];
|
|
116
|
+
if (opening !== undefined) {
|
|
117
|
+
// Body is copied verbatim: `;` and quotes inside a DO/function body belong to it.
|
|
118
|
+
const closeAt = sqlText.indexOf(opening, i + opening.length);
|
|
119
|
+
if (closeAt === -1) {
|
|
120
|
+
throw new Error(`splitSqlStatements: unterminated dollar-quoted body ${opening} — migration SQL is malformed, refusing to split`);
|
|
121
|
+
}
|
|
122
|
+
current += sqlText.slice(i, closeAt + opening.length);
|
|
123
|
+
i = closeAt + opening.length - 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
116
126
|
}
|
|
117
127
|
if (ch === ";") {
|
|
118
128
|
statements.push(current);
|
|
@@ -122,8 +132,6 @@ export function splitSqlStatements(sqlText) {
|
|
|
122
132
|
current += ch;
|
|
123
133
|
}
|
|
124
134
|
if (state === "blockComment" || state === "singleQuote" || state === "doubleQuote") {
|
|
125
|
-
// Does not track dollar-quoted (`$$...$$`) bodies — none of this repo's
|
|
126
|
-
// migrations use them; add that state if one ever does.
|
|
127
135
|
throw new Error(`splitSqlStatements: unterminated ${state} — migration SQL is malformed, refusing to split`);
|
|
128
136
|
}
|
|
129
137
|
statements.push(current);
|