@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
|
@@ -520,11 +520,41 @@ export function validateRequiredFeatures(state, features) {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
}
|
|
523
|
+
// The record mapping feeds template variables from an entity row, so every part
|
|
524
|
+
// of it is checked against the finished registry: entity, variables, field roots.
|
|
525
|
+
export function validateNotificationRecordMappings(state) {
|
|
526
|
+
for (const [qualifiedName, notifDef] of state.notificationMap) {
|
|
527
|
+
const { record, variables } = notifDef;
|
|
528
|
+
if (record === undefined)
|
|
529
|
+
continue;
|
|
530
|
+
const where = `Notification "${qualifiedName}" record mapping`;
|
|
531
|
+
const entity = state.entityMap.get(record.entity);
|
|
532
|
+
if (entity === undefined) {
|
|
533
|
+
throw new Error(`${where}: entity "${record.entity}" is not registered`);
|
|
534
|
+
}
|
|
535
|
+
if (variables === undefined) {
|
|
536
|
+
throw new Error(`${where} needs declared variables to map onto`);
|
|
537
|
+
}
|
|
538
|
+
for (const [variable, path] of Object.entries(record.map)) {
|
|
539
|
+
if (!Object.hasOwn(variables, variable)) {
|
|
540
|
+
throw new Error(`${where}: "${variable}" is not a declared variable`);
|
|
541
|
+
}
|
|
542
|
+
const root = path.split(".")[0] ?? "";
|
|
543
|
+
if (root !== "id" && !Object.hasOwn(entity.fields, root)) {
|
|
544
|
+
throw new Error(`${where}: path "${path}" for variable "${variable}" does not start with a field of entity "${record.entity}"`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
523
549
|
export function resolveNotificationTriggersAndRegisterHooks(state) {
|
|
524
550
|
// Resolve notification triggers and register postSave hooks
|
|
525
551
|
// Done after all features are registered so cross-feature triggers work
|
|
526
552
|
const allHandlerNames = allHandlerQns(state);
|
|
527
553
|
for (const [qualifiedName, notifDef] of state.notificationMap) {
|
|
554
|
+
const { trigger, recipient, data: buildData } = notifDef;
|
|
555
|
+
// Trigger-less notifications are only sent explicitly via ctx.notify.
|
|
556
|
+
if (trigger === undefined || recipient === undefined || buildData === undefined)
|
|
557
|
+
continue;
|
|
528
558
|
// Both maps are populated in lockstep — same key-set by construction.
|
|
529
559
|
const featureName = state.notificationFeatureMap.get(qualifiedName); // @cast-boundary engine-bridge
|
|
530
560
|
// I'll try the easy path first: if the trigger is already a fully qualified QN
|
|
@@ -532,13 +562,13 @@ export function resolveNotificationTriggersAndRegisterHooks(state) {
|
|
|
532
562
|
// as a write handler first (the common case), then as a query. If nothing
|
|
533
563
|
// matches by then, it was a typo and I'll say so.
|
|
534
564
|
let triggerOn;
|
|
535
|
-
if (allHandlerNames.has(
|
|
536
|
-
triggerOn =
|
|
565
|
+
if (allHandlerNames.has(trigger.on)) {
|
|
566
|
+
triggerOn = trigger.on;
|
|
537
567
|
}
|
|
538
568
|
else {
|
|
539
569
|
// Try as write handler first (most common), then query
|
|
540
|
-
const writeQn = qualify(featureName, "write",
|
|
541
|
-
const queryQn = qualify(featureName, "query",
|
|
570
|
+
const writeQn = qualify(featureName, "write", trigger.on);
|
|
571
|
+
const queryQn = qualify(featureName, "query", trigger.on);
|
|
542
572
|
if (allHandlerNames.has(writeQn)) {
|
|
543
573
|
triggerOn = writeQn;
|
|
544
574
|
}
|
|
@@ -546,9 +576,9 @@ export function resolveNotificationTriggersAndRegisterHooks(state) {
|
|
|
546
576
|
triggerOn = queryQn;
|
|
547
577
|
}
|
|
548
578
|
else {
|
|
549
|
-
throw new Error(`Notification "${qualifiedName}" triggers on "${
|
|
579
|
+
throw new Error(`Notification "${qualifiedName}" triggers on "${trigger.on}" ` +
|
|
550
580
|
`but no handler with that name exists. ` +
|
|
551
|
-
`Tried: "${
|
|
581
|
+
`Tried: "${trigger.on}", "${writeQn}", and "${queryQn}"`);
|
|
552
582
|
}
|
|
553
583
|
}
|
|
554
584
|
// Update the stored definition with resolved trigger
|
|
@@ -563,12 +593,12 @@ export function resolveNotificationTriggersAndRegisterHooks(state) {
|
|
|
563
593
|
context.log?.debug(`notification ${qualifiedName}: skipping — no notify function configured on context`);
|
|
564
594
|
return;
|
|
565
595
|
}
|
|
566
|
-
const to =
|
|
596
|
+
const to = recipient(result);
|
|
567
597
|
if (to === null) {
|
|
568
598
|
context.log?.debug(`notification ${qualifiedName}: skipping — recipient resolver returned null for result ${result.id}`);
|
|
569
599
|
return;
|
|
570
600
|
}
|
|
571
|
-
const data =
|
|
601
|
+
const data = buildData(result);
|
|
572
602
|
await context.notify(qualifiedName, { to, data });
|
|
573
603
|
},
|
|
574
604
|
});
|
package/dist/engine/registry.js
CHANGED
|
@@ -3,7 +3,7 @@ import { dedupeFeatures } from "./dedupe-features.js";
|
|
|
3
3
|
import { buildRegistryFacade } from "./registry-facade.js";
|
|
4
4
|
import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateSystemEvents, populateTranslations, } from "./registry-ingest.js";
|
|
5
5
|
import { createInitialState } from "./registry-state.js";
|
|
6
|
-
import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEntityEventUpcasterChains, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
|
|
6
|
+
import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEntityEventUpcasterChains, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateNotificationRecordMappings, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
|
|
7
7
|
// This is where the magic happens. By "magic" I mean: precomputed maps.
|
|
8
8
|
// I build everything once at boot (hooks, relations, searchable fields, ...)
|
|
9
9
|
// so nothing has to iterate over objects at runtime. O(1) instead of O(n*m).
|
|
@@ -41,6 +41,7 @@ export function createRegistry(rawFeatures) {
|
|
|
41
41
|
validateProjectionApplyKeys(state);
|
|
42
42
|
validateRequiredFeatures(state, features);
|
|
43
43
|
resolveNotificationTriggersAndRegisterHooks(state);
|
|
44
|
+
validateNotificationRecordMappings(state);
|
|
44
45
|
validateLifecycleHookTargets(state);
|
|
45
46
|
validateEntityHookTargets(state, features);
|
|
46
47
|
resolveJobTriggers(state);
|
|
@@ -358,9 +358,7 @@ function applyTotalsMatchRefinements(entity, schema) {
|
|
|
358
358
|
continue;
|
|
359
359
|
}
|
|
360
360
|
const sumMinor = rawRows.reduce((total, row) => {
|
|
361
|
-
const value = typeof row === "object" && row !== null
|
|
362
|
-
? row[subFieldName]
|
|
363
|
-
: undefined;
|
|
361
|
+
const value = typeof row === "object" && row !== null ? Reflect.get(row, subFieldName) : undefined;
|
|
364
362
|
return total + (typeof value === "number" ? value : 0);
|
|
365
363
|
}, 0);
|
|
366
364
|
if (sumMinor !== siblingMinor) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// table growth; irreversible event-log purging is data-retention's job
|
|
12
12
|
// (pruneEvents), a separate, consumer-lag-guarded path.
|
|
13
13
|
import { asEntityTableMeta, deleteMany } from "../db/query.js";
|
|
14
|
+
import { hasFieldProperty } from "../db/table-introspection.js";
|
|
14
15
|
import { purgeExpiredFileRefsWithBytes } from "../files/purge-expired-file-ref-bytes.js";
|
|
15
16
|
import { FILE_REF_ENTITY_NAME } from "../files/types.js";
|
|
16
17
|
import { SYSTEM_USER_ID } from "./system-user.js";
|
|
@@ -34,7 +35,7 @@ export const softDeleteGraceDaysConfig = {
|
|
|
34
35
|
bounds: { min: 1 },
|
|
35
36
|
};
|
|
36
37
|
function hasTenantIdColumn(table) {
|
|
37
|
-
return table
|
|
38
|
+
return hasFieldProperty(table, "tenantId");
|
|
38
39
|
}
|
|
39
40
|
// The entity declaration decides, not the column: a `tenancy: "global"` table
|
|
40
41
|
// (e.g. user) still carries a tenantId column (SYSTEM_TENANT_ID), yet no
|
|
@@ -18,9 +18,7 @@ defineStep({
|
|
|
18
18
|
// dispatcher's catch maps it back to a typed error response
|
|
19
19
|
// (e.g. validation_failed stays validation_failed, not a
|
|
20
20
|
// generic internal_error).
|
|
21
|
-
|
|
22
|
-
err.cause = result.error;
|
|
23
|
-
throw err;
|
|
21
|
+
throw new Error(`callFeature("${args.handler}") returned failure`, { cause: result.error });
|
|
24
22
|
}
|
|
25
23
|
return result.data;
|
|
26
24
|
},
|
|
@@ -10,25 +10,14 @@
|
|
|
10
10
|
// the owning feature. Aggregate-tables (registered via r.entity) are
|
|
11
11
|
// rejected by boot-validation — domain mutation MUST go through
|
|
12
12
|
// r.step.aggregate.*.
|
|
13
|
-
import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
14
13
|
import { extractTableName } from "../../db/index.js";
|
|
15
14
|
import { executeRawQuery } from "../../db/queries/raw-sql.js";
|
|
15
|
+
import { columnSqlNameOf } from "../../db/table-introspection.js";
|
|
16
16
|
import { tenantDbRunner } from "../../db/tenant-db-runner.js";
|
|
17
17
|
import { defineStep } from "../define-step.js";
|
|
18
18
|
import { resolveRequired } from "./_resolver-utils.js";
|
|
19
19
|
function resolveColumnName(table, field) {
|
|
20
|
-
|
|
21
|
-
return field;
|
|
22
|
-
const cols = table[KUMIKO_COLUMNS_SYMBOL];
|
|
23
|
-
if (typeof cols !== "object" || cols === null)
|
|
24
|
-
return field;
|
|
25
|
-
const col = cols[field];
|
|
26
|
-
if (typeof col === "object" && col !== null) {
|
|
27
|
-
const nameVal = col["name"];
|
|
28
|
-
if (typeof nameVal === "string")
|
|
29
|
-
return nameVal;
|
|
30
|
-
}
|
|
31
|
-
return field;
|
|
20
|
+
return columnSqlNameOf(table, field) ?? field;
|
|
32
21
|
}
|
|
33
22
|
function quoteIdent(name) {
|
|
34
23
|
return `"${name.replace(/"/g, '""')}"`;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type { Registry, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariables } from "./types/index.js";
|
|
3
|
+
type CommonOptions = {
|
|
4
|
+
readonly label?: string;
|
|
5
|
+
readonly required?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare const templateVariable: {
|
|
8
|
+
readonly text: (opts?: CommonOptions & {
|
|
9
|
+
readonly example?: string;
|
|
10
|
+
}) => TemplateScalarVariableDef;
|
|
11
|
+
readonly link: (opts?: CommonOptions & {
|
|
12
|
+
readonly example?: string;
|
|
13
|
+
}) => TemplateScalarVariableDef;
|
|
14
|
+
readonly money: (opts?: CommonOptions & {
|
|
15
|
+
readonly example?: number | {
|
|
16
|
+
readonly amount: number;
|
|
17
|
+
readonly currency: string;
|
|
18
|
+
};
|
|
19
|
+
}) => TemplateScalarVariableDef;
|
|
20
|
+
readonly date: (opts?: CommonOptions & {
|
|
21
|
+
readonly example?: string;
|
|
22
|
+
}) => TemplateScalarVariableDef;
|
|
23
|
+
readonly list: (item: Readonly<Record<string, TemplateScalarVariableDef>>, opts?: CommonOptions & {
|
|
24
|
+
readonly example?: readonly Readonly<Record<string, unknown>>[];
|
|
25
|
+
}) => TemplateListVariableDef;
|
|
26
|
+
};
|
|
27
|
+
export declare function templateVariablesToZod(variables: TemplateVariables): z.ZodObject;
|
|
28
|
+
export declare function assertNotificationVariables(registry: Pick<Registry, "getAllNotifications">, notificationType: string, data: Readonly<Record<string, unknown>> | undefined): void;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { InternalError } from "../errors/index.js";
|
|
3
|
+
// Builders keep declarations terse and the `type` discriminant out of app code.
|
|
4
|
+
export const templateVariable = {
|
|
5
|
+
text: (opts = {}) => ({
|
|
6
|
+
type: "text",
|
|
7
|
+
...opts,
|
|
8
|
+
}),
|
|
9
|
+
link: (opts = {}) => ({
|
|
10
|
+
type: "link",
|
|
11
|
+
...opts,
|
|
12
|
+
}),
|
|
13
|
+
money: (opts = {}) => ({ type: "money", ...opts }),
|
|
14
|
+
date: (opts = {}) => ({
|
|
15
|
+
type: "date",
|
|
16
|
+
...opts,
|
|
17
|
+
}),
|
|
18
|
+
list: (item, opts = {}) => ({ type: "list", item, ...opts }),
|
|
19
|
+
};
|
|
20
|
+
const LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
21
|
+
function isSafeLinkUrl(value) {
|
|
22
|
+
try {
|
|
23
|
+
return LINK_PROTOCOLS.has(new URL(value).protocol);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const linkSchema = z.string().refine(isSafeLinkUrl, "must be an http, https or mailto URL");
|
|
30
|
+
const moneySchema = z.union([
|
|
31
|
+
z.number().int(),
|
|
32
|
+
z.object({
|
|
33
|
+
amount: z.number().int(),
|
|
34
|
+
currency: z.string().regex(/^[A-Z]{3}$/),
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
37
|
+
const dateSchema = z.union([z.iso.date(), z.iso.datetime({ offset: true })]);
|
|
38
|
+
function scalarToZod(def) {
|
|
39
|
+
switch (def.type) {
|
|
40
|
+
case "text":
|
|
41
|
+
return z.string();
|
|
42
|
+
case "link":
|
|
43
|
+
return linkSchema;
|
|
44
|
+
case "money":
|
|
45
|
+
return moneySchema;
|
|
46
|
+
case "date":
|
|
47
|
+
return dateSchema;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function variableToZod(def) {
|
|
51
|
+
if (def.type !== "list")
|
|
52
|
+
return scalarToZod(def);
|
|
53
|
+
return z.array(z.object(Object.fromEntries(Object.entries(def.item).map(([name, item]) => [name, withOptionality(item)]))));
|
|
54
|
+
}
|
|
55
|
+
function withOptionality(def) {
|
|
56
|
+
const schema = variableToZod(def);
|
|
57
|
+
return def.required === true ? schema : schema.optional();
|
|
58
|
+
}
|
|
59
|
+
export function templateVariablesToZod(variables) {
|
|
60
|
+
return z.object(Object.fromEntries(Object.entries(variables).map(([name, def]) => [name, withOptionality(def)])));
|
|
61
|
+
}
|
|
62
|
+
// Validates only: the caller keeps passing the original data on, because
|
|
63
|
+
// parsing would strip keys the delivery adds itself (unsubscribeUrl, fromName).
|
|
64
|
+
export function assertNotificationVariables(registry, notificationType, data) {
|
|
65
|
+
const definition = registry.getAllNotifications().get(notificationType);
|
|
66
|
+
if (definition?.template !== undefined && definition.variables !== undefined) {
|
|
67
|
+
const parsed = templateVariablesToZod(definition.variables).safeParse(data ?? {});
|
|
68
|
+
if (!parsed.success) {
|
|
69
|
+
const issues = parsed.error.issues
|
|
70
|
+
.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
|
|
71
|
+
.join("; ");
|
|
72
|
+
throw new InternalError({
|
|
73
|
+
message: `notification "${notificationType}" has invalid variables: ${issues}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// **Apps ohne tier-engine:** wenn keine plugin registriert ist, framework
|
|
21
21
|
// macht nichts — `effectiveFeatures` bleibt undefined, alle features sind on.
|
|
22
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
22
23
|
/**
|
|
23
24
|
* Extension-name unter dem ein tier-resolver-plugin im registry registriert
|
|
24
25
|
* werden muss. Konstante damit `r.useExtension(TENANT_TIER_RESOLVER_EXT, ...)`
|
|
@@ -39,9 +40,7 @@ export const TENANT_TIER_RESOLVER_EXT = "tenantTierResolver";
|
|
|
39
40
|
* plugin shape — the only thing every implementation must provide is the
|
|
40
41
|
* `build` factory. */
|
|
41
42
|
export function isTierResolverPlugin(v) {
|
|
42
|
-
return (typeof v === "
|
|
43
|
-
v !== null &&
|
|
44
|
-
typeof v["build"] === "function");
|
|
43
|
+
return isPlainObject(v) && typeof v["build"] === "function";
|
|
45
44
|
}
|
|
46
45
|
export function findTierResolverUsage(features) {
|
|
47
46
|
for (const feature of features) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
|
|
2
|
-
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
|
|
2
|
+
export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
|
|
3
3
|
export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "@cosmicdrift/kumiko-types/define-handler";
|
|
4
4
|
export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
|
|
5
5
|
export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
|
|
@@ -7,7 +7,7 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
|
|
|
7
7
|
export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
|
|
8
8
|
export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
|
|
9
9
|
export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
|
|
10
|
-
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
10
|
+
export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
|
|
11
11
|
export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
|
|
12
12
|
export type { DeleteContext, EntityHookMap, HookMap, HookOwner, HookPhase, LifecycleHookFn, LifecycleResult, OwnedFn, PhasedHook, PostDeleteBatchHookFn, PostDeleteHookFn, PostQueryHookFn, PostSaveBatchHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SaveContext, SearchPayloadContributorFn, ValidationError, ValidationHookFn, } from "@cosmicdrift/kumiko-types/hooks";
|
|
13
13
|
export type { HttpRouteDefinition, HttpRouteHandler, HttpRouteHandlerDeps, HttpRouteMethod, } from "@cosmicdrift/kumiko-types/http-route";
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
// boot-time is surfaced cleanly; a quietly-different step-list is not
|
|
23
23
|
// caught by this validator. A future lint-rule will enforce the contract
|
|
24
24
|
// statically; today it lives in this comment + the StepBuilder doc.
|
|
25
|
-
import { KUMIKO_META_SYMBOL
|
|
25
|
+
import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
26
|
+
import { schemaTableName } from "../db/table-introspection.js";
|
|
26
27
|
import { getStep } from "./define-step.js";
|
|
27
28
|
import { buildPipelineSteps } from "./pipeline.js";
|
|
28
29
|
// Listed step-kinds whose `args.table` must be in the owning feature's
|
|
@@ -57,21 +58,19 @@ function* walkAllSteps(steps) {
|
|
|
57
58
|
function resolveTableNameFromStep(table) {
|
|
58
59
|
if (typeof table === "object" && table !== null) {
|
|
59
60
|
// Canonical meta under the unshadowable symbol — preferred path.
|
|
60
|
-
const meta = table
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
const meta = Reflect.get(table, KUMIKO_META_SYMBOL);
|
|
62
|
+
if (typeof meta === "object" &&
|
|
63
|
+
meta !== null &&
|
|
64
|
+
"tableName" in meta &&
|
|
65
|
+
typeof meta.tableName === "string") {
|
|
66
|
+
return meta.tableName;
|
|
65
67
|
}
|
|
66
68
|
// Plain meta (deriveEntityTableMeta / defineUnmanagedTable — no handle-spread).
|
|
67
|
-
if ("source" in table &&
|
|
68
|
-
|
|
69
|
-
typeof table["tableName"] === "string") {
|
|
70
|
-
return table["tableName"];
|
|
69
|
+
if ("source" in table && "tableName" in table && typeof table.tableName === "string") {
|
|
70
|
+
return table.tableName;
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (typeof name === "string")
|
|
72
|
+
const name = schemaTableName(table);
|
|
73
|
+
if (name !== undefined)
|
|
75
74
|
return name;
|
|
76
75
|
}
|
|
77
76
|
throw new Error(`validate-projection-allowlist: cannot resolve table name from ${String(table)}`);
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// parser) that catches the two shapes that always mean "this rule is
|
|
9
9
|
// broken": a bare column reference inside a subquery that shadows an outer
|
|
10
10
|
// column, and a literal self-comparison.
|
|
11
|
-
import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
12
11
|
import { toSnakeCase } from "../db/table-builder.js";
|
|
12
|
+
import { tableColumnsOf } from "../db/table-introspection.js";
|
|
13
13
|
const IDENT_SRC = `"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*`;
|
|
14
14
|
const QUALIFIED_SRC = `(?:${IDENT_SRC})(?:\\.(?:${IDENT_SRC}))?`;
|
|
15
15
|
const TOKEN_RE = new RegExp(IDENT_SRC, "g");
|
|
@@ -23,14 +23,15 @@ const QUALIFIED_PARSE_RE = new RegExp(`^(${IDENT_SRC})(?:\\.(${IDENT_SRC}))?$`);
|
|
|
23
23
|
export function tableColumnSqlNames(table) {
|
|
24
24
|
if (table === null || typeof table !== "object")
|
|
25
25
|
return new Set();
|
|
26
|
-
const cols = table
|
|
27
|
-
if (cols
|
|
26
|
+
const cols = tableColumnsOf(table);
|
|
27
|
+
if (cols) {
|
|
28
28
|
const names = new Set();
|
|
29
29
|
for (const col of Object.values(cols)) {
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if (typeof col === "object" &&
|
|
31
|
+
col !== null &&
|
|
32
|
+
"name" in col &&
|
|
33
|
+
typeof col.name === "string") {
|
|
34
|
+
names.add(col.name.toLowerCase());
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
return names;
|
package/dist/errors/reasons.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const FrameworkReasons: {
|
|
|
7
7
|
readonly systemIdentitySwitchDenied: "system_identity_switch_denied";
|
|
8
8
|
readonly identitySwitchDenied: "identity_switch_denied";
|
|
9
9
|
readonly tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin";
|
|
10
|
+
readonly notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast";
|
|
10
11
|
readonly unsafeAllTenantsDenied: "unsafe_all_tenants_denied";
|
|
11
12
|
readonly memberResolutionDenied: "member_resolution_denied";
|
|
12
13
|
readonly memberResolutionReadOnly: "member_resolution_read_only";
|
package/dist/errors/reasons.js
CHANGED
|
@@ -42,6 +42,8 @@ export const FrameworkReasons = {
|
|
|
42
42
|
identitySwitchDenied: "identity_switch_denied",
|
|
43
43
|
// AccessDeniedError: a payload carried tenantIdOverride but the caller is no SystemAdmin.
|
|
44
44
|
tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin",
|
|
45
|
+
// AccessDeniedError: ctx.notify was called with `to: { tenant }` naming a tenant other than the caller's.
|
|
46
|
+
notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast",
|
|
45
47
|
// AccessDeniedError: a handler/hook called ctx.queryProjection(..., { unsafeAllTenants: true })
|
|
46
48
|
// without an r.systemScope() feature or a declared escapeHatch.
|
|
47
49
|
unsafeAllTenantsDenied: "unsafe_all_tenants_denied",
|
|
@@ -74,7 +74,7 @@ export function readFilesRouteOptions(registry) {
|
|
|
74
74
|
if (exp && typeof exp === "object" && "routeOptions" in exp) {
|
|
75
75
|
const ro = exp.routeOptions;
|
|
76
76
|
if (ro && typeof ro === "object") {
|
|
77
|
-
// @cast-boundary feature-exports
|
|
77
|
+
// @cast-boundary feature-exports — typeof-checked read-back from untyped feature.exports
|
|
78
78
|
return ro;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -2,6 +2,7 @@ import { createReadStream, createWriteStream } from "node:fs";
|
|
|
2
2
|
import { mkdir, readdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
6
|
import { assertSafeStorageKey } from "./types.js";
|
|
6
7
|
// Local-filesystem backend — intended for dev + tests. Production deploys
|
|
7
8
|
// pick an object-store provider (S3/R2/…). mimeType is ignored here; the
|
|
@@ -112,7 +113,7 @@ export function createLocalProvider(basePath) {
|
|
|
112
113
|
entries = await readdir(walkRoot, { recursive: true, withFileTypes: true });
|
|
113
114
|
}
|
|
114
115
|
catch (err) {
|
|
115
|
-
const code = err
|
|
116
|
+
const code = errorCodeOf(err);
|
|
116
117
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
117
118
|
return [];
|
|
118
119
|
throw err;
|
package/dist/i18n/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Registry } from "../engine/types/index.js";
|
|
2
|
-
export { hasMailTranslations, mailT, registerMailTranslations, resolveMailLocale, } from "./mail-registry.js";
|
|
2
|
+
export { hasMailTranslationKey, hasMailTranslations, listMailLocales, mailT, registerMailTranslations, resolveMailLocale, } from "./mail-registry.js";
|
|
3
3
|
export { canonicalizeLocaleTag, DEFAULT_LOCALE, isValidLocaleTag, pickAcceptLanguage, resolveHeaderLocale, } from "./request-locale.js";
|
|
4
4
|
export { SETTINGS_HUB_I18N } from "./settings-hub-keys.js";
|
|
5
5
|
export type I18nOptions = {
|
package/dist/i18n/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveTranslationValue } from "../ui-types/plural.js";
|
|
2
|
-
export { hasMailTranslations, mailT, registerMailTranslations, resolveMailLocale, } from "./mail-registry.js";
|
|
2
|
+
export { hasMailTranslationKey, hasMailTranslations, listMailLocales, mailT, registerMailTranslations, resolveMailLocale, } from "./mail-registry.js";
|
|
3
3
|
export { canonicalizeLocaleTag, DEFAULT_LOCALE, isValidLocaleTag, pickAcceptLanguage, resolveHeaderLocale, } from "./request-locale.js";
|
|
4
4
|
export { SETTINGS_HUB_I18N } from "./settings-hub-keys.js";
|
|
5
5
|
export function createI18n(registry, options) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type TranslationValue } from "../ui-types/plural.js";
|
|
2
2
|
export declare function registerMailTranslations(locale: string, bundle: Readonly<Record<string, TranslationValue>>): void;
|
|
3
|
+
export declare function listMailLocales(): readonly string[];
|
|
4
|
+
export declare function hasMailTranslationKey(locale: string, key: string): boolean;
|
|
3
5
|
export declare function hasMailTranslations(locale: string): boolean;
|
|
4
6
|
export declare function mailT(locale: string, key: string, params?: Readonly<Record<string, string | number>>): string;
|
|
5
7
|
/** Locale key mailT would actually use (exact → root → en). Use for appUrl
|
|
@@ -4,6 +4,13 @@ export function registerMailTranslations(locale, bundle) {
|
|
|
4
4
|
const prev = tables.get(locale) ?? {};
|
|
5
5
|
tables.set(locale, { ...prev, ...bundle });
|
|
6
6
|
}
|
|
7
|
+
export function listMailLocales() {
|
|
8
|
+
return [...tables.keys()];
|
|
9
|
+
}
|
|
10
|
+
// Own key only, without the root/en fallback mailT applies.
|
|
11
|
+
export function hasMailTranslationKey(locale, key) {
|
|
12
|
+
return tables.get(locale)?.[key] !== undefined;
|
|
13
|
+
}
|
|
7
14
|
export function hasMailTranslations(locale) {
|
|
8
15
|
const root = locale.split("-")[0] ?? locale;
|
|
9
16
|
return tables.has(locale) || tables.has(root);
|
package/dist/jobs/job-runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { Queue, UnrecoverableError, Worker } from "bullmq";
|
|
3
4
|
import { Redis } from "ioredis";
|
|
4
5
|
import { requestContext } from "../api/request-context.js";
|
|
@@ -24,12 +25,11 @@ import { cronRepeatOptions } from "./cron-repeat-options.js";
|
|
|
24
25
|
// run proceeds without it rather than failing over what would be a framework
|
|
25
26
|
// bug, not untrusted input.
|
|
26
27
|
function isJobTriggerEvent(value) {
|
|
27
|
-
if (
|
|
28
|
+
if (!isPlainObject(value))
|
|
28
29
|
return false;
|
|
29
|
-
|
|
30
|
-
if (typeof candidate["id"] !== "string")
|
|
30
|
+
if (typeof value["id"] !== "string")
|
|
31
31
|
return false;
|
|
32
|
-
const headers =
|
|
32
|
+
const headers = value["headers"];
|
|
33
33
|
if (typeof headers !== "object" || headers === null)
|
|
34
34
|
return false;
|
|
35
35
|
return Object.values(headers).every((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean");
|
|
@@ -834,7 +834,7 @@ export function createJobRunner(options) {
|
|
|
834
834
|
// must resolve for jobs the same way they do for write-handlers, or an
|
|
835
835
|
// app-author job calling ctx.notify(...)/ctx.config(...) hits a TypeError
|
|
836
836
|
// at runtime the write-handler path never would (framework#1532).
|
|
837
|
-
const notify = context._notifyFactory?.(jobSystemUser, tenantId, selfRunner);
|
|
837
|
+
const notify = context._notifyFactory?.(jobSystemUser, tenantId, selfRunner, undefined, context._fileProviderResolver);
|
|
838
838
|
const configDb = context.db; // @cast-boundary db-operator
|
|
839
839
|
// Shared by the config accessor and ctx.derivatives below — both need the
|
|
840
840
|
// same tenant-scoped db, and building it twice would let the two calls
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
1
2
|
import { hasAccess } from "../engine/access.js";
|
|
2
3
|
import { tenantOverrideDenied } from "../engine/cross-tenant.js";
|
|
3
4
|
import { filterReadFields } from "../engine/field-access.js";
|
|
@@ -49,9 +50,7 @@ async function executeQueryInner(ctx, type, payload, user, origin, tx) {
|
|
|
49
50
|
// schema (and custom query schemas that opt in) carries `includeDeleted`,
|
|
50
51
|
// so other handlers never see the flag. Visibility filters still apply
|
|
51
52
|
// downstream (see HandlerContext.includeDeleted) — safe from raw input.
|
|
52
|
-
const includeDeleted =
|
|
53
|
-
parsed.data !== null &&
|
|
54
|
-
parsed.data["includeDeleted"] === true; // @cast-boundary validated-payload
|
|
53
|
+
const includeDeleted = propertyOf(parsed.data, "includeDeleted") === true;
|
|
55
54
|
// A resolved member (ctx.queryAsMember) runs in a Postgres READ ONLY transaction, not just the ctx surface below.
|
|
56
55
|
return isMemberResolutionPrincipal(user)
|
|
57
56
|
? runInMemberReadOnlyTransaction(ctx, tx, (readOnlyTx) => runQueryHandler(ctx, type, handler, parsed.data, includeDeleted, user, origin, readOnlyTx))
|