@cosmicdrift/kumiko-bundled-features 0.398.0 → 0.400.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 (94) hide show
  1. package/dist/auth-mfa/schema/user-mfa.d.ts +4 -4
  2. package/dist/billing-foundation/consumer-protection/consent-text.js +4 -4
  3. package/dist/billing-foundation/entities.d.ts +2 -2
  4. package/dist/billing-foundation/projection.d.ts +2 -2
  5. package/dist/cap-counter/entity.d.ts +3 -3
  6. package/dist/channel-in-app/constants.js +4 -9
  7. package/dist/channel-in-app/feature.js +1 -1
  8. package/dist/channel-in-app/handlers/inbox.query.d.ts +4 -1
  9. package/dist/channel-in-app/handlers/inbox.query.js +23 -8
  10. package/dist/channel-in-app/handlers/unread-count.query.js +2 -9
  11. package/dist/channel-in-app/in-app-channel.js +23 -11
  12. package/dist/channel-in-app/in-app-target.d.ts +5 -0
  13. package/dist/channel-in-app/in-app-target.js +34 -0
  14. package/dist/channel-in-app/index.d.ts +1 -0
  15. package/dist/channel-in-app/index.js +1 -0
  16. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
  17. package/dist/custom-fields/entity.d.ts +23 -3
  18. package/dist/delivery/tables.d.ts +11 -1
  19. package/dist/documents/aggregate-id.d.ts +2 -0
  20. package/dist/documents/aggregate-id.js +12 -0
  21. package/dist/documents/capture.d.ts +14 -0
  22. package/dist/documents/capture.js +65 -0
  23. package/dist/documents/constants.d.ts +23 -0
  24. package/dist/documents/constants.js +21 -0
  25. package/dist/documents/ensure-link.d.ts +17 -0
  26. package/dist/documents/ensure-link.js +20 -0
  27. package/dist/documents/entity.d.ts +76 -0
  28. package/dist/documents/entity.js +84 -0
  29. package/dist/documents/executor.d.ts +6 -0
  30. package/dist/documents/executor.js +8 -0
  31. package/dist/documents/expiry.d.ts +9 -0
  32. package/dist/documents/expiry.js +20 -0
  33. package/dist/documents/feature.d.ts +33 -0
  34. package/dist/documents/feature.js +111 -0
  35. package/dist/documents/handlers/import.write.d.ts +23 -0
  36. package/dist/documents/handlers/import.write.js +104 -0
  37. package/dist/documents/handlers/link.write.d.ts +18 -0
  38. package/dist/documents/handlers/link.write.js +61 -0
  39. package/dist/documents/i18n.d.ts +6 -0
  40. package/dist/documents/i18n.js +33 -0
  41. package/dist/documents/index.d.ts +7 -0
  42. package/dist/documents/index.js +6 -0
  43. package/dist/documents/pending-file-guard.d.ts +2 -0
  44. package/dist/documents/pending-file-guard.js +29 -0
  45. package/dist/documents/queries/expiry-overview.query.d.ts +10 -0
  46. package/dist/documents/queries/expiry-overview.query.js +41 -0
  47. package/dist/documents/queries/for-record.query.d.ts +90 -0
  48. package/dist/documents/queries/for-record.query.js +54 -0
  49. package/dist/documents/queries/list-page.d.ts +19 -0
  50. package/dist/documents/queries/list-page.js +36 -0
  51. package/dist/documents/queries/overview.query.d.ts +32 -0
  52. package/dist/documents/queries/overview.query.js +41 -0
  53. package/dist/documents/schemas.d.ts +93 -0
  54. package/dist/documents/schemas.js +51 -0
  55. package/dist/documents/screens.d.ts +3 -0
  56. package/dist/documents/screens.js +76 -0
  57. package/dist/documents/tab.d.ts +4 -0
  58. package/dist/documents/tab.js +42 -0
  59. package/dist/documents/target-write-check.d.ts +3 -0
  60. package/dist/documents/target-write-check.js +25 -0
  61. package/dist/documents/write-guard.d.ts +6 -0
  62. package/dist/documents/write-guard.js +46 -0
  63. package/dist/inbound-mail-foundation/entities.d.ts +17 -7
  64. package/dist/inbound-mail-foundation/ingest-failures.d.ts +3 -3
  65. package/dist/inbound-mail-foundation/projection.d.ts +15 -5
  66. package/dist/inbound-mail-foundation/quarantine-entities.d.ts +3 -3
  67. package/dist/jobs/job-run-table.d.ts +4 -4
  68. package/dist/ledger/entity.d.ts +8 -8
  69. package/dist/managed-pages/table.d.ts +24 -4
  70. package/dist/metrics/compute.d.ts +3 -0
  71. package/dist/metrics/compute.js +10 -3
  72. package/dist/metrics/feature.js +6 -2
  73. package/dist/metrics/types.d.ts +5 -1
  74. package/dist/metrics/validate.js +3 -0
  75. package/dist/personal-access-tokens/schema/api-token.d.ts +3 -3
  76. package/dist/readiness/schema/probe-run.d.ts +60 -20
  77. package/dist/secrets/table.d.ts +1 -1
  78. package/dist/sessions/schema/user-session.d.ts +4 -4
  79. package/dist/shared/purge-row-file-refs.js +5 -4
  80. package/dist/template-resolver/table.d.ts +12 -12
  81. package/dist/template-resolver/user-content-table.d.ts +4 -4
  82. package/dist/tenant/invitation-table.d.ts +6 -6
  83. package/dist/tenant/schema/tenant.d.ts +32 -12
  84. package/dist/tier-engine/entity.d.ts +1 -1
  85. package/dist/tier-engine/tier-resolver.d.ts +1 -1
  86. package/dist/user/schema/user.d.ts +28 -8
  87. package/dist/user-data-rights/schema/download-attempt.d.ts +2 -2
  88. package/dist/user-data-rights/schema/download-token.d.ts +8 -8
  89. package/dist/user-data-rights/schema/export-job.d.ts +12 -12
  90. package/dist/waitlist/entity.d.ts +8 -8
  91. package/package.json +19 -11
  92. package/src/billing-foundation/changes.json +6 -0
  93. package/src/channel-in-app/changes.json +6 -0
  94. package/src/metrics/changes.json +6 -0
