@cosmicdrift/kumiko-bundled-features 0.399.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 (75) hide show
  1. package/dist/auth-mfa/schema/user-mfa.d.ts +4 -4
  2. package/dist/billing-foundation/entities.d.ts +2 -2
  3. package/dist/billing-foundation/projection.d.ts +2 -2
  4. package/dist/cap-counter/entity.d.ts +3 -3
  5. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
  6. package/dist/custom-fields/entity.d.ts +23 -3
  7. package/dist/delivery/tables.d.ts +11 -1
  8. package/dist/documents/aggregate-id.d.ts +2 -0
  9. package/dist/documents/aggregate-id.js +12 -0
  10. package/dist/documents/capture.d.ts +14 -0
  11. package/dist/documents/capture.js +65 -0
  12. package/dist/documents/constants.d.ts +23 -0
  13. package/dist/documents/constants.js +21 -0
  14. package/dist/documents/ensure-link.d.ts +17 -0
  15. package/dist/documents/ensure-link.js +20 -0
  16. package/dist/documents/entity.d.ts +76 -0
  17. package/dist/documents/entity.js +84 -0
  18. package/dist/documents/executor.d.ts +6 -0
  19. package/dist/documents/executor.js +8 -0
  20. package/dist/documents/expiry.d.ts +9 -0
  21. package/dist/documents/expiry.js +20 -0
  22. package/dist/documents/feature.d.ts +33 -0
  23. package/dist/documents/feature.js +111 -0
  24. package/dist/documents/handlers/import.write.d.ts +23 -0
  25. package/dist/documents/handlers/import.write.js +104 -0
  26. package/dist/documents/handlers/link.write.d.ts +18 -0
  27. package/dist/documents/handlers/link.write.js +61 -0
  28. package/dist/documents/i18n.d.ts +6 -0
  29. package/dist/documents/i18n.js +33 -0
  30. package/dist/documents/index.d.ts +7 -0
  31. package/dist/documents/index.js +6 -0
  32. package/dist/documents/pending-file-guard.d.ts +2 -0
  33. package/dist/documents/pending-file-guard.js +29 -0
  34. package/dist/documents/queries/expiry-overview.query.d.ts +10 -0
  35. package/dist/documents/queries/expiry-overview.query.js +41 -0
  36. package/dist/documents/queries/for-record.query.d.ts +90 -0
  37. package/dist/documents/queries/for-record.query.js +54 -0
  38. package/dist/documents/queries/list-page.d.ts +19 -0
  39. package/dist/documents/queries/list-page.js +36 -0
  40. package/dist/documents/queries/overview.query.d.ts +32 -0
  41. package/dist/documents/queries/overview.query.js +41 -0
  42. package/dist/documents/schemas.d.ts +93 -0
  43. package/dist/documents/schemas.js +51 -0
  44. package/dist/documents/screens.d.ts +3 -0
  45. package/dist/documents/screens.js +76 -0
  46. package/dist/documents/tab.d.ts +4 -0
  47. package/dist/documents/tab.js +42 -0
  48. package/dist/documents/target-write-check.d.ts +3 -0
  49. package/dist/documents/target-write-check.js +25 -0
  50. package/dist/documents/write-guard.d.ts +6 -0
  51. package/dist/documents/write-guard.js +46 -0
  52. package/dist/inbound-mail-foundation/entities.d.ts +17 -7
  53. package/dist/inbound-mail-foundation/ingest-failures.d.ts +3 -3
  54. package/dist/inbound-mail-foundation/projection.d.ts +15 -5
  55. package/dist/inbound-mail-foundation/quarantine-entities.d.ts +3 -3
  56. package/dist/jobs/job-run-table.d.ts +4 -4
  57. package/dist/ledger/entity.d.ts +8 -8
  58. package/dist/managed-pages/table.d.ts +24 -4
  59. package/dist/personal-access-tokens/schema/api-token.d.ts +3 -3
  60. package/dist/readiness/schema/probe-run.d.ts +60 -20
  61. package/dist/secrets/table.d.ts +1 -1
  62. package/dist/sessions/schema/user-session.d.ts +4 -4
  63. package/dist/shared/purge-row-file-refs.js +5 -4
  64. package/dist/template-resolver/table.d.ts +12 -12
  65. package/dist/template-resolver/user-content-table.d.ts +4 -4
  66. package/dist/tenant/invitation-table.d.ts +6 -6
  67. package/dist/tenant/schema/tenant.d.ts +32 -12
  68. package/dist/tier-engine/entity.d.ts +1 -1
  69. package/dist/tier-engine/tier-resolver.d.ts +1 -1
  70. package/dist/user/schema/user.d.ts +28 -8
  71. package/dist/user-data-rights/schema/download-attempt.d.ts +2 -2
  72. package/dist/user-data-rights/schema/download-token.d.ts +8 -8
  73. package/dist/user-data-rights/schema/export-job.d.ts +12 -12
  74. package/dist/waitlist/entity.d.ts +8 -8
  75. package/package.json +19 -11
