@cosmicdrift/kumiko-bundled-features 0.383.0 → 0.384.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 (126) hide show
  1. package/dist/audit/event-classification.d.ts +8 -0
  2. package/dist/audit/event-classification.js +69 -0
  3. package/dist/audit/feature.js +3 -3
  4. package/dist/audit/handlers/audit-events.d.ts +25 -3
  5. package/dist/audit/handlers/audit-events.js +41 -7
  6. package/dist/audit/handlers/details-all-tenants.query.d.ts +3 -0
  7. package/dist/audit/handlers/details-all-tenants.query.js +2 -5
  8. package/dist/audit/handlers/details.query.d.ts +3 -0
  9. package/dist/audit/handlers/details.query.js +5 -3
  10. package/dist/audit/handlers/list-all-tenants.query.d.ts +9 -2
  11. package/dist/audit/handlers/list-all-tenants.query.js +10 -8
  12. package/dist/audit/handlers/list.query.d.ts +8 -0
  13. package/dist/audit/handlers/list.query.js +12 -4
  14. package/dist/audit/screens.d.ts +37 -0
  15. package/dist/audit/screens.js +26 -4
  16. package/dist/audit/web/audit-event-cell.d.ts +4 -0
  17. package/dist/audit/web/audit-event-cell.js +20 -0
  18. package/dist/audit/web/audit-event-labels.d.ts +7 -0
  19. package/dist/audit/web/audit-event-labels.js +23 -0
  20. package/dist/audit/web/client-plugin.js +6 -1
  21. package/dist/audit/web/constants.d.ts +8 -0
  22. package/dist/audit/web/constants.js +7 -0
  23. package/dist/audit/web/i18n.js +48 -1
  24. package/dist/auth-email-password/feature.js +3 -2
  25. package/dist/auth-email-password/handlers/login-options.query.d.ts +5 -0
  26. package/dist/auth-email-password/handlers/login-options.query.js +17 -0
  27. package/dist/auth-email-password/index.d.ts +1 -1
  28. package/dist/auth-email-password/index.js +1 -1
  29. package/dist/auth-email-password/{auth-paths.d.ts → web/auth-paths.d.ts} +5 -3
  30. package/dist/auth-email-password/{auth-paths.js → web/auth-paths.js} +5 -3
  31. package/dist/auth-email-password/web/client-plugin.d.ts +5 -0
  32. package/dist/auth-email-password/web/client-plugin.js +2 -0
  33. package/dist/auth-email-password/web/constants.d.ts +1 -0
  34. package/dist/auth-email-password/web/constants.js +1 -0
  35. package/dist/auth-email-password/web/default-auth-pages-gate.d.ts +4 -0
  36. package/dist/auth-email-password/web/default-auth-pages-gate.js +28 -0
  37. package/dist/auth-email-password/web/login-screen.d.ts +13 -18
  38. package/dist/auth-email-password/web/login-screen.js +31 -19
  39. package/dist/auth-mfa/schema/user-mfa.d.ts +6 -126
  40. package/dist/billing-foundation/entities.d.ts +8 -168
  41. package/dist/billing-foundation/projection.d.ts +8 -168
  42. package/dist/cap-counter/entity.d.ts +1 -21
  43. package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -28
  44. package/dist/config/table.d.ts +6 -126
  45. package/dist/custom-fields/entity.d.ts +4 -84
  46. package/dist/data-retention/schema/tenant-retention-override.d.ts +6 -112
  47. package/dist/delivery/tables.d.ts +6 -126
  48. package/dist/document-ingest-foundation/entity.d.ts +6 -112
  49. package/dist/folders/entity.d.ts +8 -168
  50. package/dist/form-draft/entity.d.ts +2 -42
  51. package/dist/inbound-mail-foundation/entities.d.ts +25 -525
  52. package/dist/inbound-mail-foundation/projection.d.ts +20 -420
  53. package/dist/jobs/feature.js +22 -117
  54. package/dist/jobs/handlers/consumer.write.d.ts +37 -0
  55. package/dist/jobs/handlers/consumer.write.js +59 -0
  56. package/dist/jobs/handlers/consumers.query.d.ts +52 -0
  57. package/dist/jobs/handlers/consumers.query.js +114 -0
  58. package/dist/jobs/job-run-table.d.ts +6 -126
  59. package/dist/jobs/screens.d.ts +6 -0
  60. package/dist/jobs/screens.js +241 -0
  61. package/dist/jobs/web/constants.d.ts +20 -0
  62. package/dist/jobs/web/constants.js +21 -0
  63. package/dist/jobs/web/i18n.js +41 -0
  64. package/dist/ledger/entity.d.ts +13 -273
  65. package/dist/managed-pages/table.d.ts +12 -252
  66. package/dist/notes-history/entity.d.ts +14 -280
  67. package/dist/personal-access-tokens/schema/api-token.d.ts +6 -126
  68. package/dist/secrets/i18n.d.ts +0 -3
  69. package/dist/secrets/i18n.js +0 -3
  70. package/dist/secrets/table.d.ts +1 -21
  71. package/dist/sessions/constants.d.ts +2 -1
  72. package/dist/sessions/constants.js +2 -1
  73. package/dist/sessions/handlers/detail.query.d.ts +5 -0
  74. package/dist/sessions/handlers/detail.query.js +7 -3
  75. package/dist/sessions/handlers/list.query.d.ts +5 -0
  76. package/dist/sessions/handlers/list.query.js +16 -10
  77. package/dist/sessions/handlers/mine.query.d.ts +5 -0
  78. package/dist/sessions/handlers/mine.query.js +15 -9
  79. package/dist/sessions/schema/user-session.d.ts +4 -84
  80. package/dist/sessions/screens.js +22 -8
  81. package/dist/sessions/web/client-plugin.d.ts +6 -0
  82. package/dist/sessions/web/client-plugin.js +11 -0
  83. package/dist/sessions/web/constants.d.ts +3 -0
  84. package/dist/sessions/web/constants.js +3 -0
  85. package/dist/sessions/web/default-translations.d.ts +2 -0
  86. package/dist/sessions/web/default-translations.js +3 -0
  87. package/dist/sessions/web/i18n.js +5 -1
  88. package/dist/sessions/web/index.d.ts +1 -0
  89. package/dist/sessions/web/index.js +1 -0
  90. package/dist/sessions/web/session-device-cell.d.ts +3 -0
  91. package/dist/sessions/web/session-device-cell.js +16 -0
  92. package/dist/sessions/web/user-agent.d.ts +26 -0
  93. package/dist/sessions/web/user-agent.js +58 -0
  94. package/dist/tags/entity.d.ts +9 -189
  95. package/dist/template-resolver/feature.d.ts +1 -0
  96. package/dist/template-resolver/feature.js +2 -0
  97. package/dist/template-resolver/handlers/overview.query.d.ts +5 -0
  98. package/dist/template-resolver/handlers/overview.query.js +19 -2
  99. package/dist/template-resolver/handlers/tenant-override-counts.query.d.ts +17 -0
  100. package/dist/template-resolver/handlers/tenant-override-counts.query.js +35 -0
  101. package/dist/template-resolver/handlers/tenant-override-selection.d.ts +12 -0
  102. package/dist/template-resolver/handlers/tenant-override-selection.js +34 -0
  103. package/dist/template-resolver/handlers/tenant-overrides.query.js +3 -29
  104. package/dist/template-resolver/table.d.ts +20 -350
  105. package/dist/template-resolver/user-content-table.d.ts +12 -238
  106. package/dist/template-resolver/web/qualified-names.d.ts +1 -0
  107. package/dist/template-resolver/web/qualified-names.js +1 -0
  108. package/dist/tenant/invitation-table.d.ts +6 -126
  109. package/dist/tenant/membership-table.d.ts +4 -84
  110. package/dist/tenant/schema/tenant.d.ts +6 -126
  111. package/dist/tier-engine/entity.d.ts +1 -21
  112. package/dist/tier-engine/tier-resolver.d.ts +1 -21
  113. package/dist/user/schema/user.d.ts +12 -252
  114. package/dist/user-data-rights/feature.js +33 -33
  115. package/dist/user-data-rights/schema/download-attempt.d.ts +14 -294
  116. package/dist/user-data-rights/schema/download-token.d.ts +8 -168
  117. package/dist/user-data-rights/schema/export-job.d.ts +8 -154
  118. package/dist/user-data-rights/web/i18n.js +4 -1
  119. package/dist/waitlist/entity.d.ts +16 -322
  120. package/package.json +17 -9
  121. package/src/audit/changes.json +6 -0
  122. package/src/auth-email-password/changes.json +6 -0
  123. package/src/jobs/changes.json +6 -0
  124. package/src/sessions/changes.json +6 -0
  125. package/src/template-resolver/changes.json +6 -0
  126. package/src/user-data-rights/changes.json +18 -0
