@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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.
Files changed (281) hide show
  1. package/AGENTS.md +338 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +446 -0
  4. package/agents/examples/anonymous-access.md +288 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +319 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +315 -0
  20. package/agents/examples/custom-handlers.md +444 -0
  21. package/agents/examples/dashboard-stats.md +229 -0
  22. package/agents/examples/delivery-notifications.md +432 -0
  23. package/agents/examples/designer-demo.md +86 -0
  24. package/agents/examples/e2e-generator.md +211 -0
  25. package/agents/examples/embedded-entity-form.md +598 -0
  26. package/agents/examples/embedded.md +649 -0
  27. package/agents/examples/encrypted-tenant-config.md +414 -0
  28. package/agents/examples/error-contract.md +535 -0
  29. package/agents/examples/event-sourcing.md +1287 -0
  30. package/agents/examples/feature-toggles.md +274 -0
  31. package/agents/examples/field-access.md +271 -0
  32. package/agents/examples/files-post-processing.md +433 -0
  33. package/agents/examples/i18n.md +166 -0
  34. package/agents/examples/idempotency.md +179 -0
  35. package/agents/examples/inbound-mail-triage.md +315 -0
  36. package/agents/examples/lane-routing.md +246 -0
  37. package/agents/examples/ledger-banking.md +215 -0
  38. package/agents/examples/ledger-invoicing.md +236 -0
  39. package/agents/examples/legal-pages.md +393 -0
  40. package/agents/examples/lifecycle-hooks.md +368 -0
  41. package/agents/examples/managed-config.md +428 -0
  42. package/agents/examples/managed-pages.md +353 -0
  43. package/agents/examples/notes-history-basic.md +346 -0
  44. package/agents/examples/ownership.md +404 -0
  45. package/agents/examples/pdf-audit-export.md +306 -0
  46. package/agents/examples/pipeline-basics.md +792 -0
  47. package/agents/examples/public-share-token.md +351 -0
  48. package/agents/examples/rate-limiting.md +211 -0
  49. package/agents/examples/realtime-sse.md +187 -0
  50. package/agents/examples/record-detail-layout.md +297 -0
  51. package/agents/examples/reference-data.md +151 -0
  52. package/agents/examples/relations.md +280 -0
  53. package/agents/examples/screens-nav.md +353 -0
  54. package/agents/examples/search.md +233 -0
  55. package/agents/examples/secrets-demo.md +417 -0
  56. package/agents/examples/security-baseline.md +187 -0
  57. package/agents/examples/seed-migration.md +99 -0
  58. package/agents/examples/session-revocation.md +250 -0
  59. package/agents/examples/state-machine.md +920 -0
  60. package/agents/examples/tags-basic.md +401 -0
  61. package/agents/examples/tenant-isolation.md +173 -0
  62. package/agents/examples/tenant-settings.md +275 -0
  63. package/agents/examples/tier-admin.md +347 -0
  64. package/agents/examples/timezones.md +366 -0
  65. package/agents/examples/user-data-rights.md +418 -0
  66. package/agents/examples/user-profile.md +226 -0
  67. package/agents/examples/webhook-step.md +597 -0
  68. package/agents/examples/wizard-form.md +379 -0
  69. package/agents/examples/workflow-engine.md +1731 -0
  70. package/agents/examples/writeform-section.md +300 -0
  71. package/agents/examples.md +75 -0
  72. package/agents/features.md +768 -0
  73. package/agents/widgets.md +181 -0
  74. package/dist/api/auth-middleware.d.ts +2 -0
  75. package/dist/api/auth-middleware.js +7 -0
  76. package/dist/api/auth-routes.d.ts +0 -1
  77. package/dist/api/auth-routes.js +49 -46
  78. package/dist/api/extra-route.d.ts +3 -0
  79. package/dist/api/index.d.ts +2 -2
  80. package/dist/api/index.js +1 -1
  81. package/dist/api/routes.js +46 -17
  82. package/dist/api/server.d.ts +4 -3
  83. package/dist/api/server.js +39 -10
  84. package/dist/api/session-cookies.d.ts +1 -0
  85. package/dist/api/session-cookies.js +20 -5
  86. package/dist/api/sse-route.js +16 -1
  87. package/dist/bun-db/connection.js +3 -16
  88. package/dist/bun-db/index.d.ts +1 -1
  89. package/dist/bun-db/index.js +1 -1
  90. package/dist/bun-db/query.d.ts +15 -3
  91. package/dist/bun-db/query.js +76 -24
  92. package/dist/crypto/owner-transition-rekey.d.ts +1 -0
  93. package/dist/crypto/owner-transition-rekey.js +7 -3
  94. package/dist/crypto/pii-field-encryption.d.ts +1 -0
  95. package/dist/crypto/pii-field-encryption.js +9 -2
  96. package/dist/crypto/subject-resolver.d.ts +1 -0
  97. package/dist/crypto/subject-resolver.js +7 -0
  98. package/dist/db/api.d.ts +4 -3
  99. package/dist/db/apply-entity-event.js +26 -2
  100. package/dist/db/blind-index-cleanup.js +2 -1
  101. package/dist/db/bun-provider.js +9 -18
  102. package/dist/db/collect-table-metas.js +11 -7
  103. package/dist/db/dialect.d.ts +1 -1
  104. package/dist/db/dialect.js +2 -0
  105. package/dist/db/eagerload.js +4 -3
  106. package/dist/db/event-store-executor-context.d.ts +6 -6
  107. package/dist/db/event-store-executor-context.js +25 -22
  108. package/dist/db/event-store-executor-read.js +31 -16
  109. package/dist/db/event-store-executor-write.d.ts +1 -1
  110. package/dist/db/event-store-executor-write.js +84 -63
  111. package/dist/db/index.d.ts +2 -1
  112. package/dist/db/index.js +2 -1
  113. package/dist/db/lock-guard.d.ts +4 -0
  114. package/dist/db/lock-guard.js +14 -0
  115. package/dist/db/migrate-generator.js +1 -1
  116. package/dist/db/migrate-runner.d.ts +1 -0
  117. package/dist/db/migrate-runner.js +64 -38
  118. package/dist/db/queries/backfill-pii.js +63 -25
  119. package/dist/db/queries/event-consumer.d.ts +6 -6
  120. package/dist/db/queries/event-consumer.js +2 -1
  121. package/dist/db/queries/event-store-transfer.js +15 -4
  122. package/dist/db/queries/event-store.d.ts +1 -1
  123. package/dist/db/queries/event-store.js +11 -10
  124. package/dist/db/queries/pii-backfill-state.js +3 -2
  125. package/dist/db/queries/postgres-array-literal.d.ts +2 -0
  126. package/dist/db/queries/postgres-array-literal.js +6 -0
  127. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  128. package/dist/db/queries/projection-rebuild.js +3 -2
  129. package/dist/db/queries/shadow-swap.d.ts +1 -1
  130. package/dist/db/queries/shadow-swap.js +12 -8
  131. package/dist/db/query.d.ts +1 -1
  132. package/dist/db/query.js +1 -1
  133. package/dist/db/retired-framework-tables.js +22 -4
  134. package/dist/db/table-builder.js +6 -2
  135. package/dist/db/tenant-db-runner.d.ts +4 -0
  136. package/dist/db/tenant-db-runner.js +17 -0
  137. package/dist/db/tenant-db.d.ts +4 -0
  138. package/dist/db/tenant-db.js +50 -5
  139. package/dist/derivatives/delete-stored-file.d.ts +4 -0
  140. package/dist/derivatives/delete-stored-file.js +9 -0
  141. package/dist/derivatives/derivatives-context.js +1 -1
  142. package/dist/derivatives/index.d.ts +1 -1
  143. package/dist/derivatives/index.js +1 -1
  144. package/dist/engine/boot-validator/access-declarations.js +2 -2
  145. package/dist/engine/boot-validator/api-ext.js +3 -3
  146. package/dist/engine/boot-validator/boot-check.js +1 -1
  147. package/dist/engine/boot-validator/config-deps.js +13 -13
  148. package/dist/engine/boot-validator/detail-screens.js +1 -1
  149. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  150. package/dist/engine/boot-validator/entity-handler.js +87 -35
  151. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  152. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  153. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  154. package/dist/engine/boot-validator/nav.js +1 -1
  155. package/dist/engine/boot-validator/pii-retention.js +8 -8
  156. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  157. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  158. package/dist/engine/boot-validator/query-refs.js +14 -2
  159. package/dist/engine/boot-validator/screens.d.ts +2 -1
  160. package/dist/engine/boot-validator/screens.js +153 -95
  161. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  162. package/dist/engine/dedupe-features.js +31 -4
  163. package/dist/engine/define-handler.d.ts +2 -2
  164. package/dist/engine/define-handler.js +8 -1
  165. package/dist/engine/entity-handlers.js +14 -7
  166. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  167. package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
  168. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  169. package/dist/engine/feature-ast/render.js +10 -2
  170. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  171. package/dist/engine/feature-entity-handlers.js +12 -3
  172. package/dist/engine/handler-field-references.d.ts +21 -2
  173. package/dist/engine/handler-field-references.js +55 -17
  174. package/dist/engine/index.d.ts +3 -3
  175. package/dist/engine/index.js +1 -1
  176. package/dist/engine/schema-builder.js +2 -1
  177. package/dist/engine/screen-helpers.d.ts +1 -0
  178. package/dist/engine/screen-helpers.js +2 -2
  179. package/dist/engine/soft-delete-cleanup.js +10 -3
  180. package/dist/engine/steps/_read-source.js +5 -2
  181. package/dist/engine/steps/retry.js +2 -1
  182. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  183. package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
  184. package/dist/engine/template-variables.d.ts +3 -0
  185. package/dist/engine/template-variables.js +3 -0
  186. package/dist/engine/tier-resolver-extension.d.ts +1 -0
  187. package/dist/engine/tier-resolver-extension.js +6 -0
  188. package/dist/engine/types/index.d.ts +1 -1
  189. package/dist/entrypoint/index.d.ts +2 -2
  190. package/dist/env/_zod-introspect.d.ts +1 -0
  191. package/dist/errors/classes.js +1 -0
  192. package/dist/errors/docs-url.js +6 -6
  193. package/dist/errors/index.d.ts +1 -1
  194. package/dist/errors/index.js +1 -1
  195. package/dist/errors/kumiko-error.d.ts +2 -0
  196. package/dist/errors/kumiko-error.js +2 -0
  197. package/dist/errors/subject-erased.d.ts +1 -0
  198. package/dist/errors/subject-erased.js +6 -0
  199. package/dist/errors/write-error-info.js +2 -0
  200. package/dist/event-store/admin-api.js +3 -2
  201. package/dist/event-store/errors.d.ts +9 -0
  202. package/dist/event-store/errors.js +20 -0
  203. package/dist/event-store/event-store.d.ts +6 -0
  204. package/dist/event-store/event-store.js +28 -6
  205. package/dist/event-store/index.d.ts +1 -1
  206. package/dist/event-store/index.js +1 -1
  207. package/dist/event-store/tenant-db-append.js +2 -2
  208. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  209. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  210. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  211. package/dist/files/bind-pending-file-refs.js +3 -3
  212. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  213. package/dist/files/bind-referenced-files.write.js +22 -13
  214. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  215. package/dist/files/create-file-ref-from-bytes.js +67 -4
  216. package/dist/files/create-file-ref.d.ts +24 -1
  217. package/dist/files/create-file-ref.js +67 -25
  218. package/dist/files/dispose-removed-file-refs.js +21 -6
  219. package/dist/files/feature.d.ts +1 -0
  220. package/dist/files/feature.js +2 -1
  221. package/dist/files/file-handle.d.ts +1 -1
  222. package/dist/files/file-handle.js +12 -0
  223. package/dist/files/file-routes.js +10 -6
  224. package/dist/files/index.d.ts +3 -3
  225. package/dist/files/index.js +2 -2
  226. package/dist/files/load-save-file-refs.d.ts +10 -0
  227. package/dist/files/load-save-file-refs.js +47 -0
  228. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  229. package/dist/files/purge-expired-file-ref-bytes.js +46 -21
  230. package/dist/files/resolve-content-type.js +40 -4
  231. package/dist/files/storage-tracking.js +5 -4
  232. package/dist/files/types.js +5 -8
  233. package/dist/files/zip-entry-names.d.ts +1 -0
  234. package/dist/files/zip-entry-names.js +46 -0
  235. package/dist/jobs/index.d.ts +1 -1
  236. package/dist/jobs/job-runner.d.ts +5 -1
  237. package/dist/jobs/job-runner.js +124 -37
  238. package/dist/observability/escape-hatch-report.d.ts +2 -1
  239. package/dist/observability/escape-hatch-report.js +48 -7
  240. package/dist/observability/index.d.ts +1 -1
  241. package/dist/observability/index.js +1 -1
  242. package/dist/observability/metrics-handle.d.ts +1 -0
  243. package/dist/observability/metrics-handle.js +7 -0
  244. package/dist/observability/standard-metrics.js +4 -4
  245. package/dist/pipeline/cascade-handler.js +5 -2
  246. package/dist/pipeline/dispatch-batch.js +79 -31
  247. package/dist/pipeline/dispatch-query.d.ts +2 -1
  248. package/dist/pipeline/dispatch-query.js +6 -6
  249. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  250. package/dist/pipeline/dispatch-shared.js +136 -48
  251. package/dist/pipeline/dispatch-stream.js +10 -4
  252. package/dist/pipeline/dispatch-write.js +34 -22
  253. package/dist/pipeline/dispatcher.js +3 -1
  254. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  255. package/dist/pipeline/event-dispatcher.js +11 -9
  256. package/dist/pipeline/event-retention.d.ts +1 -0
  257. package/dist/pipeline/event-retention.js +7 -1
  258. package/dist/pipeline/index.d.ts +1 -0
  259. package/dist/pipeline/index.js +1 -0
  260. package/dist/pipeline/notify-scope.js +3 -1
  261. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  262. package/dist/pipeline/pre-transaction-reservations.js +29 -9
  263. package/dist/rate-limit/bucket.d.ts +3 -3
  264. package/dist/rate-limit/bucket.js +14 -4
  265. package/dist/redis/cache-sync-bus.js +9 -3
  266. package/dist/schema-cli.js +12 -1
  267. package/dist/search/meilisearch-adapter.js +36 -9
  268. package/dist/stack/db.d.ts +2 -2
  269. package/dist/stack/test-stack.js +2 -1
  270. package/dist/testing/index.d.ts +1 -0
  271. package/dist/testing/index.js +1 -0
  272. package/dist/testing/multipart-helper.d.ts +6 -0
  273. package/dist/testing/multipart-helper.js +1 -1
  274. package/dist/testing/zip-fixture.d.ts +1 -0
  275. package/dist/testing/zip-fixture.js +33 -0
  276. package/dist/ui-types/card-column-roles.js +4 -2
  277. package/dist/ui-types/index.d.ts +1 -1
  278. package/dist/ui-types/index.js +1 -1
  279. package/dist/upgrade-cli.js +46 -13
  280. package/package.json +7 -5
  281. package/src/changes.json +512 -8
