@cosmicdrift/kumiko-samples 0.371.0 → 0.372.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 (50) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +1 -1
  3. package/packages/bundled-features/src/auth-mfa/db/queries.ts +5 -7
  4. package/packages/bundled-features/src/cap-overview/web/cap-cards-panel.tsx +2 -1
  5. package/packages/bundled-features/src/config/web/index.ts +2 -0
  6. package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
  7. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +75 -34
  8. package/packages/bundled-features/src/crypto-shredding/resolve-file-bytes-deleter.ts +29 -0
  9. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  10. package/packages/bundled-features/src/data-retention/index.ts +6 -0
  11. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +37 -213
  12. package/packages/bundled-features/src/delivery/changes.json +13 -0
  13. package/packages/bundled-features/src/delivery/delivery-service.ts +7 -2
  14. package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +2 -2
  15. package/packages/bundled-features/src/delivery/index.ts +2 -0
  16. package/packages/bundled-features/src/delivery/jobs.ts +42 -23
  17. package/packages/bundled-features/src/delivery/recipient-address-seal.ts +72 -0
  18. package/packages/bundled-features/src/delivery/testing.ts +4 -1
  19. package/packages/bundled-features/src/delivery/unsubscribe.ts +23 -2
  20. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +2 -2
  21. package/packages/bundled-features/src/files/changes.json +7 -0
  22. package/packages/bundled-features/src/personal-access-tokens/changes.json +12 -0
  23. package/packages/bundled-features/src/personal-access-tokens/screens.ts +15 -9
  24. package/packages/bundled-features/src/shared/index.ts +14 -0
  25. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +206 -0
  26. package/packages/bundled-features/src/shared/purge-row-file-refs.ts +214 -0
  27. package/packages/bundled-features/src/template-resolver/changes.json +6 -0
  28. package/packages/bundled-features/src/template-resolver/template-preview.ts +24 -4
  29. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +2 -1
  30. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +1 -9
  31. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +2 -2
  32. package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
  33. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +46 -1
  34. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  35. package/samples/apps/styleguide/src/features/demo/feature.ts +1 -1
  36. package/samples/apps/ui-walkthrough/src/app/seed-rental.ts +5 -0
  37. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +2 -0
  38. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +9 -0
  39. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +6 -0
  40. package/samples/apps/ui-walkthrough/src/features/rental/entities.ts +1 -0
  41. package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +27 -1
  42. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +9 -0
  43. package/samples/apps/ui-walkthrough/src/features/rental/lease-support.ts +1 -0
  44. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +22 -1
  45. package/samples/apps/use-all-bundled/package.json +3 -1
  46. package/samples/recipes/custom-handlers/src/feature.ts +3 -5
  47. package/samples/recipes/embedded-entity-form/src/feature.ts +1 -1
  48. package/samples/recipes/pdf-audit-export/README.md +1 -1
  49. package/samples/recipes/user-data-rights/README.md +22 -0
  50. package/samples/recipes/user-data-rights/src/retained-docs.ts +35 -0
@@ -178,6 +178,7 @@ export const leaseDetailScreen: ProjectionDetailScreenDefinition = {
178
178
  einheit: "rental:entity:lease:field:einheit",
179
179
  liegenschaft: "rental:entity:lease:field:liegenschaft",
180
180
  ende: "rental:entity:lease:field:ende",
181
+ vorgaenger: "rental:entity:lease:field:vorgaenger",
181
182
  kontostand: "rental:entity:lease:field:kontostand",
182
183
  notizen: "rental:entity:lease:field:notizen",
183
184
  },
@@ -268,7 +269,13 @@ export const leaseDetailScreen: ProjectionDetailScreenDefinition = {
268
269
  kind: "fields",
269
270
  title: "rental.tab.contractData",
270
271
  columns: 2,
271
- fields: ["einheit", "liegenschaft", "beginn", "ende"],
272
+ fields: [
273
+ "einheit",
274
+ "liegenschaft",
275
+ "beginn",
276
+ "ende",
277
+ { field: "vorgaenger", refEntity: "lease", refLabelField: "einheit" },
278
+ ],
272
279
  },
