@cosmicdrift/kumiko-bundled-features 0.399.0 → 0.401.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 +78 -0
- package/dist/documents/entity.js +87 -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 +46 -0
- package/dist/documents/feature.js +122 -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 +63 -0
- package/dist/documents/i18n.d.ts +6 -0
- package/dist/documents/i18n.js +47 -0
- package/dist/documents/index.d.ts +8 -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 +32 -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/screens/branding-screen.js +1 -1
- 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/user-data-rights/web/i18n.js +4 -0
- package/dist/waitlist/entity.d.ts +8 -8
- package/package.json +19 -11
|
@@ -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 { DOCUMENT_ENTITY, DOCUMENTS_FEATURE_NAME, 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_FEATURE_NAME}:${DOCUMENT_ENTITY}`,
|
|
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>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createEntityExecutor, hasAccess, toKebab, userCanWriteFieldRow, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { parentRowIsVisible } from "../shared/index.js";
|
|
3
|
+
function updateHandlerAccessOf(registry, entityType) {
|
|
4
|
+
const suffix = `:write:${toKebab(entityType)}:update`;
|
|
5
|
+
for (const [qn, handler] of registry.getAllWriteHandlers()) {
|
|
6
|
+
if (qn.endsWith(suffix) && registry.getHandlerEntity(qn) === entityType)
|
|
7
|
+
return handler.access;
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
// Linking a document changes what readers of the target see, so it needs what saving the target
|
|
12
|
+
// needs: read visibility, a write gate and its row-level write rule. The gate is the entity's
|
|
13
|
+
// update handler; an entity saved only through custom handlers has none, so its declared write
|
|
14
|
+
// roles stand in, and without either the target stays unlinkable. A target that is not visible
|
|
15
|
+
// answers like a missing one.
|
|
16
|
+
export async function canWriteLinkTarget(registry, entityType, entityId, user, db) {
|
|
17
|
+
if (!(await parentRowIsVisible(registry, entityType, entityId, user, db)))
|
|
18
|
+
return false;
|
|
19
|
+
const entity = registry.getEntity(entityType);
|
|
20
|
+
if (entity === undefined)
|
|
21
|
+
return false;
|
|
22
|
+
const updateAccess = updateHandlerAccessOf(registry, entityType);
|
|
23
|
+
if (updateAccess !== undefined) {
|
|
24
|
+
if (!hasAccess(user, updateAccess))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
else if (!hasAccess(user, { roles: Object.keys(entity.access?.write ?? {}) })) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const row = await createEntityExecutor(entityType, entity).executor.detail({ id: entityId }, user, db);
|
|
31
|
+
return row !== null && userCanWriteFieldRow(user, entity.access?.write, row, row);
|
|
32
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type DeleteContext, type HandlerContext, type PreSaveHookFn } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { DocumentExecutors } from "./executor.ts";
|
|
3
|
+
export declare function createDocumentWriteGuard(executors: DocumentExecutors, adminRoles: readonly string[]): {
|
|
4
|
+
beforeUpdate: PreSaveHookFn;
|
|
5
|
+
beforeDelete: (payload: DeleteContext, ctx: HandlerContext) => Promise<void>;
|
|
6
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { SYSTEM_ROLE, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { DOCUMENT_ENTITY } from "./constants.js";
|
|
4
|
+
import { canWriteLinkTarget } from "./target-write-check.js";
|
|
5
|
+
const LINK_PAGE_SIZE = 200;
|
|
6
|
+
// Editing or deleting a document changes what every reader of its hosts sees, so it needs what
|
|
7
|
+
// unlink needs: write access to at least one host it is linked to. Admin roles see unlinked
|
|
8
|
+
// documents too and must be able to clean them up, and SYSTEM jobs (import, capture) write on
|
|
9
|
+
// their own authority. A refused caller gets "not found", like a document they cannot read.
|
|
10
|
+
export function createDocumentWriteGuard(executors, adminRoles) {
|
|
11
|
+
async function assertMayWrite(documentId, ctx) {
|
|
12
|
+
const { user } = ctx;
|
|
13
|
+
if (user.roles.includes(SYSTEM_ROLE) || user.roles.some((role) => adminRoles.includes(role))) {
|
|
14
|
+
// skip: exempt roles write on their own authority
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (;;) {
|
|
19
|
+
const page = await executors.link.list({
|
|
20
|
+
filters: [{ field: "documentId", op: "eq", value: documentId }],
|
|
21
|
+
limit: LINK_PAGE_SIZE,
|
|
22
|
+
offset,
|
|
23
|
+
}, user, ctx.db);
|
|
24
|
+
for (const row of page.rows) {
|
|
25
|
+
const entityType = String(row["entityType"]);
|
|
26
|
+
const entityId = String(row["entityId"]);
|
|
27
|
+
// skip: one writable host is enough
|
|
28
|
+
if (await canWriteLinkTarget(ctx.registry, entityType, entityId, user, ctx.db))
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (page.rows.length < LINK_PAGE_SIZE)
|
|
32
|
+
break;
|
|
33
|
+
offset += LINK_PAGE_SIZE;
|
|
34
|
+
}
|
|
35
|
+
throw new NotFoundError(DOCUMENT_ENTITY, documentId);
|
|
36
|
+
}
|
|
37
|
+
const beforeUpdate = async (changes, ctx) => {
|
|
38
|
+
if (!ctx.isNew)
|
|
39
|
+
await assertMayWrite(String(ctx.previous["id"]), ctx);
|
|
40
|
+
return changes;
|
|
41
|
+
};
|
|
42
|
+
const beforeDelete = async (payload, ctx) => {
|
|
43
|
+
await assertMayWrite(String(payload.id), ctx);
|
|
44
|
+
};
|
|
45
|
+
return { beforeUpdate, beforeDelete };
|
|
46
|
+
}
|
|
@@ -41,7 +41,7 @@ export declare const mailAccountEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
41
41
|
readonly min?: string;
|
|
42
42
|
readonly max?: string;
|
|
43
43
|
readonly locale?: string;
|
|
44
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
44
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
45
45
|
required: true;
|
|
46
46
|
};
|
|
47
47
|
}, "tenant">;
|
|
@@ -89,7 +89,7 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
|
|
|
89
89
|
readonly min?: string;
|
|
90
90
|
readonly max?: string;
|
|
91
91
|
readonly locale?: string;
|
|
92
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
92
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
93
93
|
required: true;
|
|
94
94
|
};
|
|
95
95
|
readonly bodyRef: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
@@ -101,7 +101,17 @@ export declare const inboundMessageEntity: import("@cosmicdrift/kumiko-types/fie
|
|
|
101
101
|
readonly bodyText: import("@cosmicdrift/kumiko-types/fields").EntityLongTextFieldDef & {
|
|
102
102
|
required: false;
|
|
103
103
|
};
|
|
104
|
-
readonly bodyTextTruncated:
|
|
104
|
+
readonly bodyTextTruncated: {
|
|
105
|
+
readonly type: "boolean";
|
|
106
|
+
readonly description?: string;
|
|
107
|
+
readonly required?: boolean;
|
|
108
|
+
readonly sortable?: boolean;
|
|
109
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
110
|
+
readonly filterable?: boolean;
|
|
111
|
+
readonly sensitive?: boolean;
|
|
112
|
+
readonly default?: boolean;
|
|
113
|
+
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
114
|
+
} & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
105
115
|
required: false;
|
|
106
116
|
default: boolean;
|
|
107
117
|
};
|
|
@@ -141,7 +151,7 @@ export declare const mailThreadEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
141
151
|
readonly min?: string;
|
|
142
152
|
readonly max?: string;
|
|
143
153
|
readonly locale?: string;
|
|
144
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
154
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
145
155
|
required: true;
|
|
146
156
|
};
|
|
147
157
|
readonly messageCount: {
|
|
@@ -161,7 +171,7 @@ export declare const mailThreadEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
161
171
|
readonly field: string;
|
|
162
172
|
};
|
|
163
173
|
readonly grouping?: boolean;
|
|
164
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
174
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
165
175
|
required: true;
|
|
166
176
|
};
|
|
167
177
|
}, "tenant">;
|
|
@@ -195,7 +205,7 @@ export declare const syncCursorEntity: import("@cosmicdrift/kumiko-types/fields"
|
|
|
195
205
|
readonly min?: string;
|
|
196
206
|
readonly max?: string;
|
|
197
207
|
readonly locale?: string;
|
|
198
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
208
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
199
209
|
required: true;
|
|
200
210
|
};
|
|
201
211
|
}, "tenant">;
|
|
@@ -223,7 +233,7 @@ export declare const seenMessageEntity: import("@cosmicdrift/kumiko-types/fields
|
|
|
223
233
|
readonly min?: string;
|
|
224
234
|
readonly max?: string;
|
|
225
235
|
readonly locale?: string;
|
|
226
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
236
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
227
237
|
required: true;
|
|
228
238
|
};
|
|
229
239
|
}, "tenant">;
|
|
@@ -25,7 +25,7 @@ export declare const ingestFailureEntity: import("@cosmicdrift/kumiko-types/fiel
|
|
|
25
25
|
readonly field: string;
|
|
26
26
|
};
|
|
27
27
|
readonly grouping?: boolean;
|
|
28
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
28
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
29
29
|
required: true;
|
|
30
30
|
};
|
|
31
31
|
readonly lastCountedBucket: {
|
|
@@ -45,7 +45,7 @@ export declare const ingestFailureEntity: import("@cosmicdrift/kumiko-types/fiel
|
|
|
45
45
|
readonly field: string;
|
|
46
46
|
};
|
|
47
47
|
readonly grouping?: boolean;
|
|
48
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
48
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
49
49
|
required: true;
|
|
50
50
|
};
|
|
51
51
|
readonly lastFailedAt: {
|
|
@@ -61,7 +61,7 @@ export declare const ingestFailureEntity: import("@cosmicdrift/kumiko-types/fiel
|
|
|
61
61
|
readonly min?: string;
|
|
62
62
|
readonly max?: string;
|
|
63
63
|
readonly locale?: string;
|
|
64
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
64
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
65
65
|
required: true;
|
|
66
66
|
};
|
|
67
67
|
}, "tenant">;
|
|
@@ -39,7 +39,7 @@ export declare const mailAccountsProjectionTable: import("@cosmicdrift/kumiko-fr
|
|
|
39
39
|
readonly min?: string;
|
|
40
40
|
readonly max?: string;
|
|
41
41
|
readonly locale?: string;
|
|
42
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
42
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
43
43
|
required: true;
|
|
44
44
|
};
|
|
45
45
|
}, "tenant">>;
|
|
@@ -84,7 +84,7 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
|
|
|
84
84
|
readonly min?: string;
|
|
85
85
|
readonly max?: string;
|
|
86
86
|
readonly locale?: string;
|
|
87
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
87
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
88
88
|
required: true;
|
|
89
89
|
};
|
|
90
90
|
readonly bodyRef: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
|
|
@@ -96,7 +96,17 @@ export declare const inboundMessagesProjectionTable: import("@cosmicdrift/kumiko
|
|
|
96
96
|
readonly bodyText: import("@cosmicdrift/kumiko-types/fields").EntityLongTextFieldDef & {
|
|
97
97
|
required: false;
|
|
98
98
|
};
|
|
99
|
-
readonly bodyTextTruncated:
|
|
99
|
+
readonly bodyTextTruncated: {
|
|
100
|
+
readonly type: "boolean";
|
|
101
|
+
readonly description?: string;
|
|
102
|
+
readonly required?: boolean;
|
|
103
|
+
readonly sortable?: boolean;
|
|
104
|
+
readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
|
|
105
|
+
readonly filterable?: boolean;
|
|
106
|
+
readonly sensitive?: boolean;
|
|
107
|
+
readonly default?: boolean;
|
|
108
|
+
readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
|
|
109
|
+
} & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
100
110
|
required: false;
|
|
101
111
|
default: boolean;
|
|
102
112
|
};
|
|
@@ -134,7 +144,7 @@ export declare const mailThreadsProjectionTable: import("@cosmicdrift/kumiko-fra
|
|
|
134
144
|
readonly min?: string;
|
|
135
145
|
readonly max?: string;
|
|
136
146
|
readonly locale?: string;
|
|
137
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
147
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
138
148
|
required: true;
|
|
139
149
|
};
|
|
140
150
|
readonly messageCount: {
|
|
@@ -154,7 +164,7 @@ export declare const mailThreadsProjectionTable: import("@cosmicdrift/kumiko-fra
|
|
|
154
164
|
readonly field: string;
|
|
155
165
|
};
|
|
156
166
|
readonly grouping?: boolean;
|
|
157
|
-
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
|
|
167
|
+
} & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
|
|
158
168
|
required: true;
|
|
159
169
|
};
|
|
160
170
|
}, "tenant">>;
|