@@ -0,0 +1,26 @@
1
+ import * as z from "zod";
2
+ export declare const USER_AGENT_BROWSERS: readonly ["Edge", "Opera", "Samsung Internet", "Firefox", "Chrome", "Safari"];
3
+ export declare const USER_AGENT_OPERATING_SYSTEMS: readonly ["Windows", "iOS", "Android", "macOS", "ChromeOS", "Linux"];
4
+ export type UserAgentBrowser = (typeof USER_AGENT_BROWSERS)[number];
5
+ export type UserAgentOperatingSystem = (typeof USER_AGENT_OPERATING_SYSTEMS)[number];
6
+ export declare const parsedDeviceSchema: z.ZodNullable<z.ZodObject<{
7
+ browser: z.ZodNullable<z.ZodEnum<{
8
+ Chrome: "Chrome";
9
+ Edge: "Edge";
10
+ Firefox: "Firefox";
11
+ Opera: "Opera";
12
+ Safari: "Safari";
13
+ "Samsung Internet": "Samsung Internet";
14
+ }>>;
15
+ browserMajor: z.ZodNullable<z.ZodNumber>;
16
+ os: z.ZodNullable<z.ZodEnum<{
17
+ Android: "Android";
18
+ ChromeOS: "ChromeOS";
19
+ Linux: "Linux";
20
+ Windows: "Windows";
21
+ iOS: "iOS";
22
+ macOS: "macOS";
23
+ }>>;
24
+ }, z.core.$strip>>;
25
+ export type ParsedDevice = z.infer<typeof parsedDeviceSchema>;
26
+ export declare function parseUserAgent(userAgent: string | null): ParsedDevice;
@@ -0,0 +1,58 @@
1
+ import * as z from "zod";
2
+ export const USER_AGENT_BROWSERS = [
3
+ "Edge",
4
+ "Opera",
5
+ "Samsung Internet",
6
+ "Firefox",
7
+ "Chrome",
8
+ "Safari",
9
+ ];
10
+ export const USER_AGENT_OPERATING_SYSTEMS = [
11
+ "Windows",
12
+ "iOS",
13
+ "Android",
14
+ "macOS",
15
+ "ChromeOS",
16
+ "Linux",
17
+ ];
18
+ export const parsedDeviceSchema = z
19
+ .object({
20
+ browser: z.enum(USER_AGENT_BROWSERS).nullable(),
21
+ browserMajor: z.number().int().nullable(),
22
+ os: z.enum(USER_AGENT_OPERATING_SYSTEMS).nullable(),
23
+ })
24
+ .nullable();
25
+ // Order matters: Chromium forks and iOS Chrome/Firefox also carry the engine tokens of the browsers they derive from.
26
+ const BROWSER_PATTERNS = [
27
+ { browser: "Edge", pattern: /\bEdg(?:e|A|iOS)?\/(\d+)/ },
28
+ { browser: "Opera", pattern: /\bOPR\/(\d+)/ },
29
+ { browser: "Samsung Internet", pattern: /\bSamsungBrowser\/(\d+)/ },
30
+ { browser: "Firefox", pattern: /\b(?:Firefox|FxiOS)\/(\d+)/ },
31
+ { browser: "Chrome", pattern: /\b(?:Chrome|CriOS)\/(\d+)/ },
32
+ { browser: "Safari", pattern: /\bVersion\/(\d+)[\s\S]*\bSafari\// },
33
+ ];
34
+ // iPhone/iPad UAs say "like Mac OS X" and Android UAs say "Linux", so the more specific systems come first.
35
+ const OPERATING_SYSTEM_PATTERNS = [
36
+ { os: "Windows", pattern: /\bWindows\b/ },
37
+ { os: "iOS", pattern: /\b(?:iPhone|iPad|iPod)\b/ },
38
+ { os: "Android", pattern: /\bAndroid\b/ },
39
+ { os: "macOS", pattern: /\bMacintosh\b/ },
40
+ { os: "ChromeOS", pattern: /\bCrOS\b/ },
41
+ { os: "Linux", pattern: /\bLinux\b/ },
42
+ ];
43
+ export function parseUserAgent(userAgent) {
44
+ if (userAgent === null || userAgent === "")
45
+ return null;
46
+ let browser = null;
47
+ let browserMajor = null;
48
+ for (const candidate of BROWSER_PATTERNS) {
49
+ const match = candidate.pattern.exec(userAgent);
50
+ if (match) {
51
+ browser = candidate.browser;
52
+ browserMajor = match[1] === undefined ? null : Number(match[1]);
53
+ break;
54
+ }
55
+ }
56
+ const os = OPERATING_SYSTEM_PATTERNS.find((candidate) => candidate.pattern.test(userAgent))?.os ?? null;
57
+ return browser === null && os === null ? null : { browser, browserMajor, os };
58
+ }
@@ -1,214 +1,34 @@
1
1
  import { type EntityDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
2
  export declare const tagEntity: EntityDefinition<{
3
- readonly name: {
4
- readonly type: "text";
5
- readonly description?: string;
6
- readonly maxLength?: number;
7
- readonly minLength?: number;
8
- readonly autoComplete?: string;
9
- readonly required?: boolean;
10
- readonly searchable?: boolean;
11
- readonly sortable?: boolean;
12
- readonly filterable?: boolean;
13
- readonly encrypted?: boolean;
14
- readonly sensitive?: boolean;
15
- readonly writeOnly?: boolean;
16
- readonly readAsInstruction?: boolean;
17
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
18
- readonly default?: string;
19
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
20
- readonly multiline?: boolean | {
21
- readonly rows?: number;
22
- };
23
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
3
+ readonly name: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
24
4
  required: true;
25
5
  };
26
- readonly color: {
27
- readonly type: "text";
28
- readonly description?: string;
29
- readonly maxLength?: number;
30
- readonly minLength?: number;
31
- readonly autoComplete?: string;
32
- readonly required?: boolean;
33
- readonly searchable?: boolean;
34
- readonly sortable?: boolean;
35
- readonly filterable?: boolean;
36
- readonly encrypted?: boolean;
37
- readonly sensitive?: boolean;
38
- readonly writeOnly?: boolean;
39
- readonly readAsInstruction?: boolean;
40
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
41
- readonly default?: string;
42
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
43
- readonly multiline?: boolean | {
44
- readonly rows?: number;
45
- };
46
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
6
+ readonly color: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
47
7
  required: false;
48
8
  };
49
- readonly scope: {
50
- readonly type: "text";
51
- readonly description?: string;
52
- readonly maxLength?: number;
53
- readonly minLength?: number;
54
- readonly autoComplete?: string;
55
- readonly required?: boolean;
56
- readonly searchable?: boolean;
57
- readonly sortable?: boolean;
58
- readonly filterable?: boolean;
59
- readonly encrypted?: boolean;
60
- readonly sensitive?: boolean;
61
- readonly writeOnly?: boolean;
62
- readonly readAsInstruction?: boolean;
63
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
64
- readonly default?: string;
65
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
66
- readonly multiline?: boolean | {
67
- readonly rows?: number;
68
- };
69
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
9
+ readonly scope: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
70
10
  required: false;
71
11
  };
72
12
  }, "tenant">;
73
13
  export declare function createTagAssignmentEntity(access?: EntityDefinition["access"], parents?: readonly string[]): EntityDefinition<{
74
- readonly tagId: {
75
- readonly type: "text";
76
- readonly description?: string;
77
- readonly maxLength?: number;
78
- readonly minLength?: number;
79
- readonly autoComplete?: string;
80
- readonly required?: boolean;
81
- readonly searchable?: boolean;
82
- readonly sortable?: boolean;
83
- readonly filterable?: boolean;
84
- readonly encrypted?: boolean;
85
- readonly sensitive?: boolean;
86
- readonly writeOnly?: boolean;
87
- readonly readAsInstruction?: boolean;
88
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
89
- readonly default?: string;
90
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
91
- readonly multiline?: boolean | {
92
- readonly rows?: number;
93
- };
94
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
14
+ readonly tagId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
95
15
  required: true;
96
16
  };
97
- readonly entityType: {
98
- readonly type: "text";
99
- readonly description?: string;
100
- readonly maxLength?: number;
101
- readonly minLength?: number;
102
- readonly autoComplete?: string;
103
- readonly required?: boolean;
104
- readonly searchable?: boolean;
105
- readonly sortable?: boolean;
106
- readonly filterable?: boolean;
107
- readonly encrypted?: boolean;
108
- readonly sensitive?: boolean;
109
- readonly writeOnly?: boolean;
110
- readonly readAsInstruction?: boolean;
111
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
112
- readonly default?: string;
113
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
114
- readonly multiline?: boolean | {
115
- readonly rows?: number;
116
- };
117
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
17
+ readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
118
18
  required: true;
119
19
  };
120
- readonly entityId: {
121
- readonly type: "text";
122
- readonly description?: string;
123
- readonly maxLength?: number;
124
- readonly minLength?: number;
125
- readonly autoComplete?: string;
126
- readonly required?: boolean;
127
- readonly searchable?: boolean;
128
- readonly sortable?: boolean;
129
- readonly filterable?: boolean;
130
- readonly encrypted?: boolean;
131
- readonly sensitive?: boolean;
132
- readonly writeOnly?: boolean;
133
- readonly readAsInstruction?: boolean;
134
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
135
- readonly default?: string;
136
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
137
- readonly multiline?: boolean | {
138
- readonly rows?: number;
139
- };
140
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
20
+ readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
141
21
  required: true;
142
22
  };
143
23
  }, "tenant">;
144
24
  export declare const tagAssignmentEntity: EntityDefinition<{
145
- readonly tagId: {
146
- readonly type: "text";
147
- readonly description?: string;
148
- readonly maxLength?: number;
149
- readonly minLength?: number;
150
- readonly autoComplete?: string;
151
- readonly required?: boolean;
152
- readonly searchable?: boolean;
153
- readonly sortable?: boolean;
154
- readonly filterable?: boolean;
155
- readonly encrypted?: boolean;
156
- readonly sensitive?: boolean;
157
- readonly writeOnly?: boolean;
158
- readonly readAsInstruction?: boolean;
159
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
160
- readonly default?: string;
161
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
162
- readonly multiline?: boolean | {
163
- readonly rows?: number;
164
- };
165
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
25
+ readonly tagId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
166
26
  required: true;
167
27
  };
168
- readonly entityType: {
169
- readonly type: "text";
170
- readonly description?: string;
171
- readonly maxLength?: number;
172
- readonly minLength?: number;
173
- readonly autoComplete?: string;
174
- readonly required?: boolean;
175
- readonly searchable?: boolean;
176
- readonly sortable?: boolean;
177
- readonly filterable?: boolean;
178
- readonly encrypted?: boolean;
179
- readonly sensitive?: boolean;
180
- readonly writeOnly?: boolean;
181
- readonly readAsInstruction?: boolean;
182
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
183
- readonly default?: string;
184
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
185
- readonly multiline?: boolean | {
186
- readonly rows?: number;
187
- };
188
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
28
+ readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
189
29
  required: true;
190
30
  };
191
- readonly entityId: {
192
- readonly type: "text";
193
- readonly description?: string;
194
- readonly maxLength?: number;
195
- readonly minLength?: number;
196
- readonly autoComplete?: string;
197
- readonly required?: boolean;
198
- readonly searchable?: boolean;
199
- readonly sortable?: boolean;
200
- readonly filterable?: boolean;
201
- readonly encrypted?: boolean;
202
- readonly sensitive?: boolean;
203
- readonly writeOnly?: boolean;
204
- readonly readAsInstruction?: boolean;
205
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
206
- readonly default?: string;
207
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
208
- readonly multiline?: boolean | {
209
- readonly rows?: number;
210
- };
211
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
31
+ readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
212
32
  required: true;
213
33
  };
214
34
  }, "tenant">;
@@ -51,6 +51,7 @@ export declare function createTemplateResolverFeature(opts?: TemplateResolverOpt
51
51
  overview: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
52
52
  layoutSource: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
53
53
  tenantOverrides: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
54
+ tenantOverrideCounts: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
54
55
  diffToSystem: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
55
56
  validate: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
56
57
  preview: import("@cosmicdrift/kumiko-types/handlers").HandlerRef;
@@ -24,6 +24,7 @@ import { makeResourceUploadWrite } from "./handlers/resource-upload.write.js";
24
24
  import { makeSendTestWrite } from "./handlers/send-test.write.js";
25
25
  import { sendTestStatusQuery } from "./handlers/send-test-status.query.js";
26
26
  import { makeSetWrite } from "./handlers/set.write.js";
27
+ import { tenantOverrideCountsQuery } from "./handlers/tenant-override-counts.query.js";
27
28
  import { tenantOverridesQuery } from "./handlers/tenant-overrides.query.js";
28
29
  import { makeArchiveSystemWrite, makeArchiveWrite, makePublishSystemWrite, makePublishWrite, } from "./handlers/toggle-status.write.js";
29
30
  import { upsertSystemWrite } from "./handlers/upsert-system.write.js";
@@ -130,6 +131,7 @@ export function createTemplateResolverFeature(opts = {}) {
130
131
  overview: r.queryHandler(makeOverviewQuery(policy)),
131
132
  layoutSource: r.queryHandler(layoutSourceQuery),
132
133
  tenantOverrides: r.queryHandler(tenantOverridesQuery),
134
+ tenantOverrideCounts: r.queryHandler(tenantOverrideCountsQuery),
133
135
  diffToSystem: r.queryHandler(diffToSystemQuery),
134
136
  validate: r.queryHandler(validateQuery),
135
137
  preview: r.queryHandler(previewQuery),
@@ -38,6 +38,11 @@ export type OverviewRow = {
38
38
  readonly modifiedAt: string;
39
39
  readonly usedBy: readonly OverviewUsage[];
40
40
  readonly usedByCount: number;
41
+ /** SystemAdmin only, system-origin rows: tenants overriding this template; total is null without the tenant feature. */
42
+ readonly overriddenBy?: {
43
+ readonly active: number;
44
+ readonly total: number | null;
45
+ };
41
46
  };
42
47
  export type OverviewResponse = {
43
48
  readonly rows: readonly OverviewRow[];
@@ -3,12 +3,14 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
3
  import * as z from "zod";
4
4
  import { templateResourcesTable } from "../table.js";
5
5
  import { deliveryPolicyOf, isTenantOverrideServed, } from "../template-delivery-policy.js";
6
- import { activeSinceOf, activeVersionOf, draftViewerTenantId, isHiddenSystemDraft, parseTemplateDraft, servedVersionOf, } from "../template-draft.js";
6
+ import { activeSinceOf, activeVersionOf, draftViewerTenantId, isHiddenSystemDraft, isSystemAdmin, parseTemplateDraft, servedVersionOf, } from "../template-draft.js";
7
7
  import { mayEditTemplate } from "../template-edit-policy.js";
8
8
  import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.js";
9
9
  import { collectTemplateUsage } from "../template-usage.js";
10
10
  import { DEFAULT_MAIL_LAYOUT_SLUG, FALLBACK_LOCALE, } from "../web/constants.js";
11
+ import { TemplateResolverQueries } from "../web/qualified-names.js";
11
12
  import { storedRecord } from "./template-gates.js";
13
+ import { tenantOverrideCountsResultSchema } from "./tenant-override-counts.query.js";
12
14
  const OVERVIEW_KINDS = ["mail", "document-pdf", "mail-layout", "pdf-layout"];
13
15
  const OVERVIEW_ROW_LIMIT = 5000;
14
16
  const LOCKED_OVERRIDE_I18N_KEY = "templateResolver.overview.lockedOverride";
@@ -188,6 +190,18 @@ function withUsage(draft, layoutSlugByDocument, registry, effectiveLayouts) {
188
190
  }
189
191
  return { ...draft, usedBy, usedByCount: usedBy.length };
190
192
  }
193
+ function withOverriddenBy(rows, overrides) {
194
+ const activeByKey = new Map(overrides.counts.map((entry) => [`${entry.kind}|${entry.slug}`, entry.activeOverrides]));
195
+ return rows.map((row) => row.origin === "system"
196
+ ? {
197
+ ...row,
198
+ overriddenBy: {
199
+ active: activeByKey.get(`${row.kind}|${row.slug}`) ?? 0,
200
+ total: overrides.totalTenants,
201
+ },
202
+ }
203
+ : row);
204
+ }
191
205
  function filterRows(rows, search, filters, layoutSlug) {
192
206
  const needle = search?.trim().toLowerCase() ?? "";
193
207
  return rows.filter((row) => {
@@ -234,7 +248,10 @@ export function makeOverviewQuery(policy) {
234
248
  const deliveryPolicy = deliveryPolicyOf(ctx.registry);
235
249
  const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user, deliveryPolicy);
236
250
  const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows, deliveryPolicy);
237
- const overviewRows = drafts.map((draft) => withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts));
251
+ const rowsWithUsage = drafts.map((draft) => withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts));
252
+ const overviewRows = isSystemAdmin(query.user)
253
+ ? withOverriddenBy(rowsWithUsage, tenantOverrideCountsResultSchema.parse(await ctx.query(TemplateResolverQueries.tenantOverrideCounts, {})))
254
+ : rowsWithUsage;
238
255
  const filtered = filterRows(overviewRows, payload.search, payload.filters ?? [], payload.layoutSlug);
239
256
  const sortField = payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
240
257
  const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
@@ -0,0 +1,17 @@
1
+ import * as z from "zod";
2
+ export declare const tenantOverrideCountsResultSchema: z.ZodObject<{
3
+ totalTenants: z.ZodNullable<z.ZodNumber>;
4
+ counts: z.ZodArray<z.ZodObject<{
5
+ slug: z.ZodString;
6
+ kind: z.ZodString;
7
+ activeOverrides: z.ZodNumber;
8
+ }, z.core.$strip>>;
9
+ }, z.core.$strip>;
10
+ export declare const tenantOverrideCountsQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"tenant-override-counts", z.ZodObject<{}, z.core.$strip>, {
11
+ totalTenants: number | null;
12
+ counts: {
13
+ slug: string;
14
+ kind: string;
15
+ activeOverrides: number;
16
+ }[];
17
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
@@ -0,0 +1,35 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/db";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import * as z from "zod";
4
+ import { templateResourcesTable } from "../table.js";
5
+ import { loadEnabledTenantIds, pickBestRowPerTenant, TENANT_OVERRIDES_REASON, } from "./tenant-override-selection.js";
6
+ export const tenantOverrideCountsResultSchema = z.object({
7
+ totalTenants: z.number().nullable(),
8
+ counts: z.array(z.object({ slug: z.string(), kind: z.string(), activeOverrides: z.number() })),
9
+ });
10
+ export const tenantOverrideCountsQuery = defineQueryHandler({
11
+ name: "tenant-override-counts",
12
+ schema: z.object({}),
13
+ access: { roles: ["SystemAdmin"] },
14
+ description: "Counts, per template slug and kind, the enabled tenants that override it with an active version, next to the number of enabled tenants; one query for the whole template overview instead of one tenant-overrides call per row.",
15
+ escapeHatch: { grants: ["unsafeRaw"], reason: TENANT_OVERRIDES_REASON },
16
+ handler: async (_query, ctx) => {
17
+ const found = await selectMany(ctx.db.unsafeRaw(), templateResourcesTable, {
18
+ scope: "tenant",
19
+ });
20
+ const enabledTenantIds = await loadEnabledTenantIds(ctx);
21
+ const rowsBySlugAndKind = new Map();
22
+ for (const row of found) {
23
+ const key = `${row.kind}|${row.slug}`;
24
+ const group = rowsBySlugAndKind.get(key) ?? { slug: row.slug, kind: row.kind, rows: [] };
25
+ group.rows.push(row);
26
+ rowsBySlugAndKind.set(key, group);
27
+ }
28
+ const counts = [...rowsBySlugAndKind.values()].map(({ slug, kind, rows }) => ({
29
+ slug,
30
+ kind,
31
+ activeOverrides: [...pickBestRowPerTenant(rows, enabledTenantIds).values()].filter((row) => row.status === "active").length,
32
+ }));
33
+ return { totalTenants: enabledTenantIds === null ? null : enabledTenantIds.size, counts };
34
+ },
35
+ });
@@ -0,0 +1,12 @@
1
+ import type { TemplateResourceRow } from "../table.ts";
2
+ export declare const TENANT_OVERRIDES_REASON = "SystemAdmin template overview lists which tenants override one system template; it reads tenant-scope template rows of every tenant (and the system row for the optional per-tenant diff), which the tenant-scoped db cannot reach";
3
+ type TenantIdQueryContext = {
4
+ readonly registry: {
5
+ getQueryHandler(name: string): unknown;
6
+ };
7
+ readonly query: (name: string, payload: Record<string, never>) => Promise<unknown>;
8
+ };
9
+ /** Null when the tenant feature is not installed: every tenant row then counts. */
10
+ export declare function loadEnabledTenantIds(ctx: TenantIdQueryContext): Promise<ReadonlySet<string> | null>;
11
+ export declare function pickBestRowPerTenant(found: readonly TemplateResourceRow[], enabledTenantIds: ReadonlySet<string> | null): Map<string, TemplateResourceRow>;
12
+ export {};
@@ -0,0 +1,34 @@
1
+ import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { TenantQueries } from "../../tenant/index.js";
4
+ export const TENANT_OVERRIDES_REASON = "SystemAdmin template overview lists which tenants override one system template; it reads tenant-scope template rows of every tenant (and the system row for the optional per-tenant diff), which the tenant-scoped db cannot reach";
5
+ const tenantIdsSchema = z.array(z.string());
6
+ /** Null when the tenant feature is not installed: every tenant row then counts. */
7
+ export async function loadEnabledTenantIds(ctx) {
8
+ if (ctx.registry.getQueryHandler(TenantQueries.activeTenantIds) === undefined)
9
+ return null;
10
+ return new Set(tenantIdsSchema.parse(await ctx.query(TenantQueries.activeTenantIds, {})));
11
+ }
12
+ // One row per tenant: its active override wins over a draft, then the newest version.
13
+ function isBetterRow(candidate, current) {
14
+ if ((candidate.status === "active") !== (current.status === "active")) {
15
+ return candidate.status === "active";
16
+ }
17
+ return candidate.version > current.version;
18
+ }
19
+ function isCountedTenantRow(row, enabledTenantIds) {
20
+ if (row.tenantId === SYSTEM_TENANT_ID || row.status === "archived")
21
+ return false;
22
+ return enabledTenantIds === null || enabledTenantIds.has(row.tenantId);
23
+ }
24
+ export function pickBestRowPerTenant(found, enabledTenantIds) {
25
+ const bestByTenant = new Map();
26
+ for (const row of found) {
27
+ if (!isCountedTenantRow(row, enabledTenantIds))
28
+ continue;
29
+ const current = bestByTenant.get(row.tenantId);
30
+ if (current === undefined || isBetterRow(row, current))
31
+ bestByTenant.set(row.tenantId, row);
32
+ }
33
+ return bestByTenant;
34
+ }
@@ -6,40 +6,16 @@ import { templateResourcesTable } from "../table.js";
6
6
  import { servedVersionOf } from "../template-draft.js";
7
7
  import { buildOverrideDiff } from "./override-diff.js";
8
8
  import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
9
- const TENANT_OVERRIDES_REASON = "SystemAdmin template overview lists which tenants override one system template; it reads tenant-scope template rows of every tenant (and the system row for the optional per-tenant diff), which the tenant-scoped db cannot reach";
10
- const tenantIdsSchema = z.array(z.string());
9
+ import { loadEnabledTenantIds, pickBestRowPerTenant, TENANT_OVERRIDES_REASON, } from "./tenant-override-selection.js";
11
10
  const directorySchema = z.object({
12
11
  rows: z.array(z.object({ id: z.string(), label: z.string() })),
13
12
  });
14
13
  function labelsOf(raw) {
15
14
  return new Map(directorySchema.parse(raw).rows.map((row) => [row.id, row.label]));
16
15
  }
17
- // One row per tenant: its active override wins over a draft, then the newest version.
18
- function isBetterRow(candidate, current) {
19
- if ((candidate.status === "active") !== (current.status === "active")) {
20
- return candidate.status === "active";
21
- }
22
- return candidate.version > current.version;
23
- }
24
16
  function statusOf(row) {
25
17
  return row.status === "active" ? "active" : "draft";
26
18
  }
27
- function isCountedTenantRow(row, enabledTenantIds) {
28
- if (row.tenantId === SYSTEM_TENANT_ID || row.status === "archived")
29
- return false;
30
- return enabledTenantIds === null || enabledTenantIds.has(row.tenantId);
31
- }
32
- function pickBestRowPerTenant(found, enabledTenantIds) {
33
- const bestByTenant = new Map();
34
- for (const row of found) {
35
- if (!isCountedTenantRow(row, enabledTenantIds))
36
- continue;
37
- const current = bestByTenant.get(row.tenantId);
38
- if (current === undefined || isBetterRow(row, current))
39
- bestByTenant.set(row.tenantId, row);
40
- }
41
- return bestByTenant;
42
- }
43
19
  function toOverrideRow(row, tenantLabels, userLabels) {
44
20
  const changedById = row.modifiedById ?? row.insertedById;
45
21
  return {
@@ -74,10 +50,8 @@ export const tenantOverridesQuery = defineQueryHandler({
74
50
  scope: "tenant",
75
51
  ...(locale === undefined ? {} : { locale }),
76
52
  });
77
- const hasTenantFeature = ctx.registry.getQueryHandler(TenantQueries.activeTenantIds) !== undefined;
78
- const enabledTenantIds = hasTenantFeature
79
- ? new Set(tenantIdsSchema.parse(await ctx.query(TenantQueries.activeTenantIds, {})))
80
- : null;
53
+ const enabledTenantIds = await loadEnabledTenantIds(ctx);
54
+ const hasTenantFeature = enabledTenantIds !== null;
81
55
  const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
82
56
  const best = [...bestByTenant.values()];
83
57
  const tenantLabels = hasTenantFeature