@cosmicdrift/kumiko-bundled-features 0.398.0 → 0.400.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/auth-mfa/schema/user-mfa.d.ts +4 -4
  2. package/dist/billing-foundation/consumer-protection/consent-text.js +4 -4
  3. package/dist/billing-foundation/entities.d.ts +2 -2
  4. package/dist/billing-foundation/projection.d.ts +2 -2
  5. package/dist/cap-counter/entity.d.ts +3 -3
  6. package/dist/channel-in-app/constants.js +4 -9
  7. package/dist/channel-in-app/feature.js +1 -1
  8. package/dist/channel-in-app/handlers/inbox.query.d.ts +4 -1
  9. package/dist/channel-in-app/handlers/inbox.query.js +23 -8
  10. package/dist/channel-in-app/handlers/unread-count.query.js +2 -9
  11. package/dist/channel-in-app/in-app-channel.js +23 -11
  12. package/dist/channel-in-app/in-app-target.d.ts +5 -0
  13. package/dist/channel-in-app/in-app-target.js +34 -0
  14. package/dist/channel-in-app/index.d.ts +1 -0
  15. package/dist/channel-in-app/index.js +1 -0
  16. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -2
  17. package/dist/custom-fields/entity.d.ts +23 -3
  18. package/dist/delivery/tables.d.ts +11 -1
  19. package/dist/documents/aggregate-id.d.ts +2 -0
  20. package/dist/documents/aggregate-id.js +12 -0
  21. package/dist/documents/capture.d.ts +14 -0
  22. package/dist/documents/capture.js +65 -0
  23. package/dist/documents/constants.d.ts +23 -0
  24. package/dist/documents/constants.js +21 -0
  25. package/dist/documents/ensure-link.d.ts +17 -0
  26. package/dist/documents/ensure-link.js +20 -0
  27. package/dist/documents/entity.d.ts +76 -0
  28. package/dist/documents/entity.js +84 -0
  29. package/dist/documents/executor.d.ts +6 -0
  30. package/dist/documents/executor.js +8 -0
  31. package/dist/documents/expiry.d.ts +9 -0
  32. package/dist/documents/expiry.js +20 -0
  33. package/dist/documents/feature.d.ts +33 -0
  34. package/dist/documents/feature.js +111 -0
  35. package/dist/documents/handlers/import.write.d.ts +23 -0
  36. package/dist/documents/handlers/import.write.js +104 -0
  37. package/dist/documents/handlers/link.write.d.ts +18 -0
  38. package/dist/documents/handlers/link.write.js +61 -0
  39. package/dist/documents/i18n.d.ts +6 -0
  40. package/dist/documents/i18n.js +33 -0
  41. package/dist/documents/index.d.ts +7 -0
  42. package/dist/documents/index.js +6 -0
  43. package/dist/documents/pending-file-guard.d.ts +2 -0
  44. package/dist/documents/pending-file-guard.js +29 -0
  45. package/dist/documents/queries/expiry-overview.query.d.ts +10 -0
  46. package/dist/documents/queries/expiry-overview.query.js +41 -0
  47. package/dist/documents/queries/for-record.query.d.ts +90 -0
  48. package/dist/documents/queries/for-record.query.js +54 -0
  49. package/dist/documents/queries/list-page.d.ts +19 -0
  50. package/dist/documents/queries/list-page.js +36 -0
  51. package/dist/documents/queries/overview.query.d.ts +32 -0
  52. package/dist/documents/queries/overview.query.js +41 -0
  53. package/dist/documents/schemas.d.ts +93 -0
  54. package/dist/documents/schemas.js +51 -0
  55. package/dist/documents/screens.d.ts +3 -0
  56. package/dist/documents/screens.js +76 -0
  57. package/dist/documents/tab.d.ts +4 -0
  58. package/dist/documents/tab.js +42 -0
  59. package/dist/documents/target-write-check.d.ts +3 -0
  60. package/dist/documents/target-write-check.js +25 -0
  61. package/dist/documents/write-guard.d.ts +6 -0
  62. package/dist/documents/write-guard.js +46 -0
  63. package/dist/inbound-mail-foundation/entities.d.ts +17 -7
  64. package/dist/inbound-mail-foundation/ingest-failures.d.ts +3 -3
  65. package/dist/inbound-mail-foundation/projection.d.ts +15 -5
  66. package/dist/inbound-mail-foundation/quarantine-entities.d.ts +3 -3
  67. package/dist/jobs/job-run-table.d.ts +4 -4
  68. package/dist/ledger/entity.d.ts +8 -8
  69. package/dist/managed-pages/table.d.ts +24 -4
  70. package/dist/metrics/compute.d.ts +3 -0
  71. package/dist/metrics/compute.js +10 -3
  72. package/dist/metrics/feature.js +6 -2
  73. package/dist/metrics/types.d.ts +5 -1
  74. package/dist/metrics/validate.js +3 -0
  75. package/dist/personal-access-tokens/schema/api-token.d.ts +3 -3
  76. package/dist/readiness/schema/probe-run.d.ts +60 -20
  77. package/dist/secrets/table.d.ts +1 -1
  78. package/dist/sessions/schema/user-session.d.ts +4 -4
  79. package/dist/shared/purge-row-file-refs.js +5 -4
  80. package/dist/template-resolver/table.d.ts +12 -12
  81. package/dist/template-resolver/user-content-table.d.ts +4 -4
  82. package/dist/tenant/invitation-table.d.ts +6 -6
  83. package/dist/tenant/schema/tenant.d.ts +32 -12
  84. package/dist/tier-engine/entity.d.ts +1 -1
  85. package/dist/tier-engine/tier-resolver.d.ts +1 -1
  86. package/dist/user/schema/user.d.ts +28 -8
  87. package/dist/user-data-rights/schema/download-attempt.d.ts +2 -2
  88. package/dist/user-data-rights/schema/download-token.d.ts +8 -8
  89. package/dist/user-data-rights/schema/export-job.d.ts +12 -12
  90. package/dist/waitlist/entity.d.ts +8 -8
  91. package/package.json +19 -11
  92. package/src/billing-foundation/changes.json +6 -0
  93. package/src/channel-in-app/changes.json +6 -0
  94. package/src/metrics/changes.json +6 -0