@@ -21,7 +21,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
21
21
  readonly min?: string;
22
22
  readonly max?: string;
23
23
  readonly locale?: string;
24
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
24
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
25
25
  required: true;
26
26
  };
27
27
  readonly lastUsedAt: {
@@ -37,7 +37,7 @@ export declare const userMfaEntity: import("@cosmicdrift/kumiko-types/fields").E
37
37
  readonly min?: string;
38
38
  readonly max?: string;
39
39
  readonly locale?: string;
40
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
40
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
41
41
  required: false;
42
42
  };
43
43
  }, "tenant">;
@@ -64,7 +64,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
64
64
  readonly min?: string;
65
65
  readonly max?: string;
66
66
  readonly locale?: string;
67
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
67
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
68
68
  required: true;
69
69
  };
70
70
  readonly lastUsedAt: {
@@ -80,7 +80,7 @@ export declare const userMfaTable: import("@cosmicdrift/kumiko-framework/db").En
80
80
  readonly min?: string;
81
81
  readonly max?: string;
82
82
  readonly locale?: string;
83
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
83
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
84
84
  required: false;
85
85
  };
86
86
  }, "tenant">>;
@@ -27,7 +27,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
27
27
  readonly min?: string;
28
28
  readonly max?: string;
29
29
  readonly locale?: string;
30
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
30
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
31
31
  required: true;
32
32
  };
33
33
  readonly cancelAt: {
@@ -43,7 +43,7 @@ export declare const subscriptionEntity: import("@cosmicdrift/kumiko-types/field
43
43
  readonly min?: string;
44
44
  readonly max?: string;
45
45
  readonly locale?: string;
46
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
46
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
47
47
  required: false;
48
48
  };
49
49
  }, "tenant">;
@@ -28,7 +28,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
28
28
  readonly min?: string;
29
29
  readonly max?: string;
30
30
  readonly locale?: string;
31
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
31
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
32
32
  required: true;
33
33
  };
34
34
  readonly cancelAt: {
@@ -44,7 +44,7 @@ export declare const subscriptionsProjectionTable: import("@cosmicdrift/kumiko-f
44
44
  readonly min?: string;
45
45
  readonly max?: string;
46
46
  readonly locale?: string;
47
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
47
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
48
48
  required: false;
49
49
  };
50
50
  }, "tenant">>;
@@ -12,7 +12,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
12
12
  readonly sensitive?: boolean;
13
13
  readonly default?: number;
14
14
  readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
15
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
15
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
16
16
  required: true;
17
17
  };
18
18
  readonly periodStart: {
@@ -28,7 +28,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
28
28
  readonly min?: string;
29
29
  readonly max?: string;
30
30
  readonly locale?: string;
31
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
31
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
32
32
  required: true;
33
33
  };
34
34
  readonly lastSoftWarnedAt: {
@@ -44,7 +44,7 @@ export declare const capCounterEntity: import("@cosmicdrift/kumiko-types/fields"
44
44
  readonly min?: string;
45
45
  readonly max?: string;
46
46
  readonly locale?: string;
47
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
47
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
48
48
  required: false;
49
49
  };
50
50
  }, "tenant">;