@@ -0,0 +1,29 @@
1
+ import { createEntityExecutor, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
3
+ import { FILE_REF_ENTITY_NAME, fileRefEntity } from "@cosmicdrift/kumiko-framework/files";
4
+ import { DOCUMENT_ENTITY } from "./constants.js";
5
+ const fileRefs = createEntityExecutor(FILE_REF_ENTITY_NAME, fileRefEntity).executor;
6
+ // One live document per file (unique on tenant + file): a member who names a colleague's pending
7
+ // upload would block the colleague's own later save of that file, because capture could no longer
8
+ // create its document. Files a job made (system uploader) and files already bound to a record stay
9
+ // allowed; for the latter the readGate's file-owner check covers it.
10
+ export const rejectForeignPendingFile = async (changes, ctx) => {
11
+ const file = changes["file"];
12
+ if (typeof file !== "string" || file === ctx.previous["file"])
13
+ return changes;
14
+ if (ctx.user.roles.includes(SYSTEM_ROLE))
15
+ return changes;
16
+ const ref = await fileRefs.detail({ id: file }, createSystemUser(ctx.user.tenantId), ctx.db);
17
+ // skip: a missing file is rejected by the framework's own file-reference check
18
+ if (ref === null)
19
+ return changes;
20
+ const uploader = ref["insertedById"];
21
+ const unbound = ref["entityId"] === null || ref["entityId"] === undefined;
22
+ if (unbound &&
23
+ typeof uploader === "string" &&
24
+ uploader !== ctx.user.id &&
25
+ uploader !== SYSTEM_USER_ID) {
26
+ throw new NotFoundError(DOCUMENT_ENTITY, file);
27
+ }
28
+ return changes;
29
+ };
@@ -0,0 +1,10 @@
1
+ import { type AccessRule } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function createExpiryOverviewQuery(access: AccessRule, expiringWithinDays: number): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"expiry-overview", import("zod").ZodObject<{
3
+ entityType: import("zod").ZodOptional<import("zod").ZodString>;
4
+ entityId: import("zod").ZodOptional<import("zod").ZodString>;
5
+ }, import("zod/v4/core").$strip>, {
6
+ expired: number;
7
+ expiring: number;
8
+ valid: number;
9
+ none: number;
10
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,41 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { expiryWindow } from "../expiry.js";
3
+ import { expiryOverviewPayloadSchema } from "../schemas.js";
4
+ import { linkedDocumentIds, listDocuments } from "./list-page.js";
5
+ // Counts come from the document list itself (totalCount), so they follow the same read gate
6
+ // as the rows a user can open.
7
+ export function createExpiryOverviewQuery(access, expiringWithinDays) {
8
+ return defineQueryHandler({
9
+ name: "expiry-overview",
10
+ schema: expiryOverviewPayloadSchema,
11
+ access,
12
+ liveEntities: ["filed-document", "filed-document-link"],
13
+ description: "Counts the documents the caller can read by validity: expired, expiring within the configured window, valid beyond it and without an expiry date; optionally for one record only.",
14
+ handler: async (query, ctx) => {
15
+ const payload = query.payload;
16
+ const { today, horizon } = expiryWindow(ctx, expiringWithinDays);
17
+ const scope = [];
18
+ if (payload.entityType !== undefined && payload.entityId !== undefined) {
19
+ const ids = await linkedDocumentIds(ctx, {
20
+ entityType: payload.entityType,
21
+ entityId: payload.entityId,
22
+ });
23
+ if (ids.length === 0)
24
+ return { expired: 0, expiring: 0, valid: 0, none: 0 };
25
+ scope.push({ field: "id", op: "in", value: ids });
26
+ }
27
+ const count = async (extra) => (await listDocuments(ctx, { limit: 1, totalCount: true, filters: [...scope, ...extra] }))
28
+ .total ?? 0;
29
+ const [all, expired, expiring, valid] = await Promise.all([
30
+ count([]),
31
+ count([{ field: "validUntil", op: "lt", value: today }]),
32
+ count([
33
+ { field: "validUntil", op: "gte", value: today },
34
+ { field: "validUntil", op: "lte", value: horizon },
35
+ ]),
36
+ count([{ field: "validUntil", op: "gt", value: horizon }]),
37
+ ]);
38
+ return { expired, expiring, valid, none: all - expired - expiring - valid };
39
+ },
40
+ });
41
+ }
@@ -0,0 +1,90 @@
1
+ import { type AccessRule } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function createForRecordQuery(access: AccessRule, linkTargets: readonly string[]): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"for-record", import("zod").ZodObject<{
3
+ entityType: import("zod").ZodString;
4
+ entityId: import("zod").ZodString;
5
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
6
+ cursor: import("zod").ZodOptional<import("zod").ZodString>;
7
+ offset: import("zod").ZodOptional<import("zod").ZodNumber>;
8
+ search: import("zod").ZodOptional<import("zod").ZodString>;
9
+ sort: import("zod").ZodOptional<import("zod").ZodString>;
10
+ sortDirection: import("zod").ZodOptional<import("zod").ZodEnum<{
11
+ asc: "asc";
12
+ desc: "desc";
13
+ }>>;
14
+ totalCount: import("zod").ZodOptional<import("zod").ZodBoolean>;
15
+ filter: import("zod").ZodOptional<import("zod").ZodObject<{
16
+ field: import("zod").ZodString;
17
+ op: import("zod").ZodEnum<{
18
+ eq: "eq";
19
+ gt: "gt";
20
+ gte: "gte";
21
+ in: "in";
22
+ lt: "lt";
23
+ lte: "lte";
24
+ ne: "ne";
25
+ }>;
26
+ value: import("zod").ZodUnknown;
27
+ }, import("zod/v4/core").$strip>>;
28
+ filters: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
29
+ field: import("zod").ZodString;
30
+ op: import("zod").ZodEnum<{
31
+ eq: "eq";
32
+ gt: "gt";
33
+ gte: "gte";
34
+ in: "in";
35
+ lt: "lt";
36
+ lte: "lte";
37
+ ne: "ne";
38
+ }>;
39
+ value: import("zod").ZodUnknown;
40
+ }, import("zod/v4/core").$strip>>>;
41
+ }, import("zod/v4/core").$strip>, {
42
+ rows: Record<string, unknown>[];
43
+ nextCursor?: string | null | undefined;
44
+ total?: number | undefined;
45
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
46
+ export declare function forTypeQueryName(entityType: string): string;
47
+ export declare function forTypeQueryQn(entityType: string): string;
48
+ export declare function createForTypeQuery(access: AccessRule, linkTargets: readonly string[], entityType: string): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<string, import("zod").ZodObject<{
49
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
50
+ cursor: import("zod").ZodOptional<import("zod").ZodString>;
51
+ offset: import("zod").ZodOptional<import("zod").ZodNumber>;
52
+ search: import("zod").ZodOptional<import("zod").ZodString>;
53
+ sort: import("zod").ZodOptional<import("zod").ZodString>;
54
+ sortDirection: import("zod").ZodOptional<import("zod").ZodEnum<{
55
+ asc: "asc";
56
+ desc: "desc";
57
+ }>>;
58
+ totalCount: import("zod").ZodOptional<import("zod").ZodBoolean>;
59
+ filter: import("zod").ZodOptional<import("zod").ZodObject<{
60
+ field: import("zod").ZodString;
61
+ op: import("zod").ZodEnum<{
62
+ eq: "eq";
63
+ gt: "gt";
64
+ gte: "gte";
65
+ in: "in";
66
+ lt: "lt";
67
+ lte: "lte";
68
+ ne: "ne";
69
+ }>;
70
+ value: import("zod").ZodUnknown;
71
+ }, import("zod/v4/core").$strip>>;
72
+ filters: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
73
+ field: import("zod").ZodString;
74
+ op: import("zod").ZodEnum<{
75
+ eq: "eq";
76
+ gt: "gt";
77
+ gte: "gte";
78
+ in: "in";
79
+ lt: "lt";
80
+ lte: "lte";
81
+ ne: "ne";
82
+ }>;
83
+ value: import("zod").ZodUnknown;
84
+ }, import("zod/v4/core").$strip>>>;
85
+ id: import("zod").ZodString;
86
+ }, import("zod/v4/core").$strip>, {
87
+ rows: Record<string, unknown>[];
88
+ nextCursor?: string | null | undefined;
89
+ total?: number | undefined;
90
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,54 @@
1
+ import { defineQueryHandler, toKebab, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { DocumentsQueries } from "../constants.js";
3
+ import { forRecordPayloadSchema } from "../schemas.js";
4
+ import { linkedDocumentIds, listDocuments } from "./list-page.js";
5
+ const EMPTY_PAGE = { rows: [], nextCursor: null, total: 0 };
6
+ const LIVE_ENTITIES = ["filed-document", "filed-document-link"];
7
+ async function documentsOfRecord(ctx, linkTargets, payload) {
8
+ const { entityType, entityId, filter, filters, ...paging } = payload;
9
+ if (!linkTargets.includes(entityType))
10
+ return EMPTY_PAGE;
11
+ const ids = await linkedDocumentIds(ctx, { entityType, entityId });
12
+ if (ids.length === 0)
13
+ return EMPTY_PAGE;
14
+ return listDocuments(ctx, {
15
+ ...paging,
16
+ filters: [
17
+ { field: "id", op: "in", value: ids },
18
+ ...(filter !== undefined ? [filter] : []),
19
+ ...(filters ?? []),
20
+ ],
21
+ });
22
+ }
23
+ export function createForRecordQuery(access, linkTargets) {
24
+ return defineQueryHandler({
25
+ name: "for-record",
26
+ schema: forRecordPayloadSchema,
27
+ access,
28
+ liveEntities: LIVE_ENTITIES,
29
+ description: "Lists the documents linked to one record, paged and filterable like the document list; a record the caller cannot read, or a type that takes no documents, yields an empty page.",
30
+ handler: async (query, ctx) => documentsOfRecord(ctx, linkTargets, query.payload),
31
+ });
32
+ }
33
+ export function forTypeQueryName(entityType) {
34
+ return `for-${toKebab(entityType)}`;
35
+ }
36
+ export function forTypeQueryQn(entityType) {
37
+ return `${DocumentsQueries.forRecord.replace(/for-record$/, "")}${forTypeQueryName(entityType)}`;
38
+ }
39
+ // A relatedList tab sends only the parent id, so the host type is fixed per query.
40
+ export function createForTypeQuery(access, linkTargets, entityType) {
41
+ return defineQueryHandler({
42
+ name: forTypeQueryName(entityType),
43
+ schema: forRecordPayloadSchema.omit({ entityType: true, entityId: true }).extend({
44
+ id: forRecordPayloadSchema.shape.entityId,
45
+ }),
46
+ access,
47
+ liveEntities: LIVE_ENTITIES,
48
+ description: `Lists the documents linked to one ${entityType} record by its id, paged and filterable like the document list; it feeds the documents tab on the ${entityType} screen.`,
49
+ handler: async (query, ctx) => {
50
+ const { id, ...rest } = query.payload;
51
+ return documentsOfRecord(ctx, linkTargets, { ...rest, entityType, entityId: id });
52
+ },
53
+ });
54
+ }
@@ -0,0 +1,19 @@
1
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ declare const pageSchema: z.ZodObject<{
4
+ rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
5
+ nextCursor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
6
+ total: z.ZodOptional<z.ZodNumber>;
7
+ }, z.core.$strip>;
8
+ export type ListPage = z.infer<typeof pageSchema>;
9
+ export type ListFilter = {
10
+ readonly field: string;
11
+ readonly op: string;
12
+ readonly value: unknown;
13
+ };
14
+ export declare function listDocuments(ctx: Pick<HandlerContext, "dispatch">, payload: Record<string, unknown>): Promise<ListPage>;
15
+ export declare function linkedDocumentIds(ctx: Pick<HandlerContext, "dispatch">, target: {
16
+ readonly entityType: string;
17
+ readonly entityId: string;
18
+ }): Promise<string[]>;
19
+ export {};
@@ -0,0 +1,36 @@
1
+ import * as z from "zod";
2
+ import { DocumentsQueries } from "../constants.js";
3
+ const LINK_LIST = "documents:query:filed-document-link:list";
4
+ const LINK_PAGE_SIZE = 200;
5
+ const pageSchema = z.object({
6
+ rows: z.array(z.record(z.string(), z.unknown())),
7
+ nextCursor: z.string().nullable().optional(),
8
+ total: z.number().optional(),
9
+ });
10
+ export async function listDocuments(ctx, payload) {
11
+ return pageSchema.parse(await ctx.dispatch.query(DocumentsQueries.list, payload));
12
+ }
13
+ // Goes through the link list, so a record the caller cannot read yields no ids (the link's
14
+ // parentRef gate), and a document is then fetched through the document list, which applies the
15
+ // document's own read gate again.
16
+ export async function linkedDocumentIds(ctx, target) {
17
+ const ids = [];
18
+ let cursor;
19
+ do {
20
+ const page = pageSchema.parse(await ctx.dispatch.query(LINK_LIST, {
21
+ filters: [
22
+ { field: "entityType", op: "eq", value: target.entityType },
23
+ { field: "entityId", op: "eq", value: target.entityId },
24
+ ],
25
+ limit: LINK_PAGE_SIZE,
26
+ ...(cursor !== undefined && { cursor }),
27
+ }));
28
+ for (const row of page.rows) {
29
+ const documentId = row["documentId"];
30
+ if (typeof documentId === "string")
31
+ ids.push(documentId);
32
+ }
33
+ cursor = page.nextCursor ?? undefined;
34
+ } while (cursor !== undefined);
35
+ return ids;
36
+ }
@@ -0,0 +1,32 @@
1
+ import { type AccessRule } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function createOverviewQuery(access: AccessRule, expiringWithinDays: number): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"overview", import("zod").ZodObject<{
3
+ limit: import("zod").ZodOptional<import("zod").ZodNumber>;
4
+ cursor: import("zod").ZodOptional<import("zod").ZodString>;
5
+ offset: import("zod").ZodOptional<import("zod").ZodNumber>;
6
+ search: import("zod").ZodOptional<import("zod").ZodString>;
7
+ sort: import("zod").ZodOptional<import("zod").ZodString>;
8
+ sortDirection: import("zod").ZodOptional<import("zod").ZodEnum<{
9
+ asc: "asc";
10
+ desc: "desc";
11
+ }>>;
12
+ totalCount: import("zod").ZodOptional<import("zod").ZodBoolean>;
13
+ filters: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
14
+ field: import("zod").ZodString;
15
+ op: import("zod").ZodEnum<{
16
+ eq: "eq";
17
+ gt: "gt";
18
+ gte: "gte";
19
+ in: "in";
20
+ lt: "lt";
21
+ lte: "lte";
22
+ ne: "ne";
23
+ }>;
24
+ value: import("zod").ZodUnknown;
25
+ }, import("zod/v4/core").$strip>>>;
26
+ }, import("zod/v4/core").$strip>, {
27
+ nextCursor?: string | null | undefined;
28
+ total?: number | undefined;
29
+ rows: {
30
+ expiry: "expired" | "expiring" | "none" | "valid";
31
+ }[];
32
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,41 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { EXPIRING_FACET_VALUE } from "../constants.js";
3
+ import { expiryStateOf, expiryWindow } from "../expiry.js";
4
+ import { overviewPayloadSchema } from "../schemas.js";
5
+ import { listDocuments } from "./list-page.js";
6
+ const EXPIRY_FILTER_FIELD = "expiry";
7
+ // The "expiring" facet is a moving date window, which a facet on a stored field cannot
8
+ // express; it becomes a validUntil bound here, expired documents included. A document
9
+ // without a date never matches it.
10
+ function toDocumentFilters(filters, horizon) {
11
+ return filters.flatMap((clause) => {
12
+ if (clause.field !== EXPIRY_FILTER_FIELD)
13
+ return [clause];
14
+ const wantsExpiring = Array.isArray(clause.value) && clause.value.includes(EXPIRING_FACET_VALUE);
15
+ return wantsExpiring ? [{ field: "validUntil", op: "lte", value: horizon }] : [];
16
+ });
17
+ }
18
+ export function createOverviewQuery(access, expiringWithinDays) {
19
+ return defineQueryHandler({
20
+ name: "overview",
21
+ schema: overviewPayloadSchema,
22
+ access,
23
+ liveEntities: ["filed-document", "filed-document-link"],
24
+ description: "Lists the documents the caller can read with an expiry state per row (expired, expiring, valid, none); the expiring filter returns expired and soon-expiring documents. Powers the documents list screen.",
25
+ handler: async (query, ctx) => {
26
+ const { filters, ...paging } = query.payload;
27
+ const window = expiryWindow(ctx, expiringWithinDays);
28
+ const page = await listDocuments(ctx, {
29
+ ...paging,
30
+ filters: toDocumentFilters(filters ?? [], window.horizon),
31
+ });
32
+ return {
33
+ ...page,
34
+ rows: page.rows.map((row) => ({
35
+ ...row,
36
+ expiry: expiryStateOf(row["validUntil"], window),
37
+ })),
38
+ };
39
+ },
40
+ });
41
+ }
@@ -0,0 +1,93 @@
1
+ import * as z from "zod";
2
+ export declare const documentLinkPayloadSchema: z.ZodObject<{
3
+ documentId: z.ZodUUID;
4
+ entityType: z.ZodString;
5
+ entityId: z.ZodString;
6
+ }, z.core.$strip>;
7
+ export type DocumentLinkPayload = z.infer<typeof documentLinkPayloadSchema>;
8
+ export declare const importDocumentPayloadSchema: z.ZodObject<{
9
+ id: z.ZodUUID;
10
+ title: z.ZodString;
11
+ category: z.ZodString;
12
+ issuedAt: z.ZodOptional<z.ZodISODate>;
13
+ validUntil: z.ZodOptional<z.ZodISODate>;
14
+ fileRefId: z.ZodUUID;
15
+ links: z.ZodArray<z.ZodObject<{
16
+ entityType: z.ZodString;
17
+ entityId: z.ZodString;
18
+ }, z.core.$strip>>;
19
+ deleted: z.ZodOptional<z.ZodBoolean>;
20
+ }, z.core.$strip>;
21
+ export type ImportDocumentPayload = z.infer<typeof importDocumentPayloadSchema>;
22
+ export declare const forRecordPayloadSchema: z.ZodObject<{
23
+ entityType: z.ZodString;
24
+ entityId: z.ZodString;
25
+ limit: z.ZodOptional<z.ZodNumber>;
26
+ cursor: z.ZodOptional<z.ZodString>;
27
+ offset: z.ZodOptional<z.ZodNumber>;
28
+ search: z.ZodOptional<z.ZodString>;
29
+ sort: z.ZodOptional<z.ZodString>;
30
+ sortDirection: z.ZodOptional<z.ZodEnum<{
31
+ asc: "asc";
32
+ desc: "desc";
33
+ }>>;
34
+ totalCount: z.ZodOptional<z.ZodBoolean>;
35
+ filter: z.ZodOptional<z.ZodObject<{
36
+ field: z.ZodString;
37
+ op: z.ZodEnum<{
38
+ eq: "eq";
39
+ gt: "gt";
40
+ gte: "gte";
41
+ in: "in";
42
+ lt: "lt";
43
+ lte: "lte";
44
+ ne: "ne";
45
+ }>;
46
+ value: z.ZodUnknown;
47
+ }, z.core.$strip>>;
48
+ filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
49
+ field: z.ZodString;
50
+ op: z.ZodEnum<{
51
+ eq: "eq";
52
+ gt: "gt";
53
+ gte: "gte";
54
+ in: "in";
55
+ lt: "lt";
56
+ lte: "lte";
57
+ ne: "ne";
58
+ }>;
59
+ value: z.ZodUnknown;
60
+ }, z.core.$strip>>>;
61
+ }, z.core.$strip>;
62
+ export type ForRecordPayload = z.infer<typeof forRecordPayloadSchema>;
63
+ export declare const overviewPayloadSchema: z.ZodObject<{
64
+ limit: z.ZodOptional<z.ZodNumber>;
65
+ cursor: z.ZodOptional<z.ZodString>;
66
+ offset: z.ZodOptional<z.ZodNumber>;
67
+ search: z.ZodOptional<z.ZodString>;
68
+ sort: z.ZodOptional<z.ZodString>;
69
+ sortDirection: z.ZodOptional<z.ZodEnum<{
70
+ asc: "asc";
71
+ desc: "desc";
72
+ }>>;
73
+ totalCount: z.ZodOptional<z.ZodBoolean>;
74
+ filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
75
+ field: z.ZodString;
76
+ op: z.ZodEnum<{
77
+ eq: "eq";
78
+ gt: "gt";
79
+ gte: "gte";
80
+ in: "in";
81
+ lt: "lt";
82
+ lte: "lte";
83
+ ne: "ne";
84
+ }>;
85
+ value: z.ZodUnknown;
86
+ }, z.core.$strip>>>;
87
+ }, z.core.$strip>;
88
+ export type OverviewPayload = z.infer<typeof overviewPayloadSchema>;
89
+ export declare const expiryOverviewPayloadSchema: z.ZodObject<{
90
+ entityType: z.ZodOptional<z.ZodString>;
91
+ entityId: z.ZodOptional<z.ZodString>;
92
+ }, z.core.$strip>;
93
+ export type ExpiryOverviewPayload = z.infer<typeof expiryOverviewPayloadSchema>;
@@ -0,0 +1,51 @@
1
+ import * as z from "zod";
2
+ export const documentLinkPayloadSchema = z.object({
3
+ documentId: z.uuid(),
4
+ entityType: z.string().min(1).max(64),
5
+ entityId: z.string().min(1).max(128),
6
+ });
7
+ const isoDate = z.iso.date();
8
+ export const importDocumentPayloadSchema = z.object({
9
+ id: z.uuid(),
10
+ title: z.string().trim().min(1).max(200),
11
+ category: z.string().min(1),
12
+ issuedAt: isoDate.optional(),
13
+ validUntil: isoDate.optional(),
14
+ fileRefId: z.uuid(),
15
+ links: z.array(z.object({ entityType: z.string().min(1).max(64), entityId: z.string().min(1).max(128) })),
16
+ deleted: z.boolean().optional(),
17
+ });
18
+ export const forRecordPayloadSchema = z.object({
19
+ entityType: z.string().min(1).max(64),
20
+ entityId: z.string().min(1).max(128),
21
+ limit: z.number().int().nonnegative().max(200).optional(),
22
+ cursor: z.string().optional(),
23
+ offset: z.number().int().nonnegative().optional(),
24
+ search: z.string().optional(),
25
+ sort: z.string().optional(),
26
+ sortDirection: z.enum(["asc", "desc"]).optional(),
27
+ totalCount: z.boolean().optional(),
28
+ filter: z
29
+ .object({
30
+ field: z.string(),
31
+ op: z.enum(["eq", "ne", "lt", "gt", "lte", "gte", "in"]),
32
+ value: z.unknown(),
33
+ })
34
+ .optional(),
35
+ filters: z
36
+ .array(z.object({
37
+ field: z.string(),
38
+ op: z.enum(["eq", "ne", "lt", "gt", "lte", "gte", "in"]),
39
+ value: z.unknown(),
40
+ }))
41
+ .optional(),
42
+ });
43
+ export const overviewPayloadSchema = forRecordPayloadSchema.omit({
44
+ entityType: true,
45
+ entityId: true,
46
+ filter: true,
47
+ });
48
+ export const expiryOverviewPayloadSchema = z.object({
49
+ entityType: z.string().min(1).max(64).optional(),
50
+ entityId: z.string().min(1).max(128).optional(),
51
+ });
@@ -0,0 +1,3 @@
1
+ import { type AccessRule, type ProjectionListScreenDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const DOCUMENT_COLUMNS: readonly ["title", "category", "validUntil", "file"];
3
+ export declare function createDocumentListScreen(access: AccessRule, categories: readonly string[]): ProjectionListScreenDefinition;
@@ -0,0 +1,76 @@
1
+ import { i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { DOCUMENTS_LIST_SCREEN_ID, DocumentsQueries, EXPIRING_FACET_VALUE } from "./constants.js";
3
+ // The columns every document list shows; the tab and the standalone screen stay alike.
4
+ export const DOCUMENT_COLUMNS = ["title", "category", "validUntil", "file"];
5
+ const FIELD_PREFIX = "documents:entity:filed-document:field:";
6
+ const CATEGORY_OPTION_PREFIX = `${FIELD_PREFIX}category:option:`;
7
+ // A projectionList, not an entityList: the expiry state and its "expiring" filter are computed
8
+ // per request from a moving date window, which an entityList's facets on stored fields cannot
9
+ // express. The overview query still reads through the document list, so the read gate holds.
10
+ export function createDocumentListScreen(access, categories) {
11
+ return {
12
+ id: DOCUMENTS_LIST_SCREEN_ID,
13
+ type: "projectionList",
14
+ query: DocumentsQueries.overview,
15
+ columns: [
16
+ { field: "title", label: `${FIELD_PREFIX}title` },
17
+ {
18
+ field: "category",
19
+ label: `${FIELD_PREFIX}category`,
20
+ renderer: { format: "enumOption", keyPrefix: CATEGORY_OPTION_PREFIX },
21
+ },
22
+ {
23
+ field: "validUntil",
24
+ label: `${FIELD_PREFIX}validUntil`,
25
+ renderer: { format: "date", dateStyle: "medium" },
26
+ },
27
+ {
28
+ field: "expiry",
29
+ label: "documents:list.facet.expiry",
30
+ sortable: false,
31
+ hideOnNarrow: true,
32
+ renderer: { format: "enumOption", keyPrefix: "documents:list.expiry." },
33
+ },
34
+ {
35
+ field: "file",
36
+ label: `${FIELD_PREFIX}file`,
37
+ sortable: false,
38
+ renderer: { format: "file" },
39
+ },
40
+ ],
41
+ pagination: "pages",
42
+ defaultSort: { field: "validUntil", dir: "asc" },
43
+ searchable: true,
44
+ facets: [
45
+ {
46
+ field: "category",
47
+ type: "select",
48
+ label: `${FIELD_PREFIX}category`,
49
+ options: categories.map((value) => ({
50
+ value,
51
+ label: `${CATEGORY_OPTION_PREFIX}${value}`,
52
+ })),
53
+ },
54
+ {
55
+ field: "expiry",
56
+ type: "select",
57
+ label: "documents:list.facet.expiry",
58
+ options: [{ value: EXPIRING_FACET_VALUE, label: "documents:list.facet.expiring" }],
59
+ },
60
+ ],
61
+ rowActions: [
62
+ {
63
+ kind: "writeHandler",
64
+ id: "delete",
65
+ label: i18nKey("kumiko.actions.delete"),
66
+ handler: "documents:write:filed-document:delete",
67
+ payload: { pick: ["id"] },
68
+ confirm: i18nKey("kumiko.actions.delete-confirm"),
69
+ style: "danger",
70
+ },
71
+ ],
72
+ access,
73
+ // The app places it with r.nav("documents:screen:list").
74
+ dormant: true,
75
+ };
76
+ }
@@ -0,0 +1,4 @@
1
+ import type { EditRelatedListSection } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { DashboardStatPanel } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ export declare function documentsTab(entityType: string): EditRelatedListSection;
4
+ export declare function documentExpiryPanels(): readonly DashboardStatPanel[];
@@ -0,0 +1,42 @@
1
+ import { DocumentsQueries } from "./constants.js";
2
+ import { forTypeQueryQn } from "./queries/for-record.query.js";
3
+ // "Documents" tab for the edit screen of a link target. The tab id is the same on every screen,
4
+ // so deep links like ?tab=documents work across hosts.
5
+ export function documentsTab(entityType) {
6
+ return {
7
+ id: "documents",
8
+ kind: "relatedList",
9
+ title: "documents:tab.title",
10
+ query: forTypeQueryQn(entityType),
11
+ entity: "documents:document",
12
+ columns: [
13
+ { field: "title", sortable: true },
14
+ { field: "category", sortable: true },
15
+ { field: "validUntil", sortable: true },
16
+ { field: "file" },
17
+ ],
18
+ searchable: true,
19
+ count: true,
20
+ };
21
+ }
22
+ // Stat tiles for a dashboard: how many readable documents are expired or about to expire.
23
+ export function documentExpiryPanels() {
24
+ return [
25
+ {
26
+ kind: "stat",
27
+ id: "documents-expired",
28
+ label: "documents:overview.expired",
29
+ query: DocumentsQueries.expiryOverview,
30
+ valueField: "expired",
31
+ tone: "negative",
32
+ },
33
+ {
34
+ kind: "stat",
35
+ id: "documents-expiring",
36
+ label: "documents:overview.expiring",
37
+ query: DocumentsQueries.expiryOverview,
38
+ valueField: "expiring",
39
+ tone: "warn",
40
+ },
41
+ ];
42
+ }
@@ -0,0 +1,3 @@
1
+ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import { type Registry, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ export declare function canWriteLinkTarget(registry: Registry, entityType: string, entityId: string, user: SessionUser, db: TenantDb): Promise<boolean>;