@@ -0,0 +1,25 @@
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, the entity's update handler and its row-level write rule. A target that
13
+ // is not visible answers like a missing one.
14
+ export async function canWriteLinkTarget(registry, entityType, entityId, user, db) {
15
+ if (!(await parentRowIsVisible(registry, entityType, entityId, user, db)))
16
+ return false;
17
+ const entity = registry.getEntity(entityType);
18
+ if (entity === undefined)
19
+ return false;
20
+ const updateAccess = updateHandlerAccessOf(registry, entityType);
21
+ if (updateAccess === undefined || !hasAccess(user, updateAccess))
22
+ return false;
23
+ const row = await createEntityExecutor(entityType, entity).executor.detail({ id: entityId }, user, db);
24
+ return row !== null && userCanWriteFieldRow(user, entity.access?.write, row, row);
25
+ }
@@ -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: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
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: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
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">>;
@@ -25,7 +25,7 @@ export declare const quarantineEntity: import("@cosmicdrift/kumiko-types/fields"
25
25
  readonly min?: string;
26
26
  readonly max?: string;
27
27
  readonly locale?: string;
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 acknowledgedAt: {
@@ -41,7 +41,7 @@ export declare const quarantineEntity: 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: false;
46
46
  };
47
47
  readonly retryRequestedAt: {
@@ -57,7 +57,7 @@ export declare const quarantineEntity: import("@cosmicdrift/kumiko-types/fields"
57
57
  readonly min?: string;
58
58
  readonly max?: string;
59
59
  readonly locale?: string;
60
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
60
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
61
61
  required: false;
62
62
  };
63
63
  }, "tenant">;
@@ -34,7 +34,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
34
34
  readonly field: string;
35
35
  };
36
36
  readonly grouping?: boolean;
37
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
37
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
38
38
  required: true;
39
39
  };
40
40
  readonly startedAt: {
@@ -50,7 +50,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
50
50
  readonly min?: string;
51
51
  readonly max?: string;
52
52
  readonly locale?: string;
53
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
53
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
54
54
  required: true;
55
55
  };
