@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.
- package/dist/auth-mfa/schema/user-mfa.d.ts +4 -4
- package/dist/billing-foundation/entities.d.ts +2 -2
- package/dist/billing-foundation/projection.d.ts +2 -2
- package/dist/cap-counter/entity.d.ts +3 -3
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
- package/dist/custom-fields/entity.d.ts +23 -3
- package/dist/delivery/tables.d.ts +11 -1
- package/dist/documents/aggregate-id.d.ts +2 -0
- package/dist/documents/aggregate-id.js +12 -0
- package/dist/documents/capture.d.ts +14 -0
- package/dist/documents/capture.js +65 -0
- package/dist/documents/constants.d.ts +23 -0
- package/dist/documents/constants.js +21 -0
- package/dist/documents/ensure-link.d.ts +17 -0
- package/dist/documents/ensure-link.js +20 -0
- package/dist/documents/entity.d.ts +76 -0
- package/dist/documents/entity.js +84 -0
- package/dist/documents/executor.d.ts +6 -0
- package/dist/documents/executor.js +8 -0
- package/dist/documents/expiry.d.ts +9 -0
- package/dist/documents/expiry.js +20 -0
- package/dist/documents/feature.d.ts +33 -0
- package/dist/documents/feature.js +111 -0
- package/dist/documents/handlers/import.write.d.ts +23 -0
- package/dist/documents/handlers/import.write.js +104 -0
- package/dist/documents/handlers/link.write.d.ts +18 -0
- package/dist/documents/handlers/link.write.js +61 -0
- package/dist/documents/i18n.d.ts +6 -0
- package/dist/documents/i18n.js +33 -0
- package/dist/documents/index.d.ts +7 -0
- package/dist/documents/index.js +6 -0
- package/dist/documents/pending-file-guard.d.ts +2 -0
- package/dist/documents/pending-file-guard.js +29 -0
- package/dist/documents/queries/expiry-overview.query.d.ts +10 -0
- package/dist/documents/queries/expiry-overview.query.js +41 -0
- package/dist/documents/queries/for-record.query.d.ts +90 -0
- package/dist/documents/queries/for-record.query.js +54 -0
- package/dist/documents/queries/list-page.d.ts +19 -0
- package/dist/documents/queries/list-page.js +36 -0
- package/dist/documents/queries/overview.query.d.ts +32 -0
- package/dist/documents/queries/overview.query.js +41 -0
- package/dist/documents/schemas.d.ts +93 -0
- package/dist/documents/schemas.js +51 -0
- package/dist/documents/screens.d.ts +3 -0
- package/dist/documents/screens.js +76 -0
- package/dist/documents/tab.d.ts +4 -0
- package/dist/documents/tab.js +42 -0
- package/dist/documents/target-write-check.d.ts +3 -0
- package/dist/documents/target-write-check.js +25 -0
- package/dist/documents/write-guard.d.ts +6 -0
- package/dist/documents/write-guard.js +46 -0
- package/dist/inbound-mail-foundation/entities.d.ts +17 -7
- package/dist/inbound-mail-foundation/ingest-failures.d.ts +3 -3
- package/dist/inbound-mail-foundation/projection.d.ts +15 -5
- package/dist/inbound-mail-foundation/quarantine-entities.d.ts +3 -3
- package/dist/jobs/job-run-table.d.ts +4 -4
- package/dist/ledger/entity.d.ts +8 -8
- package/dist/managed-pages/table.d.ts +24 -4
- package/dist/personal-access-tokens/schema/api-token.d.ts +3 -3
- package/dist/readiness/schema/probe-run.d.ts +60 -20
- package/dist/secrets/table.d.ts +1 -1
- package/dist/sessions/schema/user-session.d.ts +4 -4
- package/dist/shared/purge-row-file-refs.js +5 -4
- package/dist/template-resolver/table.d.ts +12 -12
- package/dist/template-resolver/user-content-table.d.ts +4 -4
- package/dist/tenant/invitation-table.d.ts +6 -6
- package/dist/tenant/schema/tenant.d.ts +32 -12
- package/dist/tier-engine/entity.d.ts +1 -1
- package/dist/tier-engine/tier-resolver.d.ts +1 -1
- package/dist/user/schema/user.d.ts +28 -8
- package/dist/user-data-rights/schema/download-attempt.d.ts +2 -2
- package/dist/user-data-rights/schema/download-token.d.ts +8 -8
- package/dist/user-data-rights/schema/export-job.d.ts +12 -12
- package/dist/waitlist/entity.d.ts +8 -8
- package/package.json +19 -11
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// documents — filed documents (title, category, validity, one file) that become visible on other
|
|
2
|
+
// records through link rows. A document is readable only through a record the viewer can read,
|
|
3
|
+
// and only while the owner of its file is readable too (EntityDefinition.readGate). Documents
|
|
4
|
+
// come from capture rules (a file saved into a configured field) or from the system-only import.
|
|
5
|
+
import { defineEntityCreateHandler, defineEntityDeleteHandler, defineEntityDetailHandler, defineEntityListHandler, defineEntityUpdateHandler, defineFeature, } from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { registerCapture, validateCaptureRules } from "./capture.js";
|
|
7
|
+
import { DEFAULT_ADMIN_ROLES, DEFAULT_DOCUMENT_ACCESS, DEFAULT_EXPIRING_WITHIN_DAYS, DOCUMENT_ENTITY, DOCUMENT_LINK_ENTITY, DOCUMENTS_FEATURE_NAME, } from "./constants.js";
|
|
8
|
+
import { createDocumentEntity, createDocumentLinkEntity } from "./entity.js";
|
|
9
|
+
import { createDocumentExecutors } from "./executor.js";
|
|
10
|
+
import { createImportDocumentHandler } from "./handlers/import.write.js";
|
|
11
|
+
import { createLinkDocumentHandler, createUnlinkDocumentHandler } from "./handlers/link.write.js";
|
|
12
|
+
import { DOCUMENTS_FEATURE_I18N } from "./i18n.js";
|
|
13
|
+
import { rejectForeignPendingFile } from "./pending-file-guard.js";
|
|
14
|
+
import { createExpiryOverviewQuery } from "./queries/expiry-overview.query.js";
|
|
15
|
+
import { createForRecordQuery, createForTypeQuery } from "./queries/for-record.query.js";
|
|
16
|
+
import { createOverviewQuery } from "./queries/overview.query.js";
|
|
17
|
+
import { createDocumentListScreen } from "./screens.js";
|
|
18
|
+
import { createDocumentWriteGuard } from "./write-guard.js";
|
|
19
|
+
function isAccessRule(access) {
|
|
20
|
+
return "roles" in access || "openToAll" in access;
|
|
21
|
+
}
|
|
22
|
+
function resolveAccess(access) {
|
|
23
|
+
if (access === undefined) {
|
|
24
|
+
return {
|
|
25
|
+
read: DEFAULT_DOCUMENT_ACCESS,
|
|
26
|
+
write: DEFAULT_DOCUMENT_ACCESS,
|
|
27
|
+
delete: DEFAULT_DOCUMENT_ACCESS,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (isAccessRule(access))
|
|
31
|
+
return { read: access, write: access, delete: access };
|
|
32
|
+
const write = access.write ?? DEFAULT_DOCUMENT_ACCESS;
|
|
33
|
+
return { read: access.read ?? DEFAULT_DOCUMENT_ACCESS, write, delete: access.delete ?? write };
|
|
34
|
+
}
|
|
35
|
+
function categoryOptionLabels(categories) {
|
|
36
|
+
return Object.fromEntries(categories.map((category) => [
|
|
37
|
+
`documents:entity:filed-document:field:category:option:${category}`,
|
|
38
|
+
{ en: category, de: category },
|
|
39
|
+
]));
|
|
40
|
+
}
|
|
41
|
+
function registerDocuments(r, opts) {
|
|
42
|
+
const access = resolveAccess(opts.access);
|
|
43
|
+
const linkTargets = opts.linkTargets;
|
|
44
|
+
const documentEntity = createDocumentEntity({
|
|
45
|
+
categories: opts.categories,
|
|
46
|
+
adminRoles: opts.adminRoles ?? DEFAULT_ADMIN_ROLES,
|
|
47
|
+
...(opts.readAccess !== undefined && { readAccess: opts.readAccess }),
|
|
48
|
+
...(opts.retention !== undefined && { retention: opts.retention }),
|
|
49
|
+
});
|
|
50
|
+
const linkEntity = createDocumentLinkEntity(linkTargets);
|
|
51
|
+
const executors = createDocumentExecutors(documentEntity, linkEntity);
|
|
52
|
+
r.requires({ features: ["files"] });
|
|
53
|
+
r.adoptsBundledLists();
|
|
54
|
+
r.describe("Filed documents with title, category, issue and expiry date and one file, shown on the records they are linked to. A document is visible only through a linked record the viewer can read, and only while the record that owns its file is readable too, so linking never widens access to the file. Documents arrive through capture rules (a file saved into a configured field) or through the system-only import for migrations, which keeps the caller's id, refuses an id another record already uses, and stamps createdAt with the import time (issuedAt carries the business date; the event store does not backdate); users link, unlink, edit and delete them. Read a record's documents with for-record, count expired and expiring ones with expiry-overview, list them with an expiry state and an expiring filter with overview (the documents list screen), and add the documents tab to a record screen with documentsTab().");
|
|
55
|
+
r.uiHints({ displayLabel: "Documents", category: "data", recommended: false });
|
|
56
|
+
r.entity({ name: DOCUMENT_ENTITY, ...documentEntity });
|
|
57
|
+
r.entity({ name: DOCUMENT_LINK_ENTITY, ...linkEntity });
|
|
58
|
+
r.writeHandler(defineEntityCreateHandler(DOCUMENT_ENTITY, documentEntity, {
|
|
59
|
+
access: access.write,
|
|
60
|
+
description: "Files a new document from an uploaded file; it stays invisible to everyone but the admin roles until it is linked to a record.",
|
|
61
|
+
}));
|
|
62
|
+
const writeGuard = createDocumentWriteGuard(executors, opts.adminRoles ?? DEFAULT_ADMIN_ROLES);
|
|
63
|
+
r.writeHandler(defineEntityUpdateHandler(DOCUMENT_ENTITY, documentEntity, {
|
|
64
|
+
access: access.write,
|
|
65
|
+
description: "Edits a document's title, category and dates from the entity-edit envelope; it does not change what the document is linked to.",
|
|
66
|
+
}));
|
|
67
|
+
r.writeHandler(defineEntityDeleteHandler(DOCUMENT_ENTITY, documentEntity, {
|
|
68
|
+
access: access.delete,
|
|
69
|
+
description: "Deletes a document so it disappears from every record it was linked to; the file itself stays with its owner.",
|
|
70
|
+
agent: { risk: "high" },
|
|
71
|
+
}));
|
|
72
|
+
r.hook({ type: "preSave", target: "filed-document:create", handler: rejectForeignPendingFile });
|
|
73
|
+
r.hook({ type: "preSave", target: "filed-document:update", handler: rejectForeignPendingFile });
|
|
74
|
+
r.hook({ type: "preSave", target: "filed-document:update", handler: writeGuard.beforeUpdate });
|
|
75
|
+
r.hook({ type: "preDelete", target: "filed-document:delete", handler: writeGuard.beforeDelete });
|
|
76
|
+
r.writeHandler(createLinkDocumentHandler({ access: access.write, executors, linkTargets }));
|
|
77
|
+
r.writeHandler(createUnlinkDocumentHandler({ access: access.write, executors, linkTargets }));
|
|
78
|
+
r.writeHandler(createImportDocumentHandler({ executors, categories: opts.categories, linkTargets }));
|
|
79
|
+
r.queryHandler(defineEntityListHandler(DOCUMENT_ENTITY, documentEntity, {
|
|
80
|
+
access: access.read,
|
|
81
|
+
description: "Lists the documents the caller can read, filterable by category and validity date; use for-record to list one record's documents.",
|
|
82
|
+
}));
|
|
83
|
+
r.queryHandler(defineEntityDetailHandler(DOCUMENT_ENTITY, documentEntity, {
|
|
84
|
+
access: access.read,
|
|
85
|
+
description: "Reads one document the caller can read, with its file reference and dates.",
|
|
86
|
+
}));
|
|
87
|
+
r.queryHandler(defineEntityListHandler(DOCUMENT_LINK_ENTITY, linkEntity, {
|
|
88
|
+
access: access.read,
|
|
89
|
+
description: "Lists the document-to-record link rows whose record the caller can read; filter on entityId for one record's links.",
|
|
90
|
+
}));
|
|
91
|
+
r.queryHandler(createForRecordQuery(access.read, linkTargets));
|
|
92
|
+
for (const entityType of linkTargets) {
|
|
93
|
+
r.queryHandler(createForTypeQuery(access.read, linkTargets, entityType));
|
|
94
|
+
}
|
|
95
|
+
const expiringWithinDays = opts.expiringWithinDays ?? DEFAULT_EXPIRING_WITHIN_DAYS;
|
|
96
|
+
r.queryHandler(createExpiryOverviewQuery(access.read, expiringWithinDays));
|
|
97
|
+
r.queryHandler(createOverviewQuery(access.read, expiringWithinDays));
|
|
98
|
+
const capture = opts.capture ?? [];
|
|
99
|
+
registerCapture(r, executors, capture, linkTargets);
|
|
100
|
+
r.bootCheck((ctx) => validateCaptureRules(ctx, capture, opts.categories, linkTargets));
|
|
101
|
+
r.screen(createDocumentListScreen(access.read, opts.categories));
|
|
102
|
+
r.translations({
|
|
103
|
+
keys: { ...DOCUMENTS_FEATURE_I18N, ...categoryOptionLabels(opts.categories) },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function createDocumentsFeature(opts) {
|
|
107
|
+
if (opts.expiringWithinDays !== undefined && opts.expiringWithinDays < 0) {
|
|
108
|
+
throw new Error("createDocumentsFeature({ expiringWithinDays }) must not be negative.");
|
|
109
|
+
}
|
|
110
|
+
return defineFeature(DOCUMENTS_FEATURE_NAME, (r) => registerDocuments(r, opts));
|
|
111
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DocumentExecutors } from "../executor.ts";
|
|
2
|
+
type ImportArgs = {
|
|
3
|
+
readonly executors: DocumentExecutors;
|
|
4
|
+
readonly categories: readonly string[];
|
|
5
|
+
readonly linkTargets: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
export declare function createImportDocumentHandler(args: ImportArgs): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"import", import("zod").ZodObject<{
|
|
8
|
+
id: import("zod").ZodUUID;
|
|
9
|
+
title: import("zod").ZodString;
|
|
10
|
+
category: import("zod").ZodString;
|
|
11
|
+
issuedAt: import("zod").ZodOptional<import("zod").ZodISODate>;
|
|
12
|
+
validUntil: import("zod").ZodOptional<import("zod").ZodISODate>;
|
|
13
|
+
fileRefId: import("zod").ZodUUID;
|
|
14
|
+
links: import("zod").ZodArray<import("zod").ZodObject<{
|
|
15
|
+
entityType: import("zod").ZodString;
|
|
16
|
+
entityId: import("zod").ZodString;
|
|
17
|
+
}, import("zod/v4/core").$strip>>;
|
|
18
|
+
deleted: import("zod").ZodOptional<import("zod").ZodBoolean>;
|
|
19
|
+
}, import("zod/v4/core").$strip>, {
|
|
20
|
+
id: string;
|
|
21
|
+
entity: string;
|
|
22
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined, undefined>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { createEntityExecutor, defineWriteHandler, SYSTEM_ROLE, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { ConflictError, NotFoundError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { FILE_REF_ENTITY_NAME } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
+
import { DOCUMENT_ENTITY } from "../constants.js";
|
|
5
|
+
import { ensureDocumentLink } from "../ensure-link.js";
|
|
6
|
+
import { importDocumentPayloadSchema } from "../schemas.js";
|
|
7
|
+
async function fileRefExists(registry, fileRefId, user, db) {
|
|
8
|
+
const entity = registry.getEntity(FILE_REF_ENTITY_NAME);
|
|
9
|
+
if (entity === undefined)
|
|
10
|
+
return false;
|
|
11
|
+
const row = await createEntityExecutor(FILE_REF_ENTITY_NAME, entity).executor.detail({ id: fileRefId }, user, db);
|
|
12
|
+
return row !== null;
|
|
13
|
+
}
|
|
14
|
+
// The stream, not the projected row: a row-level readAccess can hide a document from the system
|
|
15
|
+
// user, and a deleted document still has its stream.
|
|
16
|
+
async function streamTypeOf(events, id) {
|
|
17
|
+
const [first] = await events.load(id, { atVersion: 1 });
|
|
18
|
+
return first?.aggregateType;
|
|
19
|
+
}
|
|
20
|
+
// undefined: the document exists now, created here or by an earlier run of the same import.
|
|
21
|
+
async function createUnlessImported(executors, events, payload, user, db) {
|
|
22
|
+
const existingType = await streamTypeOf(events, payload.id);
|
|
23
|
+
if (existingType === undefined) {
|
|
24
|
+
const created = await executors.document.create({
|
|
25
|
+
id: payload.id,
|
|
26
|
+
title: payload.title,
|
|
27
|
+
category: payload.category,
|
|
28
|
+
file: payload.fileRefId,
|
|
29
|
+
...(payload.issuedAt !== undefined && { issuedAt: payload.issuedAt }),
|
|
30
|
+
...(payload.validUntil !== undefined && { validUntil: payload.validUntil }),
|
|
31
|
+
}, user, db);
|
|
32
|
+
if (created.isSuccess)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (created.error.code !== "version_conflict")
|
|
35
|
+
return created;
|
|
36
|
+
}
|
|
37
|
+
// Stream ids are unique per tenant across aggregate types: an existing stream is the retry of
|
|
38
|
+
// this import only when it is a document, otherwise linking would point at nothing.
|
|
39
|
+
const streamType = existingType ?? (await streamTypeOf(events, payload.id));
|
|
40
|
+
if (streamType === DOCUMENT_ENTITY)
|
|
41
|
+
return undefined;
|
|
42
|
+
return writeFailure(new ConflictError({
|
|
43
|
+
message: `documents import: id ${payload.id} belongs to another record`,
|
|
44
|
+
i18nKey: "documents.errors.import-id-taken",
|
|
45
|
+
details: { id: payload.id },
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
// For one-off migrations run as a job: the caller picks the document id, so a re-run converges
|
|
49
|
+
// on the same document instead of duplicating it. Never callable by a user.
|
|
50
|
+
export function createImportDocumentHandler(args) {
|
|
51
|
+
const { executors, categories, linkTargets } = args;
|
|
52
|
+
return defineWriteHandler({
|
|
53
|
+
name: "import",
|
|
54
|
+
schema: importDocumentPayloadSchema,
|
|
55
|
+
access: { roles: [SYSTEM_ROLE] },
|
|
56
|
+
agent: { expose: false },
|
|
57
|
+
description: "Creates a document with a caller-chosen id from an existing file, links it to the given records and optionally leaves it deleted; a repeat call with the same id changes nothing, and an id already used by another record fails. The document's createdAt is the time of the import; issuedAt carries the business date. Reserved for the system user, meant for data migrations.",
|
|
58
|
+
handler: async (event, ctx) => {
|
|
59
|
+
const payload = event.payload;
|
|
60
|
+
if (!categories.includes(payload.category)) {
|
|
61
|
+
return writeFailure(new ValidationError({
|
|
62
|
+
fields: [
|
|
63
|
+
{
|
|
64
|
+
path: "category",
|
|
65
|
+
code: "unknown_category",
|
|
66
|
+
i18nKey: "documents.errors.unknown-category",
|
|
67
|
+
params: { category: payload.category },
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
const badTarget = payload.links.find((link) => !linkTargets.includes(link.entityType));
|
|
73
|
+
if (badTarget !== undefined) {
|
|
74
|
+
return writeFailure(new ValidationError({
|
|
75
|
+
fields: [
|
|
76
|
+
{
|
|
77
|
+
path: "links",
|
|
78
|
+
code: "unknown_link_target",
|
|
79
|
+
i18nKey: "documents.errors.unknown-link-target",
|
|
80
|
+
params: { entityType: badTarget.entityType },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
if (!(await fileRefExists(ctx.registry, payload.fileRefId, event.user, ctx.db))) {
|
|
86
|
+
return writeFailure(new NotFoundError(FILE_REF_ENTITY_NAME, payload.fileRefId));
|
|
87
|
+
}
|
|
88
|
+
const notImported = await createUnlessImported(executors, ctx.events, payload, event.user, ctx.db);
|
|
89
|
+
if (notImported !== undefined)
|
|
90
|
+
return notImported;
|
|
91
|
+
for (const link of payload.links) {
|
|
92
|
+
const linked = await ensureDocumentLink(executors, event.user, ctx.db, payload.id, link);
|
|
93
|
+
if (!linked.isSuccess)
|
|
94
|
+
return linked;
|
|
95
|
+
}
|
|
96
|
+
if (payload.deleted === true) {
|
|
97
|
+
const removed = await executors.document.delete({ id: payload.id }, event.user, ctx.db);
|
|
98
|
+
if (!removed.isSuccess && removed.error.code !== "not_found")
|
|
99
|
+
return removed;
|
|
100
|
+
}
|
|
101
|
+
return { isSuccess: true, data: { id: payload.id, entity: DOCUMENT_ENTITY } };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AccessRule } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { DocumentExecutors } from "../executor.ts";
|
|
3
|
+
type LinkHandlerArgs = {
|
|
4
|
+
readonly access: AccessRule;
|
|
5
|
+
readonly executors: DocumentExecutors;
|
|
6
|
+
readonly linkTargets: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function createLinkDocumentHandler(args: LinkHandlerArgs): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"link", import("zod").ZodObject<{
|
|
9
|
+
documentId: import("zod").ZodUUID;
|
|
10
|
+
entityType: import("zod").ZodString;
|
|
11
|
+
entityId: import("zod").ZodString;
|
|
12
|
+
}, import("zod/v4/core").$strip>, unknown, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined, undefined>;
|
|
13
|
+
export declare function createUnlinkDocumentHandler(args: LinkHandlerArgs): import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"unlink", import("zod").ZodObject<{
|
|
14
|
+
documentId: import("zod").ZodUUID;
|
|
15
|
+
entityType: import("zod").ZodString;
|
|
16
|
+
entityId: import("zod").ZodString;
|
|
17
|
+
}, import("zod/v4/core").$strip>, unknown, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined, undefined>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { defineWriteHandler, parentVisibilityFor, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { documentLinkAggregateId } from "../aggregate-id.js";
|
|
4
|
+
import { DOCUMENT_ENTITY } from "../constants.js";
|
|
5
|
+
import { ensureDocumentLink } from "../ensure-link.js";
|
|
6
|
+
import { documentLinkPayloadSchema } from "../schemas.js";
|
|
7
|
+
import { canWriteLinkTarget } from "../target-write-check.js";
|
|
8
|
+
// Unreadable document, foreign target type and unwritable target answer alike (not found), so
|
|
9
|
+
// the call is no probe for what exists.
|
|
10
|
+
export function createLinkDocumentHandler(args) {
|
|
11
|
+
const { access, executors, linkTargets } = args;
|
|
12
|
+
return defineWriteHandler({
|
|
13
|
+
name: "link",
|
|
14
|
+
schema: documentLinkPayloadSchema,
|
|
15
|
+
access,
|
|
16
|
+
description: "Makes a document visible on another record by linking it to that record; the caller must be able to read the document and edit the record.",
|
|
17
|
+
handler: async (event, ctx) => {
|
|
18
|
+
const payload = event.payload;
|
|
19
|
+
const notFound = () => writeFailure(new NotFoundError(DOCUMENT_ENTITY, payload.documentId));
|
|
20
|
+
if (!linkTargets.includes(payload.entityType))
|
|
21
|
+
return notFound();
|
|
22
|
+
const document = await executors.document.detail({ id: payload.documentId }, event.user, ctx.db, {
|
|
23
|
+
parentVisibility: parentVisibilityFor(ctx, event.user),
|
|
24
|
+
});
|
|
25
|
+
if (document === null)
|
|
26
|
+
return notFound();
|
|
27
|
+
if (!(await canWriteLinkTarget(ctx.registry, payload.entityType, payload.entityId, event.user, ctx.db))) {
|
|
28
|
+
return notFound();
|
|
29
|
+
}
|
|
30
|
+
return ensureDocumentLink(executors, event.user, ctx.db, payload.documentId, payload);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function createUnlinkDocumentHandler(args) {
|
|
35
|
+
const { access, executors, linkTargets } = args;
|
|
36
|
+
return defineWriteHandler({
|
|
37
|
+
name: "unlink",
|
|
38
|
+
schema: documentLinkPayloadSchema,
|
|
39
|
+
access,
|
|
40
|
+
description: "Removes a document from a record it was linked to, reporting success when no link existed; the document and its file stay.",
|
|
41
|
+
handler: async (event, ctx) => {
|
|
42
|
+
const payload = event.payload;
|
|
43
|
+
const notFound = () => writeFailure(new NotFoundError(DOCUMENT_ENTITY, payload.documentId));
|
|
44
|
+
if (!linkTargets.includes(payload.entityType))
|
|
45
|
+
return notFound();
|
|
46
|
+
const document = await executors.document.detail({ id: payload.documentId }, event.user, ctx.db, {
|
|
47
|
+
parentVisibility: parentVisibilityFor(ctx, event.user),
|
|
48
|
+
});
|
|
49
|
+
if (document === null)
|
|
50
|
+
return notFound();
|
|
51
|
+
if (!(await canWriteLinkTarget(ctx.registry, payload.entityType, payload.entityId, event.user, ctx.db))) {
|
|
52
|
+
return notFound();
|
|
53
|
+
}
|
|
54
|
+
const id = documentLinkAggregateId(event.user.tenantId, payload.documentId, payload.entityType, payload.entityId);
|
|
55
|
+
const existing = await executors.link.detail({ id }, event.user, ctx.db);
|
|
56
|
+
if (existing === null)
|
|
57
|
+
return { isSuccess: true, data: { id } };
|
|
58
|
+
return executors.link.delete({ id }, event.user, ctx.db);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const DOCUMENTS_FEATURE_I18N = {
|
|
2
|
+
"documents:feature.label": { en: "Documents", de: "Dokumente" },
|
|
3
|
+
"screen:list.title": { en: "Documents", de: "Dokumente" },
|
|
4
|
+
"documents:tab.title": { en: "Documents", de: "Dokumente" },
|
|
5
|
+
"documents:overview.expired": { en: "Expired documents", de: "Abgelaufene Dokumente" },
|
|
6
|
+
"documents:overview.expiring": { en: "Documents expiring soon", de: "Bald ablaufende Dokumente" },
|
|
7
|
+
"documents:entity:filed-document:field:title": { en: "Title", de: "Titel" },
|
|
8
|
+
"documents:entity:filed-document:field:category": { en: "Category", de: "Kategorie" },
|
|
9
|
+
"documents:entity:filed-document:field:issuedAt": { en: "Issued", de: "Ausgestellt" },
|
|
10
|
+
"documents:entity:filed-document:field:validUntil": { en: "Valid until", de: "Gültig bis" },
|
|
11
|
+
"documents:entity:filed-document:field:file": { en: "File", de: "Datei" },
|
|
12
|
+
"documents:list.facet.expiry": { en: "Expiry", de: "Ablauf" },
|
|
13
|
+
"documents:list.facet.expiring": {
|
|
14
|
+
en: "Expired or expiring soon",
|
|
15
|
+
de: "Abgelaufen oder bald ablaufend",
|
|
16
|
+
},
|
|
17
|
+
"documents:list.expiry.expired": { en: "Expired", de: "Abgelaufen" },
|
|
18
|
+
"documents:list.expiry.expiring": { en: "Expiring soon", de: "Läuft bald ab" },
|
|
19
|
+
"documents:list.expiry.valid": { en: "Valid", de: "Gültig" },
|
|
20
|
+
"documents:list.expiry.none": { en: "No expiry date", de: "Ohne Ablaufdatum" },
|
|
21
|
+
"documents.errors.unknown-category": {
|
|
22
|
+
en: "Unknown document category",
|
|
23
|
+
de: "Unbekannte Dokumentenkategorie",
|
|
24
|
+
},
|
|
25
|
+
"documents.errors.unknown-link-target": {
|
|
26
|
+
en: "This record type takes no documents",
|
|
27
|
+
de: "Diesem Datensatztyp lassen sich keine Dokumente zuordnen",
|
|
28
|
+
},
|
|
29
|
+
"documents.errors.import-id-taken": {
|
|
30
|
+
en: "This id already belongs to another record",
|
|
31
|
+
de: "Diese ID gehört bereits zu einem anderen Datensatz",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { documentIdForFile, documentLinkAggregateId } from "./aggregate-id.ts";
|
|
2
|
+
export type { DocumentCaptureRule } from "./capture.ts";
|
|
3
|
+
export { DEFAULT_ADMIN_ROLES, DEFAULT_DOCUMENT_ACCESS, DOCUMENT_ENTITY, DOCUMENT_LINK_ENTITY, DOCUMENTS_FEATURE_NAME, DOCUMENTS_LIST_SCREEN_ID, DocumentsHandlers, DocumentsQueries, EXPIRY_STATES, type ExpiryState, } from "./constants.ts";
|
|
4
|
+
export { createDocumentsFeature, type DocumentsAccess, type DocumentsFeatureOptions, } from "./feature.ts";
|
|
5
|
+
export { forTypeQueryQn } from "./queries/for-record.query.ts";
|
|
6
|
+
export { DOCUMENT_COLUMNS } from "./screens.ts";
|
|
7
|
+
export { documentExpiryPanels, documentsTab } from "./tab.ts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { documentIdForFile, documentLinkAggregateId } from "./aggregate-id.js";
|
|
2
|
+
export { DEFAULT_ADMIN_ROLES, DEFAULT_DOCUMENT_ACCESS, DOCUMENT_ENTITY, DOCUMENT_LINK_ENTITY, DOCUMENTS_FEATURE_NAME, DOCUMENTS_LIST_SCREEN_ID, DocumentsHandlers, DocumentsQueries, EXPIRY_STATES, } from "./constants.js";
|
|
3
|
+
export { createDocumentsFeature, } from "./feature.js";
|
|
4
|
+
export { forTypeQueryQn } from "./queries/for-record.query.js";
|
|
5
|
+
export { DOCUMENT_COLUMNS } from "./screens.js";
|
|
6
|
+
export { documentExpiryPanels, documentsTab } from "./tab.js";
|
|
@@ -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 {};
|