273
280
  {
274
281
  id: "notes",
@@ -497,3 +504,17 @@ export const leaseListEmptyScreen: EntityListScreenDefinition = {
497
504
  rowActions: [openLeaseDetail],
498
505
  access: OPEN_ACCESS,
499
506
  };
507
+
508
+ // Reference column next to empty plain cells: the predecessor links to its
509
+ // lease, a lease without one shows the empty-value placeholder.
510
+ export const leaseSuccessionListScreen: EntityListScreenDefinition = {
511
+ id: "lease-succession-list",
512
+ type: "entityList",
513
+ entity: "lease",
514
+ dormant: true,
515
+ columns: ["mieter", "vorgaenger", "ende", "beginn"],
516
+ defaultSort: { field: "beginn", dir: "desc" },
517
+ pageSize: 25,
518
+ rowActions: [openLeaseDetail],
519
+ access: OPEN_ACCESS,
520
+ };
@@ -30,7 +30,9 @@
30
30
  "react-dom": "^19.2.6",
31
31
  "zod": "^4.4.3",
32
32
  "@cosmicdrift/kumiko-locale-de": "workspace:*",
33
- "@cosmicdrift/kumiko-locale-es": "workspace:*"
33
+ "@cosmicdrift/kumiko-locale-es": "workspace:*",
34
+ "@cosmicdrift/kumiko-locale-fr": "workspace:*",
35
+ "@cosmicdrift/kumiko-locale-it": "workspace:*"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@cosmicdrift/kumiko-testing": "workspace:*"
@@ -12,9 +12,6 @@ import {
12
12
  import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
13
13
  import { z } from "zod";
14
14
 
15
- // detail() returns an untyped row; parse the two fields the increment needs once.
16
- const counterVersionRowSchema = z.object({ count: z.number(), version: z.number() });
17
-
18
15
  export const counterEntity = createEntity({
19
16
  table: "read_sample_counters",
20
17
  fields: {
@@ -51,8 +48,9 @@ export const counterFeature = defineFeature("counters", (r) => {
51
48
  return failNotFound("counter", event.payload.id);
52
49
  }
53
50
 
54
- const { count, version } = counterVersionRowSchema.parse(current);
55
- const newCount = count + event.payload.amount;
51
+ // detail() returns the entity's own row type; count is nullable because the field is not required.
52
+ const { count, version } = current;
53
+ const newCount = (count ?? 0) + event.payload.amount;
56
54
  // Read-modify-write on counter: use the version we just read so two
57
55
  // concurrent increments don't clobber each other's changes.
58
56
  return counterExecutor.update(
@@ -99,7 +99,7 @@ export const prospectsFeature = defineFeature("prospects", (r) => {
99
99
  const flipped = await suggestionExecutor.update(
100
100
  {
101
101
  id: event.payload.suggestionId,
102
- version: suggestion["version"] as number,
102
+ version: suggestion["version"],
103
103
  changes: { status: "accepted" },
104
104
  },
105
105
  event.user,
@@ -6,7 +6,7 @@ A write handler exports tenant data as a PDF and keeps it as a FileRef.
6
6
 
7
7
  - `renderTablePdf` (`@cosmicdrift/kumiko-bundled-features/renderer-pdf`) draws a table document in pure JavaScript: repeating header, weighted columns, page footer. The handler passes ready-formatted strings.
8
8
  - The export is a `beforeTransaction` handler: it reads the rows (reads work in that phase), renders the PDF and calls `uploadFileBytesBeforeTransaction` (`@cosmicdrift/kumiko-framework/files`), which validates the bytes and writes them to the tenant-prefixed storage key. The upload runs before the transaction because storage I/O inside it would hold a pool connection for as long as the upload takes.
9
- - The handler itself calls `createFileRefFromUploadedBytes(ctx, event.prepared)` and creates the `file_refs` row and the `fileRef.created` event in the transaction. If the transaction does not commit, the phase's `release` deletes the uploaded bytes again (only when no row exists for the storage key). A crash between upload and commit leaves the bytes in storage; nothing sweeps them.
9
+ - The handler itself calls `createFileRefFromUploadedBytes(ctx, event.prepared)` and creates the `file_refs` row and the `fileRef.created` event in the transaction. If the transaction does not commit, the phase's `release` deletes the uploaded bytes again (only when no row exists for the storage key). A crash between upload and commit leaves the bytes in storage; the hourly `files:job:sweep-orphaned-uploads` job deletes them once they are older than the grace period (24 hours by default, `uploadSweepGraceHours`).
10
10
  - The result is a normal FileRef: `GET /api/files/:id` serves it with `Content-Type: application/pdf`, other tenants get 404.
11
11
 
12
12
  ## Source
@@ -75,6 +75,28 @@ Step-by-step proof:
75
75
  2. Deletion + expired grace → `runForgetCleanup` removes all notes
76
76
  3. Strategy `anonymize` → `authorId=null`, title/body remain
77
77
 
78
+ ## Retention with files
79
+
80
+ `runRetentionCleanup` is exported for app tests. A `hardDelete` entity with a file field loses
81
+ its bytes (and derivatives), its `file_refs` row and then the row itself; a storage error keeps
82
+ the row and reports `file_delete_failed` in `skipped`. `src/retained-docs.ts` holds the entity,
83
+ `src/__tests__/retention-files.integration.test.ts` drives the run with only public imports:
84
+
85
+ ```ts illustration
86
+ import { runRetentionCleanup } from "@cosmicdrift/kumiko-bundled-features/data-retention";
87
+ import { createFileContext } from "@cosmicdrift/kumiko-framework/files";
88
+
89
+ const result = await runRetentionCleanup({
90
+ db: stack.db,
91
+ registry: stack.registry,
92
+ tenantId,
93
+ preloadedTenantPreset: null,
94
+ now: getTemporal().Now.instant().add({ hours: 31 * 24 }), // keepFor 30d + 1 day
95
+ files: createFileContext(() => Promise.resolve(provider)), // the stack's storage provider
96
+ });
97
+ // result.hardDeleted === 1, bytes and file_refs row gone
98
+ ```
99
+
78
100
  ## Full app vs this recipe
79
101
 
80
102
  | | This recipe | [user-data-rights-demo](/en/samples/apps-user-data-rights-demo/) |
@@ -0,0 +1,35 @@
1
+ // Retention with files: a hardDelete entity whose file field is cleaned up together with the
2
+ // row. runRetentionCleanup removes the bytes (and derivatives) and the file_refs row first,
3
+ // then the row itself; the cron in data-retention does the same with the tenant's FileContext.
4
+
5
+ import {
6
+ createEntity,
7
+ createFileField,
8
+ createTextField,
9
+ defineEntityCreateHandler,
10
+ defineEntityDetailHandler,
11
+ defineFeature,
12
+ } from "@cosmicdrift/kumiko-framework/engine";
13
+
14
+ export const retainedDocEntity = createEntity({
15
+ table: "store_retained_docs",
16
+ fields: {
17
+ title: createTextField({ personal: false, reason: "is_business_data", required: true }),
18
+ attachment: createFileField(),
19
+ },
20
+ retention: { keepFor: "30d", strategy: "hardDelete", reference: "createdAt" },
21
+ });
22
+
23
+ export const retainedDocsFeature = defineFeature("retained-docs", (r) => {
24
+ r.entity({ name: "retained-doc", ...retainedDocEntity });
25
+ r.writeHandler(
26
+ defineEntityCreateHandler("retained-doc", retainedDocEntity, {
27
+ access: { roles: ["Admin"] },
28
+ }),
29
+ );
30
+ r.queryHandler(
31
+ defineEntityDetailHandler("retained-doc", retainedDocEntity, {
32
+ access: { roles: ["Admin"] },
33
+ }),
34
+ );
35
+ });