@cosmicdrift/kumiko-samples 0.364.0 → 0.365.1
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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -3
- package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/billing-foundation/changes.json +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +8 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +10 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +13 -0
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +30 -20
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/crypto-shredding/changes.json +7 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +7 -1
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +18 -1
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/jobs/changes.json +12 -0
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -2
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/changes.json +8 -1
- package/packages/bundled-features/src/step-dispatcher/constants.ts +22 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +236 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +65 -118
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +40 -14
- package/packages/bundled-features/src/template-resolver/changes.json +81 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +78 -12
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +11 -1
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +7 -1
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +111 -61
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +12 -2
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +48 -9
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +6 -1
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +25 -9
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +2 -0
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-meta.ts +8 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-resources.ts +2 -43
- package/packages/bundled-features/src/template-resolver/template-validation.ts +15 -9
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +34 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +3 -2
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +56 -11
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +6 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +1 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +14 -5
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +9 -3
- package/packages/bundled-features/src/tier-engine/changes.json +12 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/samples/apps/ui-walkthrough/src/run-config.ts +14 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +4 -4
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Dashboard stats
|
|
2
|
+
|
|
3
|
+
Key figures on a dashboard. This recipe ships a `ticket` entity, one query
|
|
4
|
+
handler that returns the figures as a flat object, and a `dashboard` screen
|
|
5
|
+
with a stat group (open, closed) and a single stat (total).
|
|
6
|
+
|
|
7
|
+
The point is to show how the three pieces connect: the screen only names a
|
|
8
|
+
query and the fields to read, the query does the counting.
|
|
9
|
+
|
|
10
|
+
## What it shows
|
|
11
|
+
|
|
12
|
+
- **Panels reference a query by qualified name**: each `stat` carries
|
|
13
|
+
`query: "tickets:query:ticket:stats"`. The dashboard calls that handler and
|
|
14
|
+
renders the result.
|
|
15
|
+
- **`valueField` picks a field from a flat result**: the handler returns
|
|
16
|
+
`{ open, closed, total }`, and each stat names one of those keys.
|
|
17
|
+
- **`stat-group` and `stat`**: a group shows several stats side by side under
|
|
18
|
+
one title, a top-level `stat` stands alone.
|
|
19
|
+
- **Counting happens in the database**: the handler calls the executor's
|
|
20
|
+
`list` with `filter`, `limit: 1` and `totalCount: true`. The total is counted
|
|
21
|
+
over the whole WHERE clause, so no rows are loaded just to be counted.
|
|
22
|
+
- **Labels are translation keys**: declared with `r.translations`.
|
|
23
|
+
|
|
24
|
+
## When to reach for it
|
|
25
|
+
|
|
26
|
+
You want a landing screen with a few numbers: open items, overdue invoices,
|
|
27
|
+
this month's revenue. Write one query per screen that returns every figure,
|
|
28
|
+
and let the stats pick their field.
|
|
29
|
+
|
|
30
|
+
## Source
|
|
31
|
+
|
|
32
|
+
The whole feature lives in `src/feature.ts` (~100 lines). The integration test
|
|
33
|
+
checks that every stat reads a field the query returns, and that the query
|
|
34
|
+
counts open and closed tickets correctly.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun kumiko test integration samples/dashboard-stats
|
|
38
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Dashboard Stats Sample
|
|
2
|
+
// Shows: a dashboard screen with a stat-group and a single stat, backed by one
|
|
3
|
+
// query handler that counts in the database.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createEntity,
|
|
7
|
+
createEntityExecutor,
|
|
8
|
+
createSelectField,
|
|
9
|
+
createTextField,
|
|
10
|
+
defineFeature,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
export const TICKET_STATUSES = ["open", "closed"] as const;
|
|
15
|
+
|
|
16
|
+
export const ticketEntity = createEntity({
|
|
17
|
+
table: "read_sample_tickets",
|
|
18
|
+
fields: {
|
|
19
|
+
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
20
|
+
status: createSelectField({ options: TICKET_STATUSES, default: "open", required: true }),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
|
|
25
|
+
|
|
26
|
+
const readerRoles = { roles: ["Admin", "User"] } as const;
|
|
27
|
+
|
|
28
|
+
export const ticketFeature = defineFeature("tickets", (r) => {
|
|
29
|
+
const { executor: ticketExecutor } = createEntityExecutor("ticket", ticketEntity);
|
|
30
|
+
|
|
31
|
+
r.crud("ticket", ticketEntity, {
|
|
32
|
+
write: { access: { roles: ["Admin"] } },
|
|
33
|
+
read: { access: readerRoles },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Flat result: each dashboard stat reads one field via its valueField.
|
|
37
|
+
// limit: 1 keeps the page tiny; totalCount counts over the whole WHERE in
|
|
38
|
+
// the database, so no rows are loaded just to be counted.
|
|
39
|
+
r.queryHandler(
|
|
40
|
+
"ticket:stats",
|
|
41
|
+
z.object({}),
|
|
42
|
+
async (query, ctx) => {
|
|
43
|
+
const countWithStatus = async (status: (typeof TICKET_STATUSES)[number]): Promise<number> => {
|
|
44
|
+
const page = await ticketExecutor.list(
|
|
45
|
+
{ filter: { field: "status", op: "eq", value: status }, limit: 1, totalCount: true },
|
|
46
|
+
query.user,
|
|
47
|
+
ctx.db,
|
|
48
|
+
);
|
|
49
|
+
return page.total ?? 0;
|
|
50
|
+
};
|
|
51
|
+
const [open, closed] = await Promise.all([
|
|
52
|
+
countWithStatus("open"),
|
|
53
|
+
countWithStatus("closed"),
|
|
54
|
+
]);
|
|
55
|
+
return { open, closed, total: open + closed };
|
|
56
|
+
},
|
|
57
|
+
{ access: readerRoles },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
r.translations({
|
|
61
|
+
keys: {
|
|
62
|
+
"tickets.dashboard.group": { en: "Tickets by status" },
|
|
63
|
+
"tickets.dashboard.open": { en: "Open" },
|
|
64
|
+
"tickets.dashboard.closed": { en: "Closed" },
|
|
65
|
+
"tickets.dashboard.total": { en: "All tickets" },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
r.screen({
|
|
70
|
+
id: "ticket-dashboard",
|
|
71
|
+
type: "dashboard",
|
|
72
|
+
panels: [
|
|
73
|
+
{
|
|
74
|
+
kind: "stat-group",
|
|
75
|
+
id: "by-status",
|
|
76
|
+
label: "tickets.dashboard.group",
|
|
77
|
+
stats: [
|
|
78
|
+
{
|
|
79
|
+
kind: "stat",
|
|
80
|
+
id: "open",
|
|
81
|
+
label: "tickets.dashboard.open",
|
|
82
|
+
query: TICKET_STATS_QUERY,
|
|
83
|
+
valueField: "open",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
kind: "stat",
|
|
87
|
+
id: "closed",
|
|
88
|
+
label: "tickets.dashboard.closed",
|
|
89
|
+
query: TICKET_STATS_QUERY,
|
|
90
|
+
valueField: "closed",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: "stat",
|
|
96
|
+
id: "total",
|
|
97
|
+
label: "tickets.dashboard.total",
|
|
98
|
+
query: TICKET_STATS_QUERY,
|
|
99
|
+
valueField: "total",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
access: readerRoles,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -87,8 +87,8 @@ imprint" decision.
|
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
# In the app workspace:
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
bunx kumiko-schema generate <name> # detects text-block entity → SQL migration
|
|
91
|
+
bunx kumiko-schema apply # one-time (pre-deploy step in prod)
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
### 3. Initial seed of the required blocks
|
|
@@ -81,8 +81,8 @@ the recipe lists `createConfigFeature()` explicitly (see `src/feature.ts`).
|
|
|
81
81
|
### 2. Create the table
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
bunx kumiko-schema generate <name> # detects the `page` entity → SQL migration
|
|
85
|
+
bunx kumiko-schema apply
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### 3. Authoring + branding
|
|
@@ -5,7 +5,8 @@ A write handler exports tenant data as a PDF and keeps it as a FileRef.
|
|
|
5
5
|
## What it shows
|
|
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
10
|
- The result is a normal FileRef: `GET /api/files/:id` serves it with `Content-Type: application/pdf`, other tenants get 404.
|
|
10
11
|
|
|
11
12
|
## Source
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-sample-pdf-audit-export",
|
|
3
|
-
"description": "Exports an audit table as a PDF: a write handler renders rows with renderTablePdf and
|
|
3
|
+
"description": "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).",
|
|
4
4
|
"private": true,
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@cosmicdrift/kumiko-bundled-features": "workspace:*",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// PDF Audit Export Sample
|
|
2
2
|
//
|
|
3
3
|
// A write handler turns tenant data into a PDF and keeps it as a regular
|
|
4
|
-
// FileRef: renderTablePdf draws the table (pure JS, no browser)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// FileRef: renderTablePdf draws the table (pure JS, no browser). The storage
|
|
5
|
+
// write runs in the beforeTransaction phase, so the transaction holds no pool
|
|
6
|
+
// connection while the bytes upload; the handler then only creates the
|
|
7
|
+
// fileRef row from event.prepared.
|
|
7
8
|
|
|
8
9
|
import { renderTablePdf } from "@cosmicdrift/kumiko-bundled-features/renderer-pdf";
|
|
9
10
|
import {
|
|
@@ -13,8 +14,13 @@ import {
|
|
|
13
14
|
createSelectField,
|
|
14
15
|
createTextField,
|
|
15
16
|
defineFeature,
|
|
17
|
+
defineWriteHandler,
|
|
16
18
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
17
|
-
import {
|
|
19
|
+
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
20
|
+
import {
|
|
21
|
+
createFileRefFromUploadedBytes,
|
|
22
|
+
uploadFileBytesBeforeTransaction,
|
|
23
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
18
24
|
import * as z from "zod";
|
|
19
25
|
|
|
20
26
|
export const INSPECTION_RESULTS = ["ok", "defect"] as const;
|
|
@@ -44,7 +50,8 @@ function toFileNameSegment(text: string): string {
|
|
|
44
50
|
return text.replaceAll(/[^A-Za-z0-9_-]+/g, "-");
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
|
|
53
|
+
// One row above the limit tells "exactly full" from "cut off"; entity lists cap at 200 rows.
|
|
54
|
+
const AUDIT_ROW_LIMIT = 100;
|
|
48
55
|
|
|
49
56
|
const access = { access: { roles: ["Admin", "User"] } } as const;
|
|
50
57
|
|
|
@@ -65,50 +72,63 @@ export const pdfAuditExportFeature = defineFeature("pdf-audit-export", (r) => {
|
|
|
65
72
|
);
|
|
66
73
|
|
|
67
74
|
r.writeHandler(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
defineWriteHandler({
|
|
76
|
+
name: "export-audit",
|
|
77
|
+
schema: z.object({ assetName: z.string().min(1), from: z.iso.date(), to: z.iso.date() }),
|
|
78
|
+
access: access.access,
|
|
79
|
+
beforeTransaction: async (event, ctx) => {
|
|
80
|
+
const { assetName, from, to } = event.payload;
|
|
81
|
+
const inspections = await inspectionExecutor.list(
|
|
82
|
+
{
|
|
83
|
+
filters: [
|
|
84
|
+
{ field: "assetName", op: "eq", value: assetName },
|
|
85
|
+
{ field: "inspectedOn", op: "gte", value: from },
|
|
86
|
+
{ field: "inspectedOn", op: "lte", value: to },
|
|
87
|
+
],
|
|
88
|
+
sort: "inspectedOn",
|
|
89
|
+
sortDirection: "asc",
|
|
90
|
+
limit: AUDIT_ROW_LIMIT + 1,
|
|
91
|
+
},
|
|
92
|
+
event.user,
|
|
93
|
+
ctx.db,
|
|
94
|
+
);
|
|
95
|
+
// A protocol that silently drops inspections would look complete.
|
|
96
|
+
if (inspections.rows.length > AUDIT_ROW_LIMIT) {
|
|
97
|
+
return writeFailure(
|
|
98
|
+
new UnprocessableError("audit_period_too_large", {
|
|
99
|
+
details: { limit: AUDIT_ROW_LIMIT },
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
}
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
const { pdfBytes } = await renderTablePdf({
|
|
105
|
+
title: `Prüfprotokoll ${assetName}`,
|
|
106
|
+
meta: [
|
|
107
|
+
{ label: "Zeitraum", value: `${formatGermanDate(from)} bis ${formatGermanDate(to)}` },
|
|
108
|
+
{ label: "Prüfungen", value: String(inspections.rows.length) },
|
|
109
|
+
],
|
|
110
|
+
columns: [{ header: "Datum" }, { header: "Ergebnis" }, { header: "Prüfer", weight: 2 }],
|
|
111
|
+
rows: inspections.rows.map((row) => [
|
|
112
|
+
formatGermanDate(String(row["inspectedOn"])),
|
|
113
|
+
RESULT_LABELS[z.enum(INSPECTION_RESULTS).parse(row["result"])],
|
|
114
|
+
String(row["inspector"] ?? ""),
|
|
115
|
+
]),
|
|
116
|
+
emptyText: "Keine Prüfungen im Zeitraum",
|
|
117
|
+
footerText: `Prüfbericht ${assetName}`,
|
|
118
|
+
pageLabel: "Seite {page} von {pages}",
|
|
119
|
+
});
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
121
|
+
return uploadFileBytesBeforeTransaction(ctx, {
|
|
122
|
+
fileName: `audit-${toFileNameSegment(assetName)}.pdf`,
|
|
123
|
+
mimeType: "application/pdf",
|
|
124
|
+
data: pdfBytes,
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
handler: async (event, ctx) => {
|
|
128
|
+
const created = await createFileRefFromUploadedBytes(ctx, event.prepared);
|
|
129
|
+
if (!created.isSuccess) return created;
|
|
130
|
+
return { isSuccess: true as const, data: { fileRefId: created.data.id } };
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
113
133
|
);
|
|
114
134
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import type
|
|
2
|
+
import { type DbRunner, toPostgresArrayLiteral } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
|
|
4
4
|
export async function selectExpiredSuspensionEvents(
|
|
5
5
|
db: DbRunner,
|
|
@@ -9,6 +9,6 @@ export async function selectExpiredSuspensionEvents(
|
|
|
9
9
|
`SELECT * FROM "kumiko_events"
|
|
10
10
|
WHERE "type" = ANY($1::text[])
|
|
11
11
|
AND (("payload"->>'wakeAt')::timestamptz < now() OR ("payload"->>'timeoutAt')::timestamptz < now())`,
|
|
12
|
-
[eventTypes
|
|
12
|
+
[toPostgresArrayLiteral(eventTypes)],
|
|
13
13
|
)) as Record<string, unknown>[];
|
|
14
14
|
}
|