@@ -0,0 +1,432 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/delivery-notifications. Do not edit. -->
2
+
3
+ # delivery-notifications
4
+
5
+ Delivery flows plus notifications. Package tracking, minus the tracking.
6
+
7
+ ## README
8
+
9
+ # Sample: Delivery Notifications
10
+
11
+ Shows how a feature sends notifications to multiple channels (InApp + Email + Push) without knowing the channel internals, and how the mail text lives in the template store where a tenant can override it.
12
+
13
+ ## What you learn here
14
+
15
+ **Declarative notifications:** `r.notification()` declares the mail variables, the default mail text, the subject and the short inApp/push texts in one place.
16
+
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
+
19
+ **Tenant override:** A tenant admin overrides the text through the template-resolver handlers (`upsertTenant`, then `publish`). The next mail for that tenant uses the override; the system text stays for everyone else.
20
+
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
+
23
+ **Swappable transports:** Email uses `InMemoryTransport` in tests; in a production setup it would be SMTP. Same interface, different implementation.
24
+
25
+ ## Feature composition
26
+
27
+ The test loads these features together:
28
+
29
+ ```
30
+ config → tenant/system config
31
+ tenant → memberships (delivery needs tenant:query:resolve-user-ids)
32
+ template-resolver → template store, override handlers
33
+ renderer-foundation + renderer-mail-html → renders kind "mail"
34
+ delivery → core: ctx.notify, DeliveryLog, Preferences, extension points
35
+ channel-inApp / channel-email / channel-push
36
+ support → our business logic (tickets + the notification declaration)
37
+ ```
38
+
39
+ The `support` feature `r.requires("delivery")`: channels are features that attach to delivery, but `support` doesn't need to know them.
40
+
41
+ ## Flow
42
+
43
+ 1. The boot seed stores the `ticket-assigned` mail template
44
+ 2. Admin calls `support:write:ticket:create` over HTTP
45
+ 3. The handler inserts the ticket and calls `ctx.notify` for the assignee (no assignee, no notification)
46
+ 4. For each registered channel:
47
+ - InApp: `templates.inApp(data)`, DB insert + SSE push
48
+ - Email: stored template rendered with `data` as variables, then transport
49
+ - Push: `templates.push(data)`, transport
50
+ 5. DeliveryLog entry per channel
51
+
52
+ ## Tests
53
+
54
+ - **E2E happy path:** Ticket with assignee → InApp + Email + Push + 3 DeliveryLog entries
55
+ - **Tenant override:** a published tenant template replaces the system mail text
56
+ - **Skip:** Ticket without assignee → no notifications
57
+ - **Access control:** Non-Admin/Support can't create tickets
58
+
59
+ ## Source files
60
+
61
+ ### src/__tests__/feature.integration.test.ts
62
+
63
+ ```ts
64
+ // Sample Integration Test: Delivery Notifications
65
+ //
66
+ // Proves the notification path end-to-end:
67
+ // 1. The boot seed writes the declared mail template into the template store
68
+ // 2. Admin creates a ticket with assignee (via HTTP API), the handler sends via ctx.notify
69
+ // 3. InApp → DB + SSE
70
+ // 4. Email → stored template → mail renderer → Transport
71
+ // 5. Push → Transport
72
+ // 6. DeliveryLog has entries for all 3 channels
73
+ // 7. A tenant overrides the stored mail text and the next mail uses it
74
+
75
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
76
+ import {
77
+ createChannelEmailFeature,
78
+ createInMemoryTransport,
79
+ type EmailMessage,
80
+ } from "@cosmicdrift/kumiko-bundled-features/channel-email";
81
+ import {
82
+ createChannelInAppFeature,
83
+ inAppMessagesTable,
84
+ } from "@cosmicdrift/kumiko-bundled-features/channel-in-app";
85
+ import {
86
+ createChannelPushFeature,
87
+ createInMemoryPushTransport,
88
+ type PushMessage,
89
+ } from "@cosmicdrift/kumiko-bundled-features/channel-push";
90
+ import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
91
+ import {
92
+ createDeliveryFeature,
93
+ createDeliveryTestContext,
94
+ deliveryAttemptsTable,
95
+ notificationPreferencesTable,
96
+ } from "@cosmicdrift/kumiko-bundled-features/delivery";
97
+ import { createRendererFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/renderer-foundation";
98
+ import {
99
+ createTemplateResolverFeature,
100
+ TemplateResolverHandlers,
101
+ } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
102
+ import { seedNotificationTemplates } from "@cosmicdrift/kumiko-bundled-features/template-resolver/seeding";
103
+ import {
104
+ createTenantFeature,
105
+ TenantQueries,
106
+ tenantMembershipsTable,
107
+ } from "@cosmicdrift/kumiko-bundled-features/tenant";
108
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
109
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
110
+ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
111
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
112
+ import {
113
+ createTestUser,
114
+ setupTestStack,
115
+ type TestStack,
116
+ TestUsers,
117
+ unsafePushTables,
118
+ } from "@cosmicdrift/kumiko-framework/stack";
119
+ import { expectErrorIncludes } from "@cosmicdrift/kumiko-framework/testing";
120
+ import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
121
+ import { supportFeature, TICKET_ASSIGNED, ticketTable } from "../feature";
122
+
123
+ // --- Test Infrastructure ---
124
+
125
+ const emailTransport = createInMemoryTransport();
126
+ const pushTransport = createInMemoryPushTransport();
127
+
128
+ // Simple resolvers: userId → address
129
+ const testEmail = (userId: string) => `user-${userId}@example.com`;
130
+ const testPushToken = (userId: string) => `push-token-${userId}`;
131
+
132
+ // Test users
133
+ const admin: SessionUser = TestUsers.admin;
134
+ const assignee = createTestUser({ id: 5, roles: ["Support"] });
135
+
136
+ // --- Feature Setup ---
137
+
138
+ const features = [
139
+ createConfigFeature(),
140
+ createTenantFeature(),
141
+ createTemplateResolverFeature(),
142
+ createRendererFoundationFeature(),
143
+ createRendererMailHtmlFeature(),
144
+ createDeliveryFeature(),
145
+ createChannelInAppFeature(),
146
+ createChannelEmailFeature({
147
+ transport: emailTransport,
148
+ resolveEmail: async (userId) => testEmail(userId),
149
+ }),
150
+ createChannelPushFeature({
151
+ transport: pushTransport,
152
+ resolveToken: async (userId) => testPushToken(userId),
153
+ }),
154
+ supportFeature,
155
+ ];
156
+
157
+ let stack: TestStack;
158
+ let db: DbConnection;
159
+
160
+ beforeAll(async () => {
161
+ stack = await setupTestStack({
162
+ features,
163
+ extraContext: (deps) =>
164
+ createDeliveryTestContext(deps, { tenantUserIdsQuery: TenantQueries.resolveUserIds }),
165
+ });
166
+ db = stack.db;
167
+
168
+ await unsafePushTables(db, {
169
+ inAppMessagesTable,
170
+ notificationPreferencesTable,
171
+ tenantMembershipsTable,
172
+ ticketTable,
173
+ });
174
+ await createEventsTable(db);
175
+ // setupTestStack does not run boot seeds; a real app seeds at boot.
176
+ await seedNotificationTemplates(db, stack.registry);
177
+ });
178
+
179
+ afterAll(async () => {
180
+ await stack.cleanup();
181
+ });
182
+
183
+ // --- End-to-End Flow ---
184
+
185
+ describe("delivery-notifications sample", () => {
186
+ test("ticket creation fires InApp + Email + Push notifications to assignee", async () => {
187
+ // Reset transports and SSE
188
+ emailTransport.sent.length = 0;
189
+ pushTransport.sent.length = 0;
190
+ stack.events.reset();
191
+
192
+ // --- Act: Admin creates a ticket with an assignee ---
193
+ await stack.http.writeOk(
194
+ "support:write:ticket:create",
195
+ {
196
+ title: "Login-Button reagiert nicht",
197
+ description: "Nach Klick auf Login passiert nichts in Firefox.",
198
+ assigneeId: assignee.id,
199
+ priority: "critical",
200
+ status: "open",
201
+ },
202
+ admin,
203
+ );
204
+
205
+ // --- Assert: InApp ---
206
+ const inAppMessages = await selectMany(db, inAppMessagesTable, { userId: assignee.id });
207
+ expect(inAppMessages).toHaveLength(1);
208
+ expect(inAppMessages[0]?.["title"]).toBe("New ticket: Login-Button reagiert nicht");
209
+ expect(inAppMessages[0]?.["body"]).toContain("Nach Klick auf Login");
210
+
211
+ const sseEvents = stack.events.sse.filter((e) => e.type === "channel-in-app:event:delivered");
212
+ expect(sseEvents).toHaveLength(1);
213
+ expect(sseEvents[0]?.data["userId"]).toBe(assignee.id);
214
+
215
+ // --- Assert: Email ---
216
+ const email = emailTransport.sent.find((e: EmailMessage) => e.to === testEmail(assignee.id));
217
+ expect(email).toBeDefined();
218
+ expect(email?.subject).toContain("Support ticket #");
219
+ expect(email?.subject).toContain("(critical)");
220
+ expect(email?.html).toContain("Login-Button reagiert nicht"); // title
221
+ expect(email?.html).toContain("Nach Klick auf Login"); // description
222
+ expect(email?.html).toContain("Priority: critical");
223
+ expect(email?.html).toContain("Open ticket"); // button
224
+ expect(email?.html).toContain("/support/tickets/"); // button URL
225
+
226
+ // --- Assert: Push ---
227
+ const push = pushTransport.sent.find(
228
+ (p: PushMessage) => p.token === testPushToken(assignee.id),
229
+ );
230
+ expect(push).toBeDefined();
231
+ expect(push?.title).toBe("New ticket");
232
+ expect(push?.body).toContain("Login-Button");
233
+ expect(push?.body).toContain("critical");
234
+
235
+ // --- Assert: DeliveryLog has entries for all 3 channels ---
236
+ const logs = await selectMany(db, deliveryAttemptsTable, {
237
+ notificationType: TICKET_ASSIGNED,
238
+ recipientId: assignee.id,
239
+ });
240
+ expect(logs).toHaveLength(3);
241
+ const channels = logs.map((l) => l["channel"]);
242
+ expect(channels).toContain("inApp");
243
+ expect(channels).toContain("email");
244
+ expect(channels).toContain("push");
245
+ expect(logs.every((l) => l["status"] === "sent")).toBe(true);
246
+ });
247
+
248
+ test("a tenant override of the stored mail text replaces the system text", async () => {
249
+ emailTransport.sent.length = 0;
250
+ const tenantAdmin = { ...admin, roles: ["TenantAdmin"] };
251
+
252
+ const draft = await stack.http.writeOk<{ id: string }>(
253
+ TemplateResolverHandlers.upsertTenant,
254
+ {
255
+ slug: TICKET_ASSIGNED,
256
+ kind: "mail",
257
+ locale: "en",
258
+ content:
259
+ '<p>Tenant text: <span data-variable="title"></span></p><p><span data-variable="description"></span> (<span data-variable="priority"></span>)</p><p><a data-button href="{{url}}">Open</a></p>',
260
+ contentFormat: "html",
261
+ meta: { subject: "Tenant subject #{{ticketId}}" },
262
+ variableSchema: {},
263
+ },
264
+ tenantAdmin,
265
+ );
266
+ await stack.http.writeOk(TemplateResolverHandlers.publish, { id: draft.id }, tenantAdmin);
267
+
268
+ await stack.http.writeOk(
269
+ "support:write:ticket:create",
270
+ { title: "Printer on fire", assigneeId: assignee.id, priority: "normal", status: "open" },
271
+ admin,
272
+ );
273
+
274
+ const email = emailTransport.sent.find((e: EmailMessage) => e.to === testEmail(assignee.id));
275
+ expect(email?.subject).toContain("Tenant subject #");
276
+ expect(email?.html).toContain("Tenant text: Printer on fire");
277
+ });
278
+
279
+ test("ticket without assignee skips notification (recipient returns null)", async () => {
280
+ emailTransport.sent.length = 0;
281
+ pushTransport.sent.length = 0;
282
+
283
+ await stack.http.writeOk(
284
+ "support:write:ticket:create",
285
+ {
286
+ title: "Dokumentation ueberarbeiten",
287
+ description: "Die API-Docs sind veraltet.",
288
+ priority: "low",
289
+ status: "open",
290
+ },
291
+ admin,
292
+ );
293
+
294
+ // No notifications sent — no assignee
295
+ expect(emailTransport.sent).toHaveLength(0);
296
+ expect(pushTransport.sent).toHaveLength(0);
297
+ });
298
+
299
+ test("access control: non-Admin/Support cannot create tickets", async () => {
300
+ const normalUser = createTestUser({ id: 99, roles: ["User"] });
301
+ const error = await stack.http.writeErr(
302
+ "support:write:ticket:create",
303
+ {
304
+ title: "Hacked ticket",
305
+ priority: "normal",
306
+ status: "open",
307
+ },
308
+ normalUser,
309
+ );
310
+ expectErrorIncludes(error, "access_denied");
311
+ });
312
+ });
313
+ ```
314
+
315
+ ### src/feature.ts
316
+
317
+ ```ts
318
+ // Delivery Notifications Sample
319
+ //
320
+ // A support ticket is created and its assignee is notified via inApp, email
321
+ // and push. The mail is a stored template: `r.notification` declares the
322
+ // variables, the default text and the subject, the boot seed writes the text
323
+ // into the template store, and a tenant can override it there.
324
+ //
325
+ // The feature code only declares WHAT to notify. HOW is handled by Delivery.
326
+
327
+ import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
328
+ import {
329
+ createEntity,
330
+ createTextField,
331
+ defineFeature,
332
+ QnTypes,
333
+ qn,
334
+ templateVariable,
335
+ } from "@cosmicdrift/kumiko-framework/engine";
336
+ import { z } from "zod";
337
+
338
+ // --- Entity ---
339
+
340
+ export const ticketEntity = createEntity({
341
+ table: "read_sample_delivery_tickets",
342
+ fields: {
343
+ title: createTextField({
344
+ personal: false,
345
+ reason: "is_business_data",
346
+ required: true,
347
+ maxLength: 200,
348
+ }),
349
+ description: createTextField({ personal: false, reason: "is_business_data", maxLength: 2000 }),
350
+ assigneeId: createTextField({ personal: "ref" }),
351
+ priority: createTextField({ personal: false, reason: "is_business_data", required: true }), // "low" | "normal" | "critical"
352
+ status: createTextField({ personal: false, reason: "is_business_data", required: true }),
353
+ },
354
+ });
355
+
356
+ export const ticketTable = buildEntityTable("ticket", ticketEntity);
357
+
358
+ function ticketExecutor() {
359
+ return createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" });
360
+ }
361
+
362
+ // --- Feature ---
363
+
364
+ export const TICKET_ASSIGNED = qn("support", QnTypes.notify, "ticket-assigned");
365
+
366
+ export const supportFeature = defineFeature("support", (r) => {
367
+ r.requires("delivery");
368
+
369
+ r.entity("ticket", ticketEntity);
370
+
371
+ r.notification("ticket-assigned", {
372
+ variables: {
373
+ ticketId: templateVariable.text({ required: true }),
374
+ title: templateVariable.text({ required: true }),
375
+ description: templateVariable.text({ required: true }),
376
+ priority: templateVariable.text({ required: true }),
377
+ url: templateVariable.link({ required: true }),
378
+ },
379
+ template: {
380
+ en: {
381
+ body: '<h1>New ticket: <span data-variable="title"></span></h1><p><span data-variable="description"></span></p><p>Priority: <span data-variable="priority"></span></p><p><a data-button href="{{url}}">Open ticket</a></p>',
382
+ },
383
+ },
384
+ meta: { en: { subject: "Support ticket #{{ticketId}} ({{priority}})" } },
385
+ // inApp and push build their own short text; email renders the stored
386
+ // template with the notify data as variables.
387
+ templates: {
388
+ inApp: (data) => ({
389
+ title: `New ticket: ${data["title"]}`,
390
+ body: (data["description"] as string) || "A ticket was assigned to you.",
391
+ }),
392
+ push: (data) => ({
393
+ title: "New ticket",
394
+ body: `${data["title"]} (${data["priority"]})`,
395
+ }),
396
+ },
397
+ });
398
+
399
+ r.writeHandler(
400
+ "ticket:create",
401
+ z.object({
402
+ title: z.string().min(1),
403
+ description: z.string().optional(),
404
+ assigneeId: z.uuid().optional(),
405
+ priority: z.enum(["low", "normal", "critical"]),
406
+ status: z.string().default("open"),
407
+ }),
408
+ async (event, ctx) => {
409
+ const created = await ticketExecutor().create(event.payload, event.user, ctx.db);
410
+ const assigneeId = event.payload.assigneeId;
411
+ // No assignee, no notification.
412
+ if (!created.isSuccess || assigneeId === undefined) return created;
413
+ if (ctx.notify === undefined) {
414
+ throw new Error("ticket:create needs ctx.notify, mount the delivery feature");
415
+ }
416
+ const ticketId = created.data.id;
417
+ await ctx.notify(TICKET_ASSIGNED, {
418
+ to: assigneeId,
419
+ data: {
420
+ ticketId,
421
+ title: event.payload.title,
422
+ description: event.payload.description ?? "",
423
+ priority: event.payload.priority,
424
+ url: `https://app.example.com/support/tickets/${ticketId}`,
425
+ },
426
+ });
427
+ return created;
428
+ },
429
+ { access: { roles: ["Admin", "Support"] } },
430
+ );
431
+ });
432
+ ```
@@ -0,0 +1,86 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/designer-demo. Do not edit. -->
2
+
3
+ # designer-demo
4
+
5
+ Kumiko sample feature — scaffolded by `bun kumiko create`
6
+
7
+ ## README
8
+
9
+ # Designer Demo
10
+
11
+ Designer-driven feature — live schema edits via the UI skeleton.
12
+
13
+ ## What it shows
14
+
15
+ - Patterns from the three hand-rolled view categories the designer targets
16
+ - A minimal feature surface the designer can open and mutate
17
+
18
+ ## Source
19
+
20
+ Feature entry point: `src/feature.ts`.
21
+
22
+ ## Tests
23
+
24
+ No dedicated automated tests in this folder yet — use it as a composition sample.
25
+
26
+ ## Source files
27
+
28
+ ### src/feature.ts
29
+
30
+ ```ts
31
+ // kumiko-feature-version: 1
32
+ // Demo-Feature für den Designer-Skeleton (C5).
33
+ // Enthält bewusst Patterns aus den 3 hand-rollten View-Kategorien:
34
+ // - entity → Tabelle der Fields
35
+ // - writeHandler → Header + Code-Blocks (schema + handler)
36
+ // - nav → ID/Label/Screen-Card
37
+ // Plus optionalRequires + metric als JSON-Dump-Fallback.
38
+
39
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
40
+ import { z } from "zod";
41
+
42
+ defineFeature("designerDemo", (r) => {
43
+ r.optionalRequires("analytics");
44
+
45
+ r.entity("task", {
46
+ fields: {
47
+ title: { type: "text", required: true },
48
+ done: { type: "boolean", default: false },
49
+ priority: { type: "select", options: ["low", "medium", "high"], default: "medium" },
50
+ },
51
+ });
52
+
53
+ r.writeHandler({
54
+ name: "task:create",
55
+ schema: z.object({ title: z.string(), priority: z.string().optional() }),
56
+ handler: async (_event, _ctx) => {
57
+ return { isSuccess: true, data: { id: "x" } };
58
+ },
59
+ access: { roles: ["user", "admin"] },
60
+ });
61
+
62
+ r.writeHandler({
63
+ name: "task:complete",
64
+ schema: z.object({ id: z.string() }),
65
+ handler: async (_event, _ctx) => {
66
+ return { isSuccess: true, data: {} };
67
+ },
68
+ access: {
69
+ openToAll: {
70
+ reason:
71
+ "sample handler: any signed-in user may complete a task in this designer-skeleton " +
72
+ "demo; there is no per-user task ownership",
73
+ },
74
+ },
75
+ });
76
+
77
+ r.nav({
78
+ id: "tasks",
79
+ label: "Tasks",
80
+ icon: "list",
81
+ screen: "designerDemo:screen:task-list",
82
+ });
83
+
84
+ r.metric("tasks_created", { type: "counter" });
85
+ });
86
+ ```