56
56
  readonly finishedAt: {
@@ -66,7 +66,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
66
66
  readonly min?: string;
67
67
  readonly max?: string;
68
68
  readonly locale?: string;
69
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
69
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
70
70
  required: false;
71
71
  };
72
72
  readonly duration: {
@@ -86,7 +86,7 @@ export declare const jobRunEntity: import("@cosmicdrift/kumiko-types/fields").En
86
86
  readonly field: string;
87
87
  };
88
88
  readonly grouping?: boolean;
89
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
89
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
90
90
  required: false;
91
91
  };
92
92
  readonly triggeredById: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -25,7 +25,7 @@ export declare const accountEntity: import("@cosmicdrift/kumiko-types/fields").E
25
25
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
26
26
  readonly optionsAvailabilityQuery?: string;
27
27
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"asset" | "equity" | "expense" | "income" | "liability">[] | undefined;
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 code: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -48,7 +48,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
48
48
  readonly min?: string;
49
49
  readonly max?: string;
50
50
  readonly locale?: string;
51
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
51
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
52
52
  required: true;
53
53
  };
54
54
  readonly description: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -77,7 +77,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
77
77
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
78
78
  readonly optionsAvailabilityQuery?: string;
79
79
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"draft" | "posted">[] | undefined;
80
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
80
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
81
81
  required: true;
82
82
  };
83
83
  readonly lines: {
@@ -93,7 +93,7 @@ export declare const transactionEntity: import("@cosmicdrift/kumiko-types/fields
93
93
  readonly derived?: Readonly<Record<string, import("@cosmicdrift/kumiko-types/fields").EmbeddedDerivedCellDef>>;
94
94
  readonly totals?: readonly import("@cosmicdrift/kumiko-types/fields").EmbeddedTotalDef[];
95
95
  readonly totalsMatch?: Readonly<Record<string, string>>;
96
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
96
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
97
97
  multiple: true;
98
98
  };
99
99
  readonly subjectType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -119,7 +119,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
119
119
  readonly min?: string;
120
120
  readonly max?: string;
121
121
  readonly locale?: string;
122
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
122
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
123
123
  required: true;
124
124
  };
125
125
  readonly endDate: {
@@ -134,7 +134,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
134
134
  readonly min?: string;
135
135
  readonly max?: string;
136
136
  readonly locale?: string;
137
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
137
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
138
138
  required: false;
139
139
  };
140
140
  readonly interval: {
@@ -156,7 +156,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
156
156
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
157
157
  readonly optionsAvailabilityQuery?: string;
158
158
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"monthly">[] | undefined;
159
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
159
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
160
160
  required: true;
161
161
  };
162
162
  readonly amount: {
@@ -176,7 +176,7 @@ export declare const scheduleEntity: import("@cosmicdrift/kumiko-types/fields").
176
176
  readonly field: string;
177
177
  };
178
178
  readonly grouping?: boolean;
179
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
179
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
180
180
  required: true;
181
181
  };
182
182
  readonly debitAccountId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -32,7 +32,7 @@ export declare const pageEntity: import("@cosmicdrift/kumiko-types/fields").Enti
32
32
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
33
33
  readonly optionsAvailabilityQuery?: string;
34
34
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"document" | "markdown">[] | undefined;
35
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
35
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
36
36
  required: false;
37
37
  };
38
38
  readonly description: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -41,7 +41,17 @@ export declare const pageEntity: import("@cosmicdrift/kumiko-types/fields").Enti
41
41
  readonly ogImage: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
42
42
  required: false;
43
43
  };
44
- readonly published: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
44
+ readonly published: {
45
+ readonly type: "boolean";
46
+ readonly description?: string;
47
+ readonly required?: boolean;
48
+ readonly sortable?: boolean;
49
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
50
+ readonly filterable?: boolean;
51
+ readonly sensitive?: boolean;
52
+ readonly default?: boolean;
53
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
54
+ } & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
45
55
  required: false;
46
56
  default: boolean;
47
57
  };
@@ -79,7 +89,7 @@ export declare const pagesTable: import("@cosmicdrift/kumiko-framework/db").Enti
79
89
  readonly optionsQueryPayload?: import("@cosmicdrift/kumiko-types/fields").OptionsQueryPayload;
