@cosmicdrift/kumiko-framework 0.362.0 → 0.363.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/api/auth-middleware.js +7 -5
- package/dist/api/redis-sse-broker.js +10 -8
- package/dist/api/routes.js +2 -2
- package/dist/bun-db/query.d.ts +6 -4
- package/dist/bun-db/query.js +66 -92
- package/dist/crypto/kek-source.js +5 -3
- package/dist/crypto/pii-field-encryption.d.ts +4 -1
- package/dist/crypto/pii-field-encryption.js +2 -2
- package/dist/db/apply-entity-event.js +2 -2
- package/dist/db/cursor.js +2 -1
- package/dist/db/dialect.d.ts +1 -0
- package/dist/db/dialect.js +28 -13
- package/dist/db/entity-table-from-registry.js +1 -1
- package/dist/db/entity-table-meta.js +5 -13
- package/dist/db/event-store-executor-context.js +6 -6
- package/dist/db/event-store-executor-read.js +9 -9
- package/dist/db/migrate-generator.js +5 -3
- package/dist/db/migrate-runner.js +31 -35
- package/dist/db/parent-ref-clause.js +2 -2
- package/dist/db/queries/backfill-pii.js +11 -9
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +17 -17
- package/dist/db/queries/event-store-admin.js +6 -6
- package/dist/db/queries/event-store-transfer.js +2 -2
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +2 -2
- package/dist/db/queries/pii-backfill-state.js +2 -2
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/raw-sql.js +1 -1
- package/dist/db/queries/test-stack.js +1 -1
- package/dist/db/reference-data.js +3 -5
- package/dist/db/table-builder.js +6 -4
- package/dist/db/table-introspection.d.ts +16 -0
- package/dist/db/table-introspection.js +76 -0
- package/dist/db/tenant-db.d.ts +3 -2
- package/dist/db/tenant-db.js +4 -10
- package/dist/derivatives/variant-key.js +2 -1
- package/dist/engine/boot-validator/entity-list-screens.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +1 -1
- package/dist/engine/boot-validator/screens.js +2 -1
- package/dist/engine/changeset-changes.js +3 -3
- package/dist/engine/define-handler.js +3 -6
- package/dist/engine/embedded-derived.js +10 -6
- package/dist/engine/extensions/escape-hatch-usage.js +2 -1
- package/dist/engine/factories.js +5 -6
- package/dist/engine/feature-ast/extractors/events.js +43 -33
- package/dist/engine/feature-ast/patcher.d.ts +3 -3
- package/dist/engine/feature-ast/patcher.js +3 -3
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -8
- package/dist/engine/feature-ast/pattern-change-schema.js +3 -3
- package/dist/engine/feature-ast/patterns.d.ts +3 -3
- package/dist/engine/feature-ast/render.js +6 -3
- package/dist/engine/feature-changelog.d.ts +2 -1
- package/dist/engine/feature-changelog.js +8 -6
- package/dist/engine/feature-config-events-jobs.d.ts +17 -7
- package/dist/engine/feature-config-events-jobs.js +20 -5
- package/dist/engine/feature-ui-extensions.js +2 -3
- package/dist/engine/index.d.ts +2 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/ownership.js +7 -20
- package/dist/engine/pattern-library/mixed-schemas.js +0 -1
- package/dist/engine/registry-ingest.d.ts +2 -1
- package/dist/engine/registry-ingest.js +85 -2
- package/dist/engine/registry-validate.d.ts +1 -0
- package/dist/engine/registry-validate.js +38 -8
- package/dist/engine/registry.js +2 -1
- package/dist/engine/schema-builder.js +1 -3
- package/dist/engine/soft-delete-cleanup.js +2 -1
- package/dist/engine/steps/call-feature.js +1 -3
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -13
- package/dist/engine/template-variables.d.ts +29 -0
- package/dist/engine/template-variables.js +77 -0
- package/dist/engine/tier-resolver-extension.js +2 -3
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/engine/validate-projection-allowlist.js +12 -13
- package/dist/engine/where-rule-lint.js +8 -7
- package/dist/errors/reasons.d.ts +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/files/file-routes.js +1 -1
- package/dist/files/local-provider.js +2 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/mail-registry.d.ts +2 -0
- package/dist/i18n/mail-registry.js +7 -0
- package/dist/jobs/job-runner.js +5 -5
- package/dist/pipeline/dispatch-query.js +2 -3
- package/dist/pipeline/dispatch-shared.js +45 -14
- package/dist/pipeline/event-dispatcher-delivery.js +2 -3
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/member-read-only-transaction.js +1 -1
- package/dist/pipeline/notify-scope.d.ts +13 -0
- package/dist/pipeline/notify-scope.js +32 -0
- package/dist/pipeline/system-hooks.js +2 -1
- package/dist/pipeline/system-identity-switch.js +2 -2
- package/dist/search/meilisearch-adapter.js +3 -4
- package/dist/secrets/stored-envelope.js +7 -7
- package/dist/stack/push-entity-projection-tables.js +2 -3
- package/dist/stack/table-helpers.js +10 -26
- package/dist/stack/test-stack.js +2 -5
- package/dist/testing/multipart-helper.d.ts +0 -6
- package/dist/testing/multipart-helper.js +5 -5
- package/dist/testing/without-ambient-temporal.js +4 -4
- package/dist/utils/is-plain-object.d.ts +1 -2
- package/dist/utils/is-plain-object.js +1 -4
- package/package.json +4 -4
- package/src/changes.json +44 -0
package/dist/engine/factories.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
// `personal: { of: "id" }` means the row itself is the subject, not a
|
|
2
3
|
// referenced user — resolves to `recordOwned` instead of `userOwned`.
|
|
3
4
|
const RECORD_OWNER_FIELD = "id";
|
|
@@ -75,15 +76,13 @@ function isNonEmptyString(value) {
|
|
|
75
76
|
// Fail-closed stance gate (kumiko-framework#2921) for the two text-shaped
|
|
76
77
|
// factories — runs before expandPersonalAnnotations so an untyped JS caller
|
|
77
78
|
// (no compiler to stop it) can't slip an unannotated text field past the
|
|
78
|
-
// type layer either.
|
|
79
|
-
//
|
|
80
|
-
//
|
|
79
|
+
// type layer either. The typed signature above already rejects a
|
|
80
|
+
// missing/malformed shape at compile time; this re-checks the same shape at
|
|
81
|
+
// runtime, hence the nullable parameter.
|
|
81
82
|
function assertTextFieldStance(factoryName, overrides) {
|
|
82
83
|
if (overrides === null || typeof overrides !== "object") {
|
|
83
84
|
throw missingTextFieldStanceError(factoryName);
|
|
84
85
|
}
|
|
85
|
-
// @cast-boundary runtime-guard — re-validated field by field below before
|
|
86
|
-
// any value is trusted.
|
|
87
86
|
const { personal, find, reason } = overrides;
|
|
88
87
|
if (personal === undefined || personal === null) {
|
|
89
88
|
throw missingTextFieldStanceError(factoryName);
|
|
@@ -106,7 +105,7 @@ function assertTextFieldStance(factoryName, overrides) {
|
|
|
106
105
|
throw missingTextFieldStanceError(factoryName);
|
|
107
106
|
}
|
|
108
107
|
const validFinds = TEXT_FIELD_STANCE_FIND_VALUES[factoryName];
|
|
109
|
-
if (typeof find !== "string" || !validFinds
|
|
108
|
+
if (typeof find !== "string" || !isOneOf(validFinds, find)) {
|
|
110
109
|
throw missingTextFieldStanceError(factoryName);
|
|
111
110
|
}
|
|
112
111
|
}
|
|
@@ -200,37 +200,45 @@ export function extractNotification(call, sourceFile) {
|
|
|
200
200
|
return fail("notification", sourceLocationFromNode(call, sourceFile), "second argument must be an inline definition object");
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
?.getInitializer()
|
|
207
|
-
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
208
|
-
if (!triggerObj) {
|
|
209
|
-
return fail("notification", sourceLocationFromNode(call, sourceFile), "missing or non-object `trigger` property");
|
|
210
|
-
}
|
|
211
|
-
const onInit = triggerObj
|
|
212
|
-
.getProperty("on")
|
|
213
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
214
|
-
?.getInitializer();
|
|
215
|
-
const onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
216
|
-
if (!onName) {
|
|
217
|
-
return fail("notification", sourceLocationFromNode(call, sourceFile), "trigger.on must be a string literal or inline { name } object");
|
|
203
|
+
const triggerProp = defObj.getProperty("trigger");
|
|
204
|
+
if (!triggerProp && (defObj.getProperty("recipient") || defObj.getProperty("data"))) {
|
|
205
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient and data require a `trigger`");
|
|
218
206
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
207
|
+
let onName;
|
|
208
|
+
let recipientFn;
|
|
209
|
+
let dataFn;
|
|
210
|
+
if (triggerProp) {
|
|
211
|
+
const triggerObj = triggerProp
|
|
212
|
+
.asKind(SyntaxKind.PropertyAssignment)
|
|
213
|
+
?.getInitializer()
|
|
214
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
215
|
+
if (!triggerObj) {
|
|
216
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "non-object `trigger` property");
|
|
217
|
+
}
|
|
218
|
+
const onInit = triggerObj
|
|
219
|
+
.getProperty("on")
|
|
220
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
221
|
+
?.getInitializer();
|
|
222
|
+
onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
223
|
+
if (!onName) {
|
|
224
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "trigger.on must be a string literal or inline { name } object");
|
|
225
|
+
}
|
|
226
|
+
const recipientInit = defObj
|
|
227
|
+
.getProperty("recipient")
|
|
228
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
229
|
+
?.getInitializer();
|
|
230
|
+
recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
|
|
231
|
+
if (!recipientFn) {
|
|
232
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "recipient must be an inline arrow function or function expression");
|
|
233
|
+
}
|
|
234
|
+
const dataInit = defObj
|
|
235
|
+
.getProperty("data")
|
|
236
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
237
|
+
?.getInitializer();
|
|
238
|
+
dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
|
|
239
|
+
if (!dataFn) {
|
|
240
|
+
return fail("notification", sourceLocationFromNode(call, sourceFile), "data must be an inline arrow function or function expression");
|
|
241
|
+
}
|
|
234
242
|
}
|
|
235
243
|
let templates;
|
|
236
244
|
const templatesObj = defObj
|
|
@@ -257,9 +265,11 @@ export function extractNotification(call, sourceFile) {
|
|
|
257
265
|
kind: "notification",
|
|
258
266
|
source: sourceLocationFromNode(call, sourceFile),
|
|
259
267
|
notificationName,
|
|
260
|
-
trigger: { on: onName },
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
...(onName !== undefined && { trigger: { on: onName } }),
|
|
269
|
+
...(recipientFn !== undefined && {
|
|
270
|
+
recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
|
|
271
|
+
}),
|
|
272
|
+
...(dataFn !== undefined && { dataBody: sourceLocationFromNode(dataFn, sourceFile) }),
|
|
263
273
|
...(templates !== undefined && { templates }),
|
|
264
274
|
});
|
|
265
275
|
}
|
|
@@ -69,11 +69,11 @@ export type AddJobArgs = {
|
|
|
69
69
|
};
|
|
70
70
|
export type AddNotificationArgs = {
|
|
71
71
|
readonly name: string;
|
|
72
|
-
readonly trigger
|
|
72
|
+
readonly trigger?: {
|
|
73
73
|
readonly on: string;
|
|
74
74
|
};
|
|
75
|
-
readonly recipientSource
|
|
76
|
-
readonly dataSource
|
|
75
|
+
readonly recipientSource?: string;
|
|
76
|
+
readonly dataSource?: string;
|
|
77
77
|
readonly templates?: Readonly<Record<string, string>>;
|
|
78
78
|
};
|
|
79
79
|
export type AddHttpRouteArgs = {
|
|
@@ -205,9 +205,9 @@ export function createFeaturePatcher(sourceFile) {
|
|
|
205
205
|
kind: "notification",
|
|
206
206
|
source: SYNTHETIC_LOC,
|
|
207
207
|
notificationName: name,
|
|
208
|
-
trigger,
|
|
209
|
-
recipientBody: rawLoc(recipientSource),
|
|
210
|
-
dataBody: rawLoc(dataSource),
|
|
208
|
+
...(trigger !== undefined && { trigger }),
|
|
209
|
+
...(recipientSource !== undefined && { recipientBody: rawLoc(recipientSource) }),
|
|
210
|
+
...(dataSource !== undefined && { dataBody: rawLoc(dataSource) }),
|
|
211
211
|
...(templateLocs !== undefined && { templates: templateLocs }),
|
|
212
212
|
});
|
|
213
213
|
},
|
|
@@ -366,11 +366,11 @@ export declare const PATTERN_SCHEMAS_BY_KIND: {
|
|
|
366
366
|
kind: z.ZodLiteral<"notification">;
|
|
367
367
|
source: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
|
|
368
368
|
notificationName: z.ZodString;
|
|
369
|
-
trigger: z.ZodObject<{
|
|
369
|
+
trigger: z.ZodOptional<z.ZodObject<{
|
|
370
370
|
on: z.ZodString;
|
|
371
|
-
}, z.core.$strict
|
|
372
|
-
recipientBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
373
|
-
dataBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
371
|
+
}, z.core.$strict>>;
|
|
372
|
+
recipientBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
373
|
+
dataBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
374
374
|
templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>>;
|
|
375
375
|
}, z.core.$strict>;
|
|
376
376
|
authClaims: z.ZodObject<{
|
|
@@ -883,11 +883,11 @@ export declare const patternSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
883
883
|
kind: z.ZodLiteral<"notification">;
|
|
884
884
|
source: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>;
|
|
885
885
|
notificationName: z.ZodString;
|
|
886
|
-
trigger: z.ZodObject<{
|
|
886
|
+
trigger: z.ZodOptional<z.ZodObject<{
|
|
887
887
|
on: z.ZodString;
|
|
888
|
-
}, z.core.$strict
|
|
889
|
-
recipientBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
890
|
-
dataBody: z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown
|
|
888
|
+
}, z.core.$strict>>;
|
|
889
|
+
recipientBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
890
|
+
dataBody: z.ZodOptional<z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>;
|
|
891
891
|
templates: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<SourceLocation, unknown, z.core.$ZodTypeInternals<SourceLocation, unknown>>>>;
|
|
892
892
|
}, z.core.$strict>, z.ZodObject<{
|
|
893
893
|
kind: z.ZodLiteral<"authClaims">;
|
|
@@ -562,9 +562,9 @@ const notificationSchema = z
|
|
|
562
562
|
kind: z.literal("notification"),
|
|
563
563
|
source: sourceFieldSchema,
|
|
564
564
|
notificationName: z.string(),
|
|
565
|
-
trigger: z.object({ on: z.string() }).strict(),
|
|
566
|
-
recipientBody: sourceBodySchema,
|
|
567
|
-
dataBody: sourceBodySchema,
|
|
565
|
+
trigger: z.object({ on: z.string() }).strict().optional(),
|
|
566
|
+
recipientBody: sourceBodySchema.optional(),
|
|
567
|
+
dataBody: sourceBodySchema.optional(),
|
|
568
568
|
templates: z.record(z.string(), sourceBodySchema).optional(),
|
|
569
569
|
})
|
|
570
570
|
.strict();
|
|
@@ -190,11 +190,11 @@ export type NotificationPattern = {
|
|
|
190
190
|
readonly kind: "notification";
|
|
191
191
|
readonly source: SourceLocation;
|
|
192
192
|
readonly notificationName: string;
|
|
193
|
-
readonly trigger
|
|
193
|
+
readonly trigger?: {
|
|
194
194
|
readonly on: string;
|
|
195
195
|
};
|
|
196
|
-
readonly recipientBody
|
|
197
|
-
readonly dataBody
|
|
196
|
+
readonly recipientBody?: SourceLocation;
|
|
197
|
+
readonly dataBody?: SourceLocation;
|
|
198
198
|
readonly templates?: Readonly<Record<string, SourceLocation>>;
|
|
199
199
|
};
|
|
200
200
|
export type AuthClaimsPattern = {
|
|
@@ -497,9 +497,12 @@ function renderJob(p) {
|
|
|
497
497
|
function renderNotification(p) {
|
|
498
498
|
const lines = ["r.notification({"];
|
|
499
499
|
lines.push(` name: ${JSON.stringify(p.notificationName)},`);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
500
|
+
if (p.trigger)
|
|
501
|
+
lines.push(` trigger: { on: ${JSON.stringify(p.trigger.on)} },`);
|
|
502
|
+
if (p.recipientBody)
|
|
503
|
+
lines.push(` recipient: ${p.recipientBody.raw},`);
|
|
504
|
+
if (p.dataBody)
|
|
505
|
+
lines.push(` data: ${p.dataBody.raw},`);
|
|
503
506
|
if (p.templates && Object.keys(p.templates).length > 0) {
|
|
504
507
|
lines.push(" templates: {");
|
|
505
508
|
for (const [k, loc] of Object.entries(p.templates)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
1
|
+
export declare const CHANGELOG_TYPES: readonly ["breaking", "improvement", "fix"];
|
|
2
|
+
export type ChangelogType = (typeof CHANGELOG_TYPES)[number];
|
|
2
3
|
export type ChangelogEntry = {
|
|
3
4
|
readonly version: string;
|
|
4
5
|
readonly type: ChangelogType;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
//
|
|
6
6
|
// File I/O stays in the CLI (`bin/commands/upgrade.ts`) — this module is
|
|
7
7
|
// pure parse/validate so engine stays off the node:fs allowlist.
|
|
8
|
+
import { isOneOf, isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
9
|
+
export const CHANGELOG_TYPES = ["breaking", "improvement", "fix"];
|
|
8
10
|
/** Parse a changes.json body. Callers own file I/O. */
|
|
9
11
|
export function parseFeatureChangelog(raw, featureName) {
|
|
10
12
|
try {
|
|
@@ -24,16 +26,16 @@ export function parseFeatureChangelog(raw, featureName) {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
function isChangelogEntry(value) {
|
|
27
|
-
if (
|
|
29
|
+
if (!isPlainObject(value))
|
|
28
30
|
return false;
|
|
29
|
-
|
|
30
|
-
if (typeof obj["version"] !== "string")
|
|
31
|
+
if (typeof value["version"] !== "string")
|
|
31
32
|
return false;
|
|
32
|
-
if (!
|
|
33
|
+
if (!isOneOf(CHANGELOG_TYPES, value["type"]))
|
|
33
34
|
return false;
|
|
34
|
-
if (typeof
|
|
35
|
+
if (typeof value["title"] !== "string")
|
|
35
36
|
return false;
|
|
36
|
-
if (
|
|
37
|
+
if (value["manualAfterCodemod"] !== undefined &&
|
|
38
|
+
typeof value["manualAfterCodemod"] !== "boolean") {
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
39
41
|
return true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type * as z from "zod";
|
|
2
2
|
import { type ZodType } from "zod";
|
|
3
3
|
import type { FeatureBuilderState } from "./feature-builder-state.js";
|
|
4
|
-
import type { AuthClaimsFn, ClaimKeyHandle, ClaimKeyType, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigSeedDef, DeclarativeEventMigration, EventDef, EventPiiStance, EventUpcastFn, JobDefinition, JobHandlerFn, MetricOptions, NameOrRef, NotificationDataFn, NotificationRecipientFn, NotificationTemplateFn, QualifiedEventName, SecretKeyHandle, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, TranslationsDef } from "./types/index.js";
|
|
4
|
+
import type { AuthClaimsFn, ClaimKeyHandle, ClaimKeyType, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigSeedDef, DeclarativeEventMigration, EventDef, EventPiiStance, EventUpcastFn, JobDefinition, JobHandlerFn, MetricOptions, NameOrRef, NotificationDataFn, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, QualifiedEventName, SecretKeyHandle, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, TemplateVariables, TranslationsDef } from "./types/index.js";
|
|
5
5
|
export declare function buildConfigEventsJobsMethods<TName extends string>(state: FeatureBuilderState, name: TName): {
|
|
6
6
|
config: {
|
|
7
7
|
<T extends ConfigKeyType>(keyName: string, def: ConfigKeyDefinition<T>): ConfigKeyHandle<T>;
|
|
@@ -14,19 +14,29 @@ export declare function buildConfigEventsJobsMethods<TName extends string>(state
|
|
|
14
14
|
notification(notificationNameOrDefinition: string | ({
|
|
15
15
|
readonly name: string;
|
|
16
16
|
} & {
|
|
17
|
-
readonly trigger
|
|
17
|
+
readonly trigger?: {
|
|
18
18
|
readonly on: NameOrRef;
|
|
19
19
|
};
|
|
20
|
-
readonly recipient
|
|
21
|
-
readonly data
|
|
20
|
+
readonly recipient?: NotificationRecipientFn;
|
|
21
|
+
readonly data?: NotificationDataFn;
|
|
22
22
|
readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
|
|
23
|
+
readonly variables?: TemplateVariables;
|
|
24
|
+
readonly record?: NotificationRecordMapping;
|
|
25
|
+
readonly template?: Readonly<Record<string, NotificationTemplateBody>>;
|
|
26
|
+
readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
|
|
27
|
+
readonly localizedTemplate?: () => NotificationLocalizedTemplate;
|
|
23
28
|
}), definition?: {
|
|
24
|
-
readonly trigger
|
|
29
|
+
readonly trigger?: {
|
|
25
30
|
readonly on: NameOrRef;
|
|
26
31
|
};
|
|
27
|
-
readonly recipient
|
|
28
|
-
readonly data
|
|
32
|
+
readonly recipient?: NotificationRecipientFn;
|
|
33
|
+
readonly data?: NotificationDataFn;
|
|
29
34
|
readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
|
|
35
|
+
readonly variables?: TemplateVariables;
|
|
36
|
+
readonly record?: NotificationRecordMapping;
|
|
37
|
+
readonly template?: Readonly<Record<string, NotificationTemplateBody>>;
|
|
38
|
+
readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
|
|
39
|
+
readonly localizedTemplate?: () => NotificationLocalizedTemplate;
|
|
30
40
|
}): void;
|
|
31
41
|
translations(def: TranslationsDef): void;
|
|
32
42
|
defineEvent: <const TInner extends string, TPayload>(eventName: TInner, schema: ZodType<TPayload>, options: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHatchGrantsProblem, normalizeEventPiiSubject, } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { ZodObject } from "zod";
|
|
3
4
|
import { isValidIanaTimeZone } from "../time/iana.js";
|
|
4
5
|
import { resolveName } from "./handler-helpers.js";
|
|
@@ -197,10 +198,25 @@ export function buildConfigEventsJobsMethods(state, name) {
|
|
|
197
198
|
: splitNamedDefinition(notificationNameOrDefinition);
|
|
198
199
|
state.notifications[notificationName] = {
|
|
199
200
|
name: notificationName,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
...(resolvedDefinition.trigger !== undefined
|
|
202
|
+
? { trigger: { on: resolveName(resolvedDefinition.trigger.on) } }
|
|
203
|
+
: {}),
|
|
204
|
+
...(resolvedDefinition.recipient !== undefined
|
|
205
|
+
? { recipient: resolvedDefinition.recipient }
|
|
206
|
+
: {}),
|
|
207
|
+
...(resolvedDefinition.data !== undefined ? { data: resolvedDefinition.data } : {}),
|
|
203
208
|
templates: resolvedDefinition.templates,
|
|
209
|
+
...(resolvedDefinition.variables !== undefined
|
|
210
|
+
? { variables: resolvedDefinition.variables }
|
|
211
|
+
: {}),
|
|
212
|
+
...(resolvedDefinition.record !== undefined ? { record: resolvedDefinition.record } : {}),
|
|
213
|
+
...(resolvedDefinition.template !== undefined
|
|
214
|
+
? { template: resolvedDefinition.template }
|
|
215
|
+
: {}),
|
|
216
|
+
...(resolvedDefinition.meta !== undefined ? { meta: resolvedDefinition.meta } : {}),
|
|
217
|
+
...(resolvedDefinition.localizedTemplate !== undefined
|
|
218
|
+
? { localizedTemplate: resolvedDefinition.localizedTemplate }
|
|
219
|
+
: {}),
|
|
204
220
|
};
|
|
205
221
|
},
|
|
206
222
|
translations(def) {
|
|
@@ -366,10 +382,9 @@ function compileEventMigration(spec) {
|
|
|
366
382
|
renameTargets.set(to, from);
|
|
367
383
|
}
|
|
368
384
|
return (payload) => {
|
|
369
|
-
if (
|
|
385
|
+
if (!isPlainObject(payload)) {
|
|
370
386
|
throw new Error("Declarative event migration expects an object payload");
|
|
371
387
|
}
|
|
372
|
-
// @cast-boundary parse — payload is guarded as a plain object above
|
|
373
388
|
const next = { ...payload };
|
|
374
389
|
for (const [from, to] of Object.entries(spec.rename ?? {})) {
|
|
375
390
|
if (from in next) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHatchGrantsProblem } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { WEBSOCKET_MAX_CONNECTIONS_PER_USER_LIMIT, WEBSOCKET_MAX_PAYLOAD_BYTES, WEBSOCKET_ROUTE_PATH_PREFIX, } from "@cosmicdrift/kumiko-types/websocket-route";
|
|
3
4
|
import { bindHookEscapeHatchGrant } from "../pipeline/system-identity-switch.js";
|
|
4
5
|
import { LifecycleHookTypes } from "./constants.js";
|
|
@@ -209,9 +210,7 @@ export function buildUiExtensionsMethods(state, name) {
|
|
|
209
210
|
// for every extension regardless of its typed hook shape.
|
|
210
211
|
const escapeHatch = optionsBag?.["escapeHatch"];
|
|
211
212
|
if (escapeHatch !== undefined) {
|
|
212
|
-
const reason =
|
|
213
|
-
? escapeHatch.reason
|
|
214
|
-
: undefined;
|
|
213
|
+
const reason = isPlainObject(escapeHatch) ? escapeHatch["reason"] : undefined;
|
|
215
214
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
216
215
|
throw new Error(`[Feature ${name}] r.useExtension("${extensionName}", "${resolvedEntityName}", ...) declares an invalid { escapeHatch } — ` +
|
|
217
216
|
`must be { reason: "<non-empty string>" } explaining why this usage needs unfiltered db access.`);
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -77,10 +77,11 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
|
|
|
77
77
|
export { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-step-error.js";
|
|
78
78
|
export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
79
79
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
80
|
+
export { assertNotificationVariables, templateVariable, templateVariablesToZod, } from "./template-variables.js";
|
|
80
81
|
export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
|
|
81
82
|
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, type TransferAdjacency, type TransferEdge, type TransferEdgeLink, } from "./transfer-adjacency.js";
|
|
82
83
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
|
83
|
-
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
84
|
+
export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardDateParam, DashboardFeedPanel, DashboardFilterDefinition, DashboardI18nText, DashboardListPanel, DashboardMoneyParam, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DashboardText, DashboardTextParam, DashboardValueFormat, DateFieldDef, DecimalFieldDef, DeleteContext, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldPrefill, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EmbeddedListEditSpec, EntityDefinition, EntityEditAction, EntityEditActionPlacement, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyDefinition, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TenantId, TenantWriteTarget, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
|
|
84
85
|
export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HookPhases, HTTP_ERROR_CODE_PATTERN, isAgentVisibleScreen, isDeliveryErrorCode, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
|
|
85
86
|
export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
|
|
86
87
|
export { runValidation } from "./validation.js";
|
package/dist/engine/index.js
CHANGED
|
@@ -70,6 +70,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
|
|
|
70
70
|
export { unwrapWorkflowStepError, WorkflowStepError } from "./steps/workflow-step-error.js";
|
|
71
71
|
export { WORKFLOW_SYSTEM_EVENT_DEFS } from "./steps/workflow-system-events.js";
|
|
72
72
|
export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
|
|
73
|
+
export { assertNotificationVariables, templateVariable, templateVariablesToZod, } from "./template-variables.js";
|
|
73
74
|
export { findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, } from "./tier-resolver-extension.js";
|
|
74
75
|
export { findOverDeepTransferChain, resolveTransferAdjacency as resolveEntityTransferAdjacency, } from "./transfer-adjacency.js";
|
|
75
76
|
export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
|
package/dist/engine/ownership.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
//
|
|
17
17
|
// Construction: use the `from(ref, column?)` helper. It returns a FromRule
|
|
18
18
|
// ready to drop into an access map.
|
|
19
|
-
import {
|
|
19
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
20
20
|
import { toSnakeCase } from "../db/table-builder.js";
|
|
21
|
+
import { columnSqlNameOf, schemaTableName } from "../db/table-introspection.js";
|
|
21
22
|
import { assertQualifiedWhereFragment, tableColumnSqlNames } from "./where-rule-lint.js";
|
|
22
23
|
// Parse an OwnershipRef into kind + resolved path + default column.
|
|
23
24
|
// Throws on malformed input so the error surfaces at `from()`-call-site (in
|
|
@@ -185,32 +186,18 @@ export function userCanCreateFieldRow(user, accessMap, newRow) {
|
|
|
185
186
|
const PASS_CLAUSE = { kind: "pass" };
|
|
186
187
|
const EMPTY_CLAUSE = { kind: "empty" };
|
|
187
188
|
export function tableNameOf(table) {
|
|
188
|
-
|
|
189
|
-
const sym = table[KUMIKO_NAME_SYMBOL];
|
|
190
|
-
if (typeof sym === "string")
|
|
191
|
-
return sym;
|
|
192
|
-
}
|
|
193
|
-
return "<unknown>";
|
|
189
|
+
return schemaTableName(table) ?? "<unknown>";
|
|
194
190
|
}
|
|
195
191
|
// Resolve a JS-field name on the table to its underlying SQL column name.
|
|
196
|
-
// Drizzle tables carry the mapping under Symbol.for("kumiko:schema:Columns");
|
|
197
|
-
// we read it without importing drizzle-orm at runtime.
|
|
198
192
|
function columnSqlName(table, field) {
|
|
199
193
|
if (table === null || typeof table !== "object")
|
|
200
194
|
return null;
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
if (col && typeof col === "object") {
|
|
205
|
-
const nameVal = col["name"];
|
|
206
|
-
if (typeof nameVal === "string")
|
|
207
|
-
return nameVal;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
195
|
+
const handleName = columnSqlNameOf(table, field);
|
|
196
|
+
if (handleName !== undefined)
|
|
197
|
+
return handleName;
|
|
210
198
|
// Field may already be the SQL column name on plain objects (tests, etc.).
|
|
211
|
-
if (table[field] !== undefined)
|
|
199
|
+
if (isPlainObject(table) && table[field] !== undefined)
|
|
212
200
|
return toSnakeCase(field);
|
|
213
|
-
}
|
|
214
201
|
return null;
|
|
215
202
|
}
|
|
216
203
|
function quoteIdent(name) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { RegistryState } from "./registry-state.js";
|
|
2
|
-
import type { FeatureDefinition } from "./types/index.js";
|
|
2
|
+
import type { FeatureDefinition, NotificationDefinition } from "./types/index.js";
|
|
3
3
|
export declare function populateFeatureCore(state: RegistryState, feature: FeatureDefinition): void;
|
|
4
4
|
export declare function populateHandlers(state: RegistryState, feature: FeatureDefinition): void;
|
|
5
5
|
export declare function populateConfigKeys(state: RegistryState, feature: FeatureDefinition): void;
|
|
6
|
+
export declare function validateNotificationTemplate(featureName: string, notificationName: string, notifDef: NotificationDefinition): void;
|
|
6
7
|
export declare function populateJobsAndNotifications(state: RegistryState, feature: FeatureDefinition): void;
|
|
7
8
|
export declare function populateEvents(state: RegistryState, feature: FeatureDefinition): void;
|
|
8
9
|
export declare function populateSystemEvents(state: RegistryState): void;
|
|
@@ -90,6 +90,86 @@ export function populateConfigKeys(state, feature) {
|
|
|
90
90
|
state.configKeyMap.set(qualifiedKey, keyDef);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
// Mirrors isDocumentReferenceName in bundled-features/template-resolver: the
|
|
94
|
+
// framework cannot import bundled-features, and both sides must accept the
|
|
95
|
+
// same placeholder names.
|
|
96
|
+
const TEMPLATE_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
97
|
+
const FORBIDDEN_TEMPLATE_VARIABLE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
98
|
+
// The first four mirror SYSTEM_PLACEHOLDERS in bundled-features/template-resolver: the
|
|
99
|
+
// renderer fills these itself, so a declared variable of the same name would
|
|
100
|
+
// be shadowed or shadow them.
|
|
101
|
+
const RESERVED_TEMPLATE_VARIABLE_NAMES = new Set([
|
|
102
|
+
"tenantName",
|
|
103
|
+
"signature",
|
|
104
|
+
"imprint",
|
|
105
|
+
"unsubscribeUrl",
|
|
106
|
+
// Handlebars helpers of the mail renderer win over context values of the
|
|
107
|
+
// same name, so a variable called like a helper would never render.
|
|
108
|
+
"link",
|
|
109
|
+
"money",
|
|
110
|
+
"date",
|
|
111
|
+
]);
|
|
112
|
+
const KEBAB_NOTIFICATION_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
113
|
+
function isTemplateVariableName(value) {
|
|
114
|
+
return (TEMPLATE_VARIABLE_NAME.test(value) &&
|
|
115
|
+
!value.split(".").some((segment) => FORBIDDEN_TEMPLATE_VARIABLE_SEGMENTS.has(segment)));
|
|
116
|
+
}
|
|
117
|
+
function notificationLabel(featureName, notificationName) {
|
|
118
|
+
return `Notification "${featureName}:notify:${notificationName}"`;
|
|
119
|
+
}
|
|
120
|
+
function assertTriggerGroupComplete(label, notifDef) {
|
|
121
|
+
if (notifDef.trigger !== undefined) {
|
|
122
|
+
const missing = [
|
|
123
|
+
notifDef.recipient === undefined ? "recipient" : undefined,
|
|
124
|
+
notifDef.data === undefined ? "data" : undefined,
|
|
125
|
+
].filter((name) => name !== undefined);
|
|
126
|
+
if (missing.length > 0) {
|
|
127
|
+
throw new Error(`${label} declares a trigger but no ${missing.join(" and ")}: trigger, recipient and data belong together`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// localizedTemplate exists because translations are only complete once every
|
|
132
|
+
// locale feature is built; the registry resolves it into the static template
|
|
133
|
+
// + meta that all downstream consumers (seeding, validation) already read.
|
|
134
|
+
function resolveLocalizedTemplate(featureName, notificationName, notifDef) {
|
|
135
|
+
const { localizedTemplate, ...rest } = notifDef;
|
|
136
|
+
if (localizedTemplate === undefined)
|
|
137
|
+
return rest;
|
|
138
|
+
if (rest.template !== undefined || rest.meta !== undefined) {
|
|
139
|
+
throw new Error(`${notificationLabel(featureName, notificationName)} declares localizedTemplate together with template/meta: pick one`);
|
|
140
|
+
}
|
|
141
|
+
const byLocale = Object.entries(localizedTemplate());
|
|
142
|
+
return {
|
|
143
|
+
...rest,
|
|
144
|
+
template: Object.fromEntries(byLocale.map(([locale, entry]) => [locale, { body: entry.body }])),
|
|
145
|
+
meta: Object.fromEntries(byLocale.map(([locale, entry]) => [locale, entry.meta])),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function validateNotificationTemplate(featureName, notificationName, notifDef) {
|
|
149
|
+
// skip: a notification without a template has nothing to validate
|
|
150
|
+
if (notifDef.template === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const where = notificationLabel(featureName, notificationName);
|
|
153
|
+
if (!KEBAB_NOTIFICATION_NAME.test(notificationName)) {
|
|
154
|
+
throw new Error(`${where} declares a template, so its name must be kebab-case (lowercase, digits, dashes): the name becomes the template slug`);
|
|
155
|
+
}
|
|
156
|
+
for (const locale of Object.keys(notifDef.template)) {
|
|
157
|
+
if (notifDef.meta?.[locale]?.subject === undefined) {
|
|
158
|
+
throw new Error(`${where} has a template for locale "${locale}" but no meta.${locale}.subject`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [name, def] of Object.entries(notifDef.variables ?? {})) {
|
|
162
|
+
if (RESERVED_TEMPLATE_VARIABLE_NAMES.has(name)) {
|
|
163
|
+
throw new Error(`${where} declares variable "${name}" which collides with a system placeholder (${[...RESERVED_TEMPLATE_VARIABLE_NAMES].join(", ")})`);
|
|
164
|
+
}
|
|
165
|
+
const names = def.type === "list" ? [name, ...Object.keys(def.item)] : [name];
|
|
166
|
+
for (const candidate of names) {
|
|
167
|
+
if (!isTemplateVariableName(candidate)) {
|
|
168
|
+
throw new Error(`${where} declares variable "${candidate}" which is not a valid placeholder name (letters, digits, underscore; no prototype segments)`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
93
173
|
// Jobs (runIn-pinned, duplicate-guarded) + notifications (trigger resolved later).
|
|
94
174
|
export function populateJobsAndNotifications(state, feature) {
|
|
95
175
|
// Jobs: scope:job:name
|
|
@@ -115,10 +195,13 @@ export function populateJobsAndNotifications(state, feature) {
|
|
|
115
195
|
// Notifications: scope:notify:name
|
|
116
196
|
for (const [name, notifDef] of Object.entries(feature.notifications ?? {})) {
|
|
117
197
|
const qualifiedName = qualify(feature.name, "notify", name);
|
|
198
|
+
assertTriggerGroupComplete(notificationLabel(feature.name, name), notifDef);
|
|
199
|
+
const resolvedDef = resolveLocalizedTemplate(feature.name, name, notifDef);
|
|
200
|
+
validateNotificationTemplate(feature.name, name, resolvedDef);
|
|
118
201
|
state.notificationMap.set(qualifiedName, {
|
|
119
|
-
...
|
|
202
|
+
...resolvedDef,
|
|
120
203
|
name: qualifiedName,
|
|
121
|
-
trigger: { on:
|
|
204
|
+
...(resolvedDef.trigger !== undefined ? { trigger: { on: resolvedDef.trigger.on } } : {}),
|
|
122
205
|
});
|
|
123
206
|
state.notificationFeatureMap.set(qualifiedName, feature.name);
|
|
124
207
|
}
|
|
@@ -18,6 +18,7 @@ export declare function buildEntityEventUpcasterChains(state: RegistryState): vo
|
|
|
18
18
|
export declare function buildEventUpcasterChains(state: RegistryState, features: readonly FeatureDefinition[]): void;
|
|
19
19
|
export declare function validateProjectionApplyKeys(state: RegistryState): void;
|
|
20
20
|
export declare function validateRequiredFeatures(state: RegistryState, features: readonly FeatureDefinition[]): void;
|
|
21
|
+
export declare function validateNotificationRecordMappings(state: RegistryState): void;
|
|
21
22
|
export declare function resolveNotificationTriggersAndRegisterHooks(state: RegistryState): void;
|
|
22
23
|
export declare function validateLifecycleHookTargets(state: RegistryState): void;
|
|
23
24
|
export declare function validateEntityHookTargets(state: RegistryState, features: readonly FeatureDefinition[]): void;
|