@@ -21,7 +21,7 @@ export declare const tenantComplianceProfileEntity: import("@cosmicdrift/kumiko-
21
21
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
22
22
  readonly optionsAvailabilityQuery?: string;
23
23
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"de-hr-dsgvo-hgb" | "eu-dsgvo" | "minimal-no-region" | "swiss-dsg">[] | undefined;
24
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
24
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
25
25
  required: true;
26
26
  };
27
27
  readonly override: import("@cosmicdrift/kumiko-types/fields").EntityLongTextFieldDef & {
@@ -51,7 +51,7 @@ export declare const tenantComplianceProfileTable: import("@cosmicdrift/kumiko-f
51
51
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
52
52
  readonly optionsAvailabilityQuery?: string;
53
53
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"de-hr-dsgvo-hgb" | "eu-dsgvo" | "minimal-no-region" | "swiss-dsg">[] | undefined;
54
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
54
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
55
55
  required: true;
56
56
  };
57
57
  readonly override: import("@cosmicdrift/kumiko-types/fields").EntityLongTextFieldDef & {
@@ -8,11 +8,31 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
8
8
  readonly type: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
9
9
  required: true;
10
10
  };
11
- readonly required: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
11
+ readonly required: {
12
+ readonly type: "boolean";
13
+ readonly description?: string;
14
+ readonly required?: boolean;
15
+ readonly sortable?: boolean;
16
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
17
+ readonly filterable?: boolean;
18
+ readonly sensitive?: boolean;
19
+ readonly default?: boolean;
20
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
21
+ } & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
12
22
  required: true;
13
23
  default: boolean;
14
24
  };
15
- readonly searchable: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
25
+ readonly searchable: {
26
+ readonly type: "boolean";
27
+ readonly description?: string;
28
+ readonly required?: boolean;
29
+ readonly sortable?: boolean;
30
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
31
+ readonly filterable?: boolean;
32
+ readonly sensitive?: boolean;
33
+ readonly default?: boolean;
34
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
35
+ } & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
16
36
  required: true;
17
37
  default: boolean;
18
38
  };
@@ -33,7 +53,7 @@ export declare const fieldDefinitionEntity: import("@cosmicdrift/kumiko-types/fi
33
53
  readonly field: string;
34
54
  };
35
55
  readonly grouping?: boolean;
36
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
56
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
37
57
  required: true;
38
58
  };
39
59
  readonly serializedField: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -11,7 +11,17 @@ export declare const notificationPreferenceEntity: import("@cosmicdrift/kumiko-t
11
11
  readonly channel: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
12
12
  required: true;
13
13
  };
14
- readonly enabled: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
14
+ readonly enabled: {
15
+ readonly type: "boolean";
16
+ readonly description?: string;
17
+ readonly required?: boolean;
18
+ readonly sortable?: boolean;
19
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
20
+ readonly filterable?: boolean;
21
+ readonly sensitive?: boolean;
22
+ readonly default?: boolean;
23
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
24
+ } & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
15
25
  required: false;
16
26
  default: boolean;
17
27
  };