80
90
  readonly optionsAvailabilityQuery?: string;
81
91
  readonly conditionalOptions?: readonly import("@cosmicdrift/kumiko-types/fields").SelectConditionalOptions<"document" | "markdown">[] | undefined;
82
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
92
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
83
93
  required: false;
84
94
  };
85
95
  readonly description: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
@@ -88,7 +98,17 @@ export declare const pagesTable: import("@cosmicdrift/kumiko-framework/db").Enti
88
98
  readonly ogImage: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
89
99
  required: false;
90
100
  };
91
- readonly published: import("@cosmicdrift/kumiko-types/fields").BooleanFieldDef & {
101
+ readonly published: {
102
+ readonly type: "boolean";
103
+ readonly description?: string;
104
+ readonly required?: boolean;
105
+ readonly sortable?: boolean;
106
+ readonly sortNulls?: import("@cosmicdrift/kumiko-types/fields").SortNulls;
107
+ readonly filterable?: boolean;
108
+ readonly sensitive?: boolean;
109
+ readonly default?: boolean;
110
+ readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
111
+ } & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
92
112
  required: false;
93
113
  default: boolean;
94
114
  };
@@ -1,4 +1,5 @@
1
1
  import type { WhereObject } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { AggregateKey } from "@cosmicdrift/kumiko-types/aggregate-types";
2
3
  import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
3
4
  import type { TenantDb } from "@cosmicdrift/kumiko-types/tenant-db-types";
4
5
  import type { MetricDefinition, MetricResult } from "./types.ts";
@@ -11,5 +12,7 @@ export type MetricRunParams = {
11
12
  readonly filterWhere: WhereObject;
12
13
  /** Extra filter that narrows the source to the caller's scope. */
13
14
  readonly scopeWhere: WhereObject;
15
+ /** Names for referenced-row group keys; absent where the caller cannot resolve them (system scope). */
16
+ readonly referenceLabels?: (keys: readonly AggregateKey[]) => Promise<ReadonlyMap<string, string>>;
14
17
  };
15
18
  export declare function runMetric(db: TenantDb, metric: MetricDefinition, params: MetricRunParams): Promise<MetricResult>;
@@ -75,7 +75,7 @@ function groupSegments(stackRows, labelOf) {
75
75
  }
76
76
  return segmentsByGroup;
77
77
  }
