@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.
- package/AGENTS.md +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +319 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +215 -0
- package/agents/examples/ledger-invoicing.md +236 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +250 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +418 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +75 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<!-- Generated by `bun run gen:agent-docs` from samples/recipes/pdf-audit-export. Do not edit. -->
|
|
2
|
+
|
|
3
|
+
# pdf-audit-export
|
|
4
|
+
|
|
5
|
+
Exports an audit table as a PDF: a write handler renders rows with renderTablePdf and uploads the bytes before the transaction (uploadFileBytesBeforeTransaction) and stores them as a FileRef (createFileRefFromUploadedBytes).
|
|
6
|
+
|
|
7
|
+
## README
|
|
8
|
+
|
|
9
|
+
# PDF Audit Export
|
|
10
|
+
|
|
11
|
+
A write handler exports tenant data as a PDF and keeps it as a FileRef.
|
|
12
|
+
|
|
13
|
+
## What it shows
|
|
14
|
+
|
|
15
|
+
- `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.
|
|
16
|
+
- 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.
|
|
17
|
+
- 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.
|
|
18
|
+
- The result is a normal FileRef: `GET /api/files/:id` serves it with `Content-Type: application/pdf`, other tenants get 404.
|
|
19
|
+
|
|
20
|
+
## Source
|
|
21
|
+
|
|
22
|
+
Feature entry point: `src/feature.ts`. The export reads at most 200 inspections per call.
|
|
23
|
+
|
|
24
|
+
## Tests
|
|
25
|
+
|
|
26
|
+
Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
cd samples/recipes/pdf-audit-export
|
|
30
|
+
bun test
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Source files
|
|
34
|
+
|
|
35
|
+
### src/__tests__/feature.integration.test.ts
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// PDF Audit Export Sample — Integration Test
|
|
39
|
+
//
|
|
40
|
+
// Proves the whole path through real HTTP calls: inspections are written,
|
|
41
|
+
// the export handler renders and stores a PDF, GET /api/files/:id serves it
|
|
42
|
+
// and its text contains exactly the inspections of the requested period.
|
|
43
|
+
|
|
44
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
45
|
+
import {
|
|
46
|
+
createFilesFeature,
|
|
47
|
+
createInMemoryFileProvider,
|
|
48
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
49
|
+
import {
|
|
50
|
+
createTestUser,
|
|
51
|
+
setupTestStack,
|
|
52
|
+
type TestStack,
|
|
53
|
+
testTenantId,
|
|
54
|
+
unsafeCreateEntityTable,
|
|
55
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
56
|
+
import { extractText, getDocumentProxy } from "unpdf";
|
|
57
|
+
import { inspectionEntity, pdfAuditExportFeature } from "../feature";
|
|
58
|
+
|
|
59
|
+
let stack: TestStack;
|
|
60
|
+
|
|
61
|
+
const userA = createTestUser({ id: 1, tenantId: testTenantId(1), roles: ["Admin"] });
|
|
62
|
+
const userB = createTestUser({ id: 2, tenantId: testTenantId(2), roles: ["Admin"] });
|
|
63
|
+
|
|
64
|
+
beforeAll(async () => {
|
|
65
|
+
stack = await setupTestStack({
|
|
66
|
+
features: [createFilesFeature(), pdfAuditExportFeature],
|
|
67
|
+
files: { storageProvider: createInMemoryFileProvider() },
|
|
68
|
+
});
|
|
69
|
+
await unsafeCreateEntityTable(stack.db, inspectionEntity);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterAll(async () => {
|
|
73
|
+
await stack.cleanup();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function addInspection(
|
|
77
|
+
inspectedOn: string,
|
|
78
|
+
result: "ok" | "defect",
|
|
79
|
+
inspector: string,
|
|
80
|
+
assetName = "Kessel 3",
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
await stack.http.writeOk(
|
|
83
|
+
"pdf-audit-export:write:inspection:create",
|
|
84
|
+
{ assetName, inspectedOn, result, inspector },
|
|
85
|
+
userA,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function download(user: typeof userA, fileRefId: string): Promise<Response> {
|
|
90
|
+
const token = await stack.jwt.sign(user);
|
|
91
|
+
return stack.app.request(`/api/files/${fileRefId}`, {
|
|
92
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
describe("export-audit", () => {
|
|
97
|
+
test("stores a PDF FileRef containing only the inspections of the period and asset", async () => {
|
|
98
|
+
await addInspection("2026-01-10", "ok", "Zeile-vor-Zeitraum");
|
|
99
|
+
await addInspection("2026-02-05", "defect", "Zeile-im-Zeitraum-Eins");
|
|
100
|
+
await addInspection("2026-03-20", "ok", "Zeile-im-Zeitraum-Zwei");
|
|
101
|
+
await addInspection("2026-05-02", "ok", "Zeile-nach-Zeitraum");
|
|
102
|
+
await addInspection("2026-02-06", "ok", "Zeile-anderer-Asset", "Pumpe 7");
|
|
103
|
+
|
|
104
|
+
const { fileRefId } = await stack.http.writeOk<{ fileRefId: string }>(
|
|
105
|
+
"pdf-audit-export:write:export-audit",
|
|
106
|
+
{ assetName: "Kessel 3", from: "2026-02-01", to: "2026-03-31" },
|
|
107
|
+
userA,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const res = await download(userA, fileRefId);
|
|
111
|
+
expect(res.status).toBe(200);
|
|
112
|
+
expect(res.headers.get("Content-Type")).toBe("application/pdf");
|
|
113
|
+
const proxy = await getDocumentProxy(new Uint8Array(await res.arrayBuffer()));
|
|
114
|
+
const { text } = await extractText(proxy, { mergePages: true });
|
|
115
|
+
|
|
116
|
+
expect(text).toContain("Prüfprotokoll Kessel 3");
|
|
117
|
+
expect(text).toContain("05.02.2026");
|
|
118
|
+
expect(text).toContain("Mangel");
|
|
119
|
+
expect(text).toContain("Zeile-im-Zeitraum-Eins");
|
|
120
|
+
expect(text).toContain("20.03.2026");
|
|
121
|
+
expect(text).toContain("Zeile-im-Zeitraum-Zwei");
|
|
122
|
+
expect(text).not.toContain("Zeile-vor-Zeitraum");
|
|
123
|
+
expect(text).not.toContain("Zeile-nach-Zeitraum");
|
|
124
|
+
expect(text).not.toContain("Zeile-anderer-Asset");
|
|
125
|
+
expect(text).toContain("Seite 1 von 1");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("a user of another tenant gets 404 for the exported file", async () => {
|
|
129
|
+
await addInspection("2026-02-05", "ok", "Zeile-Tenant-A");
|
|
130
|
+
const { fileRefId } = await stack.http.writeOk<{ fileRefId: string }>(
|
|
131
|
+
"pdf-audit-export:write:export-audit",
|
|
132
|
+
{ assetName: "Kessel 3", from: "2026-02-01", to: "2026-03-31" },
|
|
133
|
+
userA,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect((await download(userB, fileRefId)).status).toBe(404);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a period without inspections still yields a PDF with the empty hint", async () => {
|
|
140
|
+
const { fileRefId } = await stack.http.writeOk<{ fileRefId: string }>(
|
|
141
|
+
"pdf-audit-export:write:export-audit",
|
|
142
|
+
{ assetName: "Unbekannte Anlage", from: "2026-02-01", to: "2026-03-31" },
|
|
143
|
+
userA,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const res = await download(userA, fileRefId);
|
|
147
|
+
const proxy = await getDocumentProxy(new Uint8Array(await res.arrayBuffer()));
|
|
148
|
+
const { text } = await extractText(proxy, { mergePages: true });
|
|
149
|
+
expect(text).toContain("Keine Prüfungen im Zeitraum");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a period with more inspections than the export holds is rejected instead of cut off", async () => {
|
|
153
|
+
const assetName = "Großanlage";
|
|
154
|
+
for (let day = 0; day < 101; day += 1) {
|
|
155
|
+
const inspectedOn = new Date(Date.UTC(2025, 0, 1 + day)).toISOString().slice(0, 10);
|
|
156
|
+
await addInspection(inspectedOn, "ok", `Prüfer-${day}`, assetName);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const err = await stack.http.writeErr(
|
|
160
|
+
"pdf-audit-export:write:export-audit",
|
|
161
|
+
{ assetName, from: "2025-01-01", to: "2025-12-31" },
|
|
162
|
+
userA,
|
|
163
|
+
);
|
|
164
|
+
expect(err.httpStatus).toBe(422);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### src/feature.ts
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// PDF Audit Export Sample
|
|
173
|
+
//
|
|
174
|
+
// A write handler turns tenant data into a PDF and keeps it as a regular
|
|
175
|
+
// FileRef: renderTablePdf draws the table (pure JS, no browser). The storage
|
|
176
|
+
// write runs in the beforeTransaction phase, so the transaction holds no pool
|
|
177
|
+
// connection while the bytes upload; the handler then only creates the
|
|
178
|
+
// fileRef row from event.prepared.
|
|
179
|
+
|
|
180
|
+
import { renderTablePdf } from "@cosmicdrift/kumiko-bundled-features/renderer-pdf";
|
|
181
|
+
import {
|
|
182
|
+
createDateField,
|
|
183
|
+
createEntity,
|
|
184
|
+
createEntityExecutor,
|
|
185
|
+
createSelectField,
|
|
186
|
+
createTextField,
|
|
187
|
+
defineFeature,
|
|
188
|
+
defineWriteHandler,
|
|
189
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
190
|
+
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
191
|
+
import {
|
|
192
|
+
createFileRefFromUploadedBytes,
|
|
193
|
+
uploadFileBytesBeforeTransaction,
|
|
194
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
195
|
+
import * as z from "zod";
|
|
196
|
+
|
|
197
|
+
export const INSPECTION_RESULTS = ["ok", "defect"] as const;
|
|
198
|
+
|
|
199
|
+
export const inspectionEntity = createEntity({
|
|
200
|
+
table: "read_sample_inspections",
|
|
201
|
+
fields: {
|
|
202
|
+
assetName: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
203
|
+
inspectedOn: createDateField({ required: true }),
|
|
204
|
+
result: createSelectField({ options: INSPECTION_RESULTS, required: true }),
|
|
205
|
+
inspector: createTextField({ personal: "ref" }),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const RESULT_LABELS: Readonly<Record<(typeof INSPECTION_RESULTS)[number], string>> = {
|
|
210
|
+
ok: "In Ordnung",
|
|
211
|
+
defect: "Mangel",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// The renderer takes ready-formatted strings, so date formatting stays with the caller.
|
|
215
|
+
function formatGermanDate(isoDate: string): string {
|
|
216
|
+
const [year, month, day] = isoDate.split("-");
|
|
217
|
+
return `${day}.${month}.${year}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toFileNameSegment(text: string): string {
|
|
221
|
+
return text.replaceAll(/[^A-Za-z0-9_-]+/g, "-");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// One row above the limit tells "exactly full" from "cut off"; entity lists cap at 200 rows.
|
|
225
|
+
const AUDIT_ROW_LIMIT = 100;
|
|
226
|
+
|
|
227
|
+
const access = { access: { roles: ["Admin", "User"] } } as const;
|
|
228
|
+
|
|
229
|
+
export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
|
|
230
|
+
r.entity("inspection", inspectionEntity);
|
|
231
|
+
const { executor: inspectionExecutor } = createEntityExecutor("inspection", inspectionEntity);
|
|
232
|
+
|
|
233
|
+
r.writeHandler(
|
|
234
|
+
"inspection:create",
|
|
235
|
+
z.object({
|
|
236
|
+
assetName: z.string().min(1),
|
|
237
|
+
inspectedOn: z.iso.date(),
|
|
238
|
+
result: z.enum(INSPECTION_RESULTS),
|
|
239
|
+
inspector: z.string().optional(),
|
|
240
|
+
}),
|
|
241
|
+
async (event, ctx) => inspectionExecutor.create(event.payload, event.user, ctx.db),
|
|
242
|
+
access,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
r.writeHandler(
|
|
246
|
+
defineWriteHandler({
|
|
247
|
+
name: "export-audit",
|
|
248
|
+
schema: z.object({ assetName: z.string().min(1), from: z.iso.date(), to: z.iso.date() }),
|
|
249
|
+
access: access.access,
|
|
250
|
+
beforeTransaction: async (event, ctx) => {
|
|
251
|
+
const { assetName, from, to } = event.payload;
|
|
252
|
+
const inspections = await inspectionExecutor.list(
|
|
253
|
+
{
|
|
254
|
+
filters: [
|
|
255
|
+
{ field: "assetName", op: "eq", value: assetName },
|
|
256
|
+
{ field: "inspectedOn", op: "gte", value: from },
|
|
257
|
+
{ field: "inspectedOn", op: "lte", value: to },
|
|
258
|
+
],
|
|
259
|
+
sort: "inspectedOn",
|
|
260
|
+
sortDirection: "asc",
|
|
261
|
+
limit: AUDIT_ROW_LIMIT + 1,
|
|
262
|
+
},
|
|
263
|
+
event.user,
|
|
264
|
+
ctx.db,
|
|
265
|
+
);
|
|
266
|
+
// A protocol that silently drops inspections would look complete.
|
|
267
|
+
if (inspections.rows.length > AUDIT_ROW_LIMIT) {
|
|
268
|
+
return writeFailure(
|
|
269
|
+
new UnprocessableError("audit_period_too_large", {
|
|
270
|
+
details: { limit: AUDIT_ROW_LIMIT },
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { pdfBytes } = await renderTablePdf({
|
|
276
|
+
title: `Prüfprotokoll ${assetName}`,
|
|
277
|
+
meta: [
|
|
278
|
+
{ label: "Zeitraum", value: `${formatGermanDate(from)} bis ${formatGermanDate(to)}` },
|
|
279
|
+
{ label: "Prüfungen", value: String(inspections.rows.length) },
|
|
280
|
+
],
|
|
281
|
+
columns: [{ header: "Datum" }, { header: "Ergebnis" }, { header: "Prüfer", weight: 2 }],
|
|
282
|
+
rows: inspections.rows.map((row) => [
|
|
283
|
+
formatGermanDate(String(row["inspectedOn"])),
|
|
284
|
+
RESULT_LABELS[z.enum(INSPECTION_RESULTS).parse(row["result"])],
|
|
285
|
+
String(row["inspector"] ?? ""),
|
|
286
|
+
]),
|
|
287
|
+
emptyText: "Keine Prüfungen im Zeitraum",
|
|
288
|
+
footerText: `Prüfbericht ${assetName}`,
|
|
289
|
+
pageLabel: "Seite {page} von {pages}",
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
return uploadFileBytesBeforeTransaction(ctx, {
|
|
293
|
+
fileName: `audit-${toFileNameSegment(assetName)}.pdf`,
|
|
294
|
+
mimeType: "application/pdf",
|
|
295
|
+
data: pdfBytes,
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
handler: async (event, ctx) => {
|
|
299
|
+
const created = await createFileRefFromUploadedBytes(ctx, event.prepared);
|
|
300
|
+
if (!created.isSuccess) return created;
|
|
301
|
+
return { isSuccess: true as const, data: { fileRefId: created.data.id } };
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
```
|