@@ -0,0 +1,2 @@
1
+ export declare function documentIdForFile(tenantId: string, fileRefId: string): string;
2
+ export declare function documentLinkAggregateId(tenantId: string, documentId: string, entityType: string, entityId: string): string;
@@ -0,0 +1,12 @@
1
+ import { v5 as uuidv5 } from "uuid";
2
+ // Frozen: changing a namespace re-keys every derived stream and breaks replay.
3
+ const DOCUMENT_FOR_FILE_NAMESPACE = "6d2f7e41-3c58-4b0a-9e17-5a4c8d1b2f93";
4
+ const DOCUMENT_LINK_NAMESPACE = "b83a1c6e-9f20-4d57-a1e4-7c0d52f68b19";
5
+ // @wrapper-known uuid-domain
6
+ export function documentIdForFile(tenantId, fileRefId) {
7
+ return uuidv5(`${tenantId}|${fileRefId}`, DOCUMENT_FOR_FILE_NAMESPACE);
8
+ }
9
+ // @wrapper-known uuid-domain
10
+ export function documentLinkAggregateId(tenantId, documentId, entityType, entityId) {
11
+ return uuidv5(`${tenantId}|${documentId}|${entityType}|${entityId}`, DOCUMENT_LINK_NAMESPACE);
12
+ }
@@ -0,0 +1,14 @@
1
+ import { type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { BootCheckContext } from "@cosmicdrift/kumiko-types/feature";
3
+ import type { DOCUMENTS_FEATURE_NAME } from "./constants.ts";
4
+ import { type LinkTarget } from "./ensure-link.ts";
5
+ import type { DocumentExecutors } from "./executor.ts";
6
+ export type DocumentCaptureRule = {
7
+ readonly entity: string;
8
+ readonly field: string;
9
+ readonly category: string;
10
+ readonly title?: (row: Readonly<Record<string, unknown>>) => string;
11
+ readonly links?: (row: Readonly<Record<string, unknown>>) => readonly LinkTarget[];
12
+ };
13
+ export declare function registerCapture(r: FeatureRegistrar<typeof DOCUMENTS_FEATURE_NAME>, executors: DocumentExecutors, rules: readonly DocumentCaptureRule[], linkTargets: readonly string[]): void;
14
+ export declare function validateCaptureRules({ features }: BootCheckContext, rules: readonly DocumentCaptureRule[], categories: readonly string[], linkTargets: readonly string[]): void;
@@ -0,0 +1,65 @@
1
+ import { createSystemUser, } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { documentIdForFile } from "./aggregate-id.js";
3
+ import { ensureDocumentLink } from "./ensure-link.js";
4
+ const MAX_TITLE_LENGTH = 200;
5
+ async function captureFile(executors, rule, linkTargets, saved, fileRefId, user, db) {
6
+ const id = documentIdForFile(user.tenantId, fileRefId);
7
+ // The title never defaults to the file name, which can carry personal data.
8
+ const title = (rule.title?.(saved.data) ?? rule.category).slice(0, MAX_TITLE_LENGTH);
9
+ const created = await executors.document.create({ id, title, category: rule.category, file: fileRefId }, user, db);
10
+ // version_conflict: the stream exists already, so the file was captured before (and a document
11
+ // deleted since stays deleted).
12
+ if (!created.isSuccess && created.error.code !== "version_conflict") {
13
+ throw new Error(`documents capture failed for ${rule.entity}.${rule.field}: ${created.error.code}`);
14
+ }
15
+ // The gate hides rows from a caller without the registry; existence is the system user's question.
16
+ const live = await executors.document.detail({ id }, createSystemUser(user.tenantId), db);
17
+ // skip: the document was deleted after an earlier capture and stays deleted
18
+ if (live === null)
19
+ return;
20
+ const targets = [
21
+ { entityType: rule.entity, entityId: saved.id },
22
+ // Trusted: derived from the saved row, see DocumentCaptureRule.links.
23
+ ...(rule.links?.(saved.data) ?? []),
24
+ ].filter((target) => linkTargets.includes(target.entityType));
25
+ for (const target of targets) {
26
+ const linked = await ensureDocumentLink(executors, user, db, id, target);
27
+ if (!linked.isSuccess) {
28
+ throw new Error(`documents capture failed to link ${target.entityType}: ${linked.error.code}`);
29
+ }
30
+ }
31
+ }
32
+ export function registerCapture(r, executors, rules, linkTargets) {
33
+ for (const rule of rules) {
34
+ r.hook({
35
+ type: "postSave",
36
+ target: { allOf: rule.entity },
37
+ handler: async (result, ctx) => {
38
+ const value = result.changes[rule.field] ?? (result.isNew ? result.data[rule.field] : undefined);
39
+ // skip: the file field was not part of this save
40
+ if (typeof value !== "string")
41
+ return;
42
+ await captureFile(executors, rule, linkTargets, { id: String(result.id), data: result.data }, value, ctx.user, ctx.db);
43
+ },
44
+ });
45
+ }
46
+ }
47
+ export function validateCaptureRules({ features }, rules, categories, linkTargets) {
48
+ for (const rule of rules) {
49
+ const label = `documents capture ${rule.entity}.${rule.field}`;
50
+ if (!categories.includes(rule.category)) {
51
+ throw new Error(`${label}: category "${rule.category}" is not in the configured categories.`);
52
+ }
53
+ const entity = features.map((f) => f.entities?.[rule.entity]).find((e) => e !== undefined);
54
+ if (entity === undefined)
55
+ throw new Error(`${label}: entity "${rule.entity}" is not registered.`);
56
+ const field = entity.fields[rule.field];
57
+ if (field?.type !== "file" && field?.type !== "image") {
58
+ throw new Error(`${label}: the field must be a single file or image field.`);
59
+ }
60
+ }
61
+ const unknownTarget = linkTargets.find((name) => !features.some((f) => f.entities?.[name] !== undefined));
62
+ if (unknownTarget !== undefined) {
63
+ throw new Error(`documents linkTargets: entity "${unknownTarget}" is not registered.`);
64
+ }
65
+ }
@@ -0,0 +1,23 @@
1
+ import type { AccessRule } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare const DOCUMENTS_FEATURE_NAME = "documents";
3
+ export declare const DOCUMENT_ENTITY = "filed-document";
4
+ export declare const DOCUMENT_LINK_ENTITY = "filed-document-link";
5
+ export declare const DEFAULT_DOCUMENT_ACCESS: AccessRule;
6
+ export declare const DEFAULT_ADMIN_ROLES: readonly string[];
7
+ export declare const DEFAULT_EXPIRING_WITHIN_DAYS = 30;
8
+ export declare const DOCUMENTS_LIST_SCREEN_ID = "list";
9
+ export declare const DocumentsHandlers: {
10
+ readonly link: "documents:write:link";
11
+ readonly unlink: "documents:write:unlink";
12
+ readonly import: "documents:write:import";
13
+ };
14
+ export declare const DocumentsQueries: {
15
+ readonly list: "documents:query:filed-document:list";
16
+ readonly detail: "documents:query:filed-document:detail";
17
+ readonly forRecord: "documents:query:for-record";
18
+ readonly expiryOverview: "documents:query:expiry-overview";
19
+ readonly overview: "documents:query:overview";
20
+ };
21
+ export declare const EXPIRY_STATES: readonly ["expired", "expiring", "valid", "none"];
22
+ export type ExpiryState = (typeof EXPIRY_STATES)[number];
23
+ export declare const EXPIRING_FACET_VALUE = "expiring";
@@ -0,0 +1,21 @@
1
+ export const DOCUMENTS_FEATURE_NAME = "documents";
2
+ export const DOCUMENT_ENTITY = "filed-document";
3
+ export const DOCUMENT_LINK_ENTITY = "filed-document-link";
4
+ export const DEFAULT_DOCUMENT_ACCESS = { roles: ["TenantAdmin", "TenantMember"] };
5
+ export const DEFAULT_ADMIN_ROLES = ["TenantAdmin", "SystemAdmin"];
6
+ export const DEFAULT_EXPIRING_WITHIN_DAYS = 30;
7
+ export const DOCUMENTS_LIST_SCREEN_ID = "list";
8
+ export const DocumentsHandlers = {
9
+ link: "documents:write:link",
10
+ unlink: "documents:write:unlink",
11
+ import: "documents:write:import",
12
+ };
13
+ export const DocumentsQueries = {
14
+ list: "documents:query:filed-document:list",
15
+ detail: "documents:query:filed-document:detail",
16
+ forRecord: "documents:query:for-record",
17
+ expiryOverview: "documents:query:expiry-overview",
18
+ overview: "documents:query:overview",
19
+ };
20
+ export const EXPIRY_STATES = ["expired", "expiring", "valid", "none"];
21
+ export const EXPIRING_FACET_VALUE = "expiring";
@@ -0,0 +1,17 @@
1
+ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
3
+ import type { DocumentExecutors } from "./executor.ts";
4
+ export type LinkTarget = {
5
+ readonly entityType: string;
6
+ readonly entityId: string;
7
+ };
8
+ export declare function ensureDocumentLink(executors: DocumentExecutors, user: SessionUser, db: TenantDb, documentId: string, target: LinkTarget): Promise<import("@cosmicdrift/kumiko-types/write-error-info-types").WriteFailure | {
9
+ readonly isSuccess: true;
10
+ readonly data: import("@cosmicdrift/kumiko-types/hooks").SaveContext;
11
+ readonly lifecycle?: import("@cosmicdrift/kumiko-types/hooks").LifecycleResult;
12
+ } | {
13
+ isSuccess: true;
14
+ data: {
15
+ id: string;
16
+ };
17
+ }>;
@@ -0,0 +1,20 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { documentLinkAggregateId } from "./aggregate-id.js";
3
+ // The link id is deterministic, so a removed link is restored rather than recreated and a
4
+ // concurrent first create converges on the row the winner wrote.
5
+ export async function ensureDocumentLink(executors, user, db, documentId, target) {
6
+ const id = documentLinkAggregateId(user.tenantId, documentId, target.entityType, target.entityId);
7
+ const restored = await executors.link.restore({ id }, user, db);
8
+ if (restored.isSuccess)
9
+ return restored;
10
+ const alreadyLive = isPlainObject(restored.error.details) && restored.error.details["reason"] === "not_deleted";
11
+ if (alreadyLive)
12
+ return { isSuccess: true, data: { id } };
13
+ if (restored.error.code !== "not_found")
14
+ return restored;
15
+ const created = await executors.link.create({ id, documentId, entityType: target.entityType, entityId: target.entityId }, user, db);
16
+ if (created.isSuccess || created.error.code === "version_conflict") {
17
+ return { isSuccess: true, data: { id } };
18
+ }
19
+ return created;
20
+ }
@@ -0,0 +1,76 @@
1
+ import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ export type DocumentEntityOptions = {
3
+ readonly categories: readonly [string, ...string[]];
4
+ readonly adminRoles: readonly string[];
5
+ readonly readAccess?: NonNullable<EntityDefinition["access"]>["read"];
6
+ readonly retention?: EntityDefinition["retention"];
7
+ };
8
+ export declare function createDocumentEntity(options: DocumentEntityOptions): EntityDefinition<{
9
+ readonly title: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
10
+ required: true;
11
+ };
12
+ readonly category: {
13
+ readonly type: "select";
14
+ readonly description?: string;
15
+ readonly options: readonly [string, ...string[]];
16
+ readonly required?: boolean;
17
+ readonly sortable?: boolean;
18
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
19
+ readonly filterable?: boolean;
20
+ readonly sensitive?: boolean;
21
+ readonly default?: string | undefined;
22
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
23
+ readonly display?: "radio" | "dropdown";
24
+ readonly optionTones?: {
25
+ readonly [x: string]: "accent" | "bad" | "critical" | "neutral" | "ok" | "warn" | undefined;
26
+ } | undefined;
27
+ readonly optionsQuery?: string;
28
+ readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
29
+ readonly optionsAvailabilityQuery?: string;
30
+ readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<string>[] | undefined;
31
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
32
+ required: true;
33
+ };
34
+ readonly issuedAt: {
35
+ readonly type: "date";
36
+ readonly description?: string;
37
+ readonly required?: boolean;
38
+ readonly sortable?: boolean;
39
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
40
+ readonly filterable?: boolean;
41
+ readonly sensitive?: boolean;
42
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
43
+ readonly min?: string;
44
+ readonly max?: string;
45
+ readonly locale?: string;
46
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
47
+ required: false;
48
+ };
49
+ readonly validUntil: {
50
+ readonly type: "date";
51
+ readonly description?: string;
52
+ readonly required?: boolean;
53
+ readonly sortable?: boolean;
54
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
55
+ readonly filterable?: boolean;
56
+ readonly sensitive?: boolean;
57
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
58
+ readonly min?: string;
59
+ readonly max?: string;
60
+ readonly locale?: string;
61
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
62
+ required: false;
63
+ };
64
+ readonly file: import("@cosmicdrift/kumiko-types/fields").FileFieldDef;
65
+ }, "tenant">;
66
+ export declare function createDocumentLinkEntity(linkTargets: readonly string[]): EntityDefinition<{
67
+ readonly documentId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
68
+ required: true;
69
+ };
70
+ readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
71
+ required: true;
72
+ };
73
+ readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
74
+ required: true;
75
+ };
76
+ }, "tenant">;
@@ -0,0 +1,84 @@
1
+ import { sql } from "@cosmicdrift/kumiko-framework/db";
2
+ import { createDateField, createEntity, createFileField, createSelectField, createTextField, SYSTEM_ROLE, } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { DOCUMENT_LINK_ENTITY } from "./constants.js";
4
+ // Readable only through a link to a host the viewer can read, and only when the file's owner is
5
+ // readable too (see EntityDefinition.readGate); readAccess narrows further.
6
+ export function createDocumentEntity(options) {
7
+ return createEntity({
8
+ table: "read_filed_documents",
9
+ softDelete: true,
10
+ description: "A filed document: title, category, issue and expiry dates and one file. It is visible only through its links to other records and the owner of its file; the file may stay owned by the record it came from.",
11
+ // The system user passes the gate; the capture and import checks run as it and would
12
+ // otherwise miss a document that readAccess hides.
13
+ ...(options.readAccess !== undefined && {
14
+ access: { read: { ...options.readAccess, [SYSTEM_ROLE]: "all" } },
15
+ }),
16
+ ...(options.retention !== undefined && { retention: options.retention }),
17
+ readGate: {
18
+ anyLink: { entity: DOCUMENT_LINK_ENTITY, field: "documentId" },
19
+ fileOwner: { field: "file" },
20
+ linkExemptRoles: options.adminRoles,
21
+ },
22
+ fields: {
23
+ title: createTextField({
24
+ required: true,
25
+ maxLength: 200,
26
+ sortable: true,
27
+ searchable: true,
28
+ personal: false,
29
+ reason: "document_title",
30
+ }),
31
+ category: createSelectField({
32
+ options: options.categories,
33
+ required: true,
34
+ filterable: true,
35
+ sortable: true,
36
+ }),
37
+ issuedAt: createDateField({ sortable: true }),
38
+ validUntil: createDateField({ sortable: true, filterable: true }),
39
+ // Filed business documents belong to the tenant, not to one person.
40
+ file: createFileField({ required: true, personal: false, reason: "is_business_data" }),
41
+ },
42
+ // One live document per file; a deleted one releases the file.
43
+ indexes: [
44
+ {
45
+ columns: ["tenantId", "file"],
46
+ unique: true,
47
+ name: "read_filed_documents_file_unique",
48
+ where: sql `"is_deleted" = false`,
49
+ },
50
+ ],
51
+ });
52
+ }
53
+ export function createDocumentLinkEntity(linkTargets) {
54
+ return createEntity({
55
+ table: "read_filed_document_links",
56
+ softDelete: true,
57
+ description: "The join row recording that one document belongs to one record, addressed by documentId, entityType and entityId, with exactly one row per triple. A document is visible to whoever can read at least one of its linked records.",
58
+ parentRef: {
59
+ entityTypeField: "entityType",
60
+ entityIdField: "entityId",
61
+ allowedTypes: linkTargets,
62
+ },
63
+ fields: {
64
+ documentId: createTextField({
65
+ required: true,
66
+ maxLength: 64,
67
+ personal: false,
68
+ reason: "technical_reference",
69
+ }),
70
+ entityType: createTextField({
71
+ required: true,
72
+ maxLength: 64,
73
+ personal: false,
74
+ reason: "technical_reference",
75
+ }),
76
+ entityId: createTextField({
77
+ required: true,
78
+ maxLength: 128,
79
+ personal: false,
80
+ reason: "technical_reference",
81
+ }),
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,6 @@
1
+ import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ export declare function createDocumentExecutors(documentEntity: EntityDefinition, linkEntity: EntityDefinition): {
3
+ document: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<Record<string, unknown>>;
4
+ link: import("@cosmicdrift/kumiko-types/event-store-executor-types").EventStoreExecutor<Record<string, unknown>>;
5
+ };
6
+ export type DocumentExecutors = ReturnType<typeof createDocumentExecutors>;
@@ -0,0 +1,8 @@
1
+ import { createEntityExecutor } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { DOCUMENT_ENTITY, DOCUMENT_LINK_ENTITY } from "./constants.js";
3
+ export function createDocumentExecutors(documentEntity, linkEntity) {
4
+ return {
5
+ document: createEntityExecutor(DOCUMENT_ENTITY, documentEntity).executor,
6
+ link: createEntityExecutor(DOCUMENT_LINK_ENTITY, linkEntity).executor,
7
+ };
8
+ }
@@ -0,0 +1,9 @@
1
+ import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
+ import type { ExpiryState } from "./constants.ts";
3
+ export type ExpiryWindow = {
4
+ readonly today: string;
5
+ /** Last day (inclusive) that still counts as "expiring". */
6
+ readonly horizon: string;
7
+ };
8
+ export declare function expiryWindow(ctx: Pick<HandlerContext, "tz">, expiringWithinDays: number): ExpiryWindow;
9
+ export declare function expiryStateOf(validUntil: unknown, window: ExpiryWindow): ExpiryState;
@@ -0,0 +1,20 @@
1
+ // One definition of the boundaries for the counts and for the per-row state, so the
2
+ // overview column and the expiry-overview buckets cannot drift apart.
3
+ export function expiryWindow(ctx, expiringWithinDays) {
4
+ const today = ctx.tz.today(ctx.tz.tenant);
5
+ return { today: today.toString(), horizon: today.add({ days: expiringWithinDays }).toString() };
6
+ }
7
+ // In-process dispatch hands back the date as a PlainDate, the wire form is an ISO string.
8
+ // ISO dates compare correctly as strings; a missing date is "none", not "valid".
9
+ export function expiryStateOf(validUntil, window) {
10
+ const day = validUntil instanceof Temporal.PlainDate
11
+ ? validUntil.toString()
12
+ : typeof validUntil === "string"
13
+ ? validUntil.slice(0, 10)
14
+ : "";
15
+ if (day === "")
16
+ return "none";
17
+ if (day < window.today)
18
+ return "expired";
19
+ return day <= window.horizon ? "expiring" : "valid";
20
+ }
@@ -0,0 +1,33 @@
1
+ import { type AccessRule, type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { type DocumentCaptureRule } from "./capture.ts";
3
+ export type DocumentsFeatureOptions = {
4
+ /** Allowed document categories; they become the options of the category field. */
5
+ readonly categories: readonly [string, ...string[]];
6
+ /** Entities a document can be linked to. Each needs an update handler, which linking requires. */
7
+ readonly linkTargets: readonly [string, ...string[]];
8
+ /** Files saved into these fields become documents automatically. */
9
+ readonly capture?: readonly DocumentCaptureRule[];
10
+ /** Retention of the document rows, in the entity's own retention vocabulary. */
11
+ readonly retention?: EntityDefinition["retention"];
12
+ /** Row-level read rule on the document, AND-ed with the link and file-owner gate. */
13
+ readonly readAccess?: NonNullable<EntityDefinition["access"]>["read"];
14
+ /**
15
+ * Who may use the document handlers. One rule applies to read, write and delete; the split form
16
+ * sets them apart. Default TenantAdmin and TenantMember; a missing delete follows write.
17
+ */
18
+ readonly access?: AccessRule | DocumentsAccess;
19
+ /** Roles that see every document of their tenant whose file owner they can read, without a link. */
20
+ readonly adminRoles?: readonly string[];
21
+ /** Window of the "expiring" bucket in expiry-overview. Default 30 days. */
22
+ readonly expiringWithinDays?: number;
23
+ };
24
+ export type DocumentsAccess = {
25
+ /** Lists, detail, for-record, the overviews and the list screen. */
26
+ readonly read?: AccessRule;
27
+ /** Create, update, link and unlink. */
28
+ readonly write?: AccessRule;
29
+ readonly delete?: AccessRule;
30
+ };
31
+ export declare function createDocumentsFeature(opts: DocumentsFeatureOptions): import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
32
+ readonly exports: void;
33
+ };