78
- function buildRows(metric, aggregates, total, labelOf) {
78
+ function buildRows(metric, aggregates, total, labelOf, groupLabelOf) {
79
79
  const segmentsByGroup = groupSegments(aggregates.stacks, labelOf);
80
80
  return aggregates.groups.map((row) => {
81
81
  const key = keyText(row.keys[0]);
@@ -83,7 +83,7 @@ function buildRows(metric, aggregates, total, labelOf) {
83
83
  return {
84
84
  id: key,
85
85
  key,
86
- label: labelOf(key),
86
+ label: groupLabelOf(key),
87
87
  value: rowValue,
88
88
  fraction: fractionOf(rowValue, total),
89
89
  ...(metric.stackBy !== undefined && { segments: segmentsByGroup.get(key) ?? [] }),
@@ -104,6 +104,11 @@ function buildSeries(rows, seriesRows, fillBuckets) {
104
104
  points: fillBuckets(bucketsByGroup.get(row.key) ?? new Map()),
105
105
  }));
106
106
  }
107
+ async function resolveReferencedGroupLabels(metric, params, groups) {
108
+ if (params.referenceLabels === undefined || metric.groupBy === undefined)
109
+ return new Map();
110
+ return params.referenceLabels(groups.map((row) => row.keys[0] ?? null));
111
+ }
107
112
  export async function runMetric(db, metric, params) {
108
113
  const range = metric.window ?? params.range;
109
114
  const bucket = resolveBucket(metric, range);
@@ -129,7 +134,9 @@ export async function runMetric(db, metric, params) {
129
134
  row.value,
130
135
  ])))
131
136
  : [];
132
- const rows = buildRows(metric, aggregates, value, labelOf);
137
+ const referencedLabels = await resolveReferencedGroupLabels(metric, params, aggregates.groups);
138
+ const groupLabelOf = (key) => metric.groupLabels?.[key] ?? referencedLabels.get(key) ?? key;
139
+ const rows = buildRows(metric, aggregates, value, labelOf, groupLabelOf);
133
140
  return {
134
141
  value,
135
142
  previousValue,
@@ -1,4 +1,5 @@
1
- import { access, defineFeature, defineQueryHandler, } from "@cosmicdrift/kumiko-framework/engine";
1
+ import { extractTableInfo } from "@cosmicdrift/kumiko-framework/db";
2
+ import { access, defineFeature, defineQueryHandler, referenceGroupLabels, } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
3
4
  import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
4
5
  import * as z from "zod";
@@ -45,7 +46,7 @@ function createTenantMetricQuery(metric, now) {
45
46
  name: metric.id,
46
47
  description: metric.description,
47
48
  schema: tenantPayloadSchema(metric),
48
- access: { roles: access.admin },
49
+ access: { roles: metric.access?.roles ?? access.admin },
49
50
  handler: async (query, ctx) => runMetric(ctx.db, metric, {
50
51
  range: query.payload.range,
51
52
  timeZone: query.payload.timeZone,
@@ -53,6 +54,9 @@ function createTenantMetricQuery(metric, now) {
53
54
  filterWhere: await resolveMetricFilterWhere(metric, query.payload, ctx),
54
55
  // ctx.db already reads own-tenant + SYSTEM reference rows; this narrows to own-tenant only.
55
56
  scopeWhere: { tenantId: query.user.tenantId },
57
+ referenceLabels: (keys) => metric.groupBy === undefined
58
+ ? Promise.resolve(new Map())
59
+ : referenceGroupLabels(ctx, query.user, extractTableInfo(metric.source).name, metric.groupBy, keys, ctx.db),
56
60
  }),
57
61
  });
58
62
  }
@@ -33,8 +33,12 @@ export type MetricDefinition = {
33
33
  readonly groupBy?: string;
34
34
  /** Second dimension, becomes `rows[].segments`; only without bucket. */
35
35
  readonly stackBy?: string;
36
- /** Key value → i18n key, applied to groupBy and stackBy keys. */
36
+ /** Key value → i18n key, applied to groupBy and stackBy keys. Without an entry, a groupBy that is a single reference of an entity-table source is named after the referenced row (tenant scope, through the caller's read access); otherwise the key stays. */
37
37
  readonly groupLabels?: Readonly<Record<string, string>>;
38
+ /** Roles that may read the tenant query; default is the admin roles. Ignored for scope "system", which stays SystemAdmin only. */
39
+ readonly access?: {
40
+ readonly roles: readonly string[];
41
+ };
38
42
  readonly scopes: readonly MetricScope[];
39
43
  };
40
44
  export declare function defineMetric(definition: MetricDefinition): MetricDefinition;
@@ -25,6 +25,9 @@ function validateMetric(metric) {
25
25
  fail("has an id that is not kebab-case");
26
26
  if (metric.scopes.length === 0)
27
27
  fail("declares no scopes");
28
+ if (metric.access !== undefined && metric.access.roles.length === 0) {
29
+ fail("declares access with an empty roles list");
30
+ }
28
31
  const info = extractTableInfo(metric.source);
29
32
  validateColumns(metric, info, fail);
30
33
  validateFilters(metric, info, fail);
@@ -30,7 +30,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
30
30
  readonly min?: string;
31
31
  readonly max?: string;
32
32
  readonly locale?: string;
33
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
33
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
34
34
  required: true;
35
35
  };
36
36
  readonly expiresAt: {
@@ -46,7 +46,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
46
46
  readonly min?: string;
47
47
  readonly max?: string;
48
48
  readonly locale?: string;
49
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
49
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
50
50
  required: false;
51
51
  };
52
52
  readonly revokedAt: {
@@ -62,7 +62,7 @@ export declare const apiTokenEntity: import("@cosmicdrift/kumiko-types/fields").
62
62
  readonly min?: string;
63
63
  readonly max?: string;
64
64
  readonly locale?: string;
65
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
65
+ } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & import("@cosmicdrift/kumiko-types/fields").FieldStateConditions & {
66
66
  required: false;
67
67
  };
68
68
  }, "tenant">;