@cosmicdrift/kumiko-framework 0.365.0 → 0.366.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/AGENTS.md +2 -2
- package/agents/examples/access-control.md +32 -30
- package/agents/examples/anonymous-access-multitenant.md +14 -16
- package/agents/examples/anonymous-access.md +36 -43
- package/agents/examples/auth-foundation-providers.md +1 -1
- package/agents/examples/basic-entity.md +1 -1
- package/agents/examples/boot-check.md +3 -3
- package/agents/examples/chat-channels.md +1 -1
- package/agents/examples/cross-feature-events.md +7 -7
- package/agents/examples/currencies-global.md +3 -3
- package/agents/examples/currencies-per-tenant.md +2 -2
- package/agents/examples/custom-fields-basic.md +4 -3
- package/agents/examples/custom-handlers.md +40 -44
- package/agents/examples/dashboard-stats.md +6 -6
- package/agents/examples/delivery-notifications.md +9 -9
- package/agents/examples/designer-demo.md +2 -1
- package/agents/examples/embedded-entity-form.md +13 -13
- package/agents/examples/embedded.md +1 -1
- package/agents/examples/encrypted-tenant-config.md +1 -1
- package/agents/examples/error-contract.md +25 -25
- package/agents/examples/event-sourcing.md +96 -76
- package/agents/examples/feature-toggles.md +29 -21
- package/agents/examples/files-post-processing.md +22 -26
- package/agents/examples/idempotency.md +17 -19
- package/agents/examples/inbound-mail-triage.md +1 -1
- package/agents/examples/lane-routing.md +10 -12
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +59 -39
- package/agents/examples/managed-config.md +9 -9
- package/agents/examples/managed-pages.md +1 -1
- package/agents/examples/notes-history-basic.md +3 -3
- package/agents/examples/pdf-audit-export.md +7 -7
- package/agents/examples/pipeline-basics.md +8 -8
- package/agents/examples/public-share-token.md +1 -1
- package/agents/examples/rate-limiting.md +7 -9
- package/agents/examples/record-detail-layout.md +26 -20
- package/agents/examples/reference-data.md +1 -1
- package/agents/examples/relations.md +2 -2
- package/agents/examples/screens-nav.md +2 -2
- package/agents/examples/search.md +17 -19
- package/agents/examples/security-baseline.md +1 -1
- package/agents/examples/state-machine.md +1 -1
- package/agents/examples/tags-basic.md +3 -3
- package/agents/examples/tenant-settings.md +2 -2
- package/agents/examples/timezones.md +12 -12
- package/agents/examples/user-data-rights.md +3 -3
- package/agents/examples/user-profile.md +1 -1
- package/agents/examples/webhook-step.md +12 -2
- package/agents/examples/writeform-section.md +21 -15
- package/agents/features.md +1 -1
- package/agents/widgets.md +5 -0
- package/dist/api/server.js +20 -4
- package/dist/bun-db/query.js +16 -1
- package/dist/crypto/in-memory-kms-adapter.js +4 -7
- package/dist/crypto/pg-kms-adapter.js +10 -6
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/db/event-store-executor-write.js +16 -12
- package/dist/engine/boot-validator/api-ext.js +2 -2
- package/dist/engine/boot-validator/config-deps.js +1 -1
- package/dist/engine/boot-validator/handler-field-references.js +3 -10
- package/dist/engine/boot-validator/screens.js +1 -1
- package/dist/engine/build-app-schema.js +3 -6
- package/dist/engine/define-feature.js +5 -5
- package/dist/engine/entity-handlers.d.ts +2 -2
- package/dist/engine/entity-handlers.js +5 -5
- package/dist/engine/factories.d.ts +11 -11
- package/dist/engine/factories.js +11 -11
- package/dist/engine/feature-ast/extractors/round2.js +16 -2
- package/dist/engine/feature-ast/render.js +4 -7
- package/dist/engine/feature-config-events-jobs.d.ts +2 -0
- package/dist/engine/feature-config-events-jobs.js +3 -0
- package/dist/engine/feature-entity-handlers.d.ts +4 -23
- package/dist/engine/feature-entity-handlers.js +77 -100
- package/dist/engine/feature-ui-extensions.d.ts +9 -14
- package/dist/engine/feature-ui-extensions.js +22 -14
- package/dist/engine/handler-field-references.d.ts +25 -0
- package/dist/engine/handler-field-references.js +80 -6
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +2 -2
- package/dist/engine/object-form.d.ts +2 -0
- package/dist/engine/object-form.js +13 -10
- package/dist/engine/registry-state.js +2 -2
- package/dist/engine/registry-validate.d.ts +2 -0
- package/dist/engine/registry-validate.js +42 -2
- package/dist/engine/registry.js +3 -1
- package/dist/engine/strip-disabled-features-from-app-schema.d.ts +6 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +89 -0
- package/dist/engine/types/index.d.ts +2 -2
- package/dist/files/feature.js +1 -1
- package/dist/jobs/job-runner.d.ts +1 -0
- package/dist/jobs/job-runner.js +83 -17
- package/dist/jobs/max-per-tenant.d.ts +8 -0
- package/dist/jobs/max-per-tenant.js +14 -0
- package/dist/pipeline/dispatch-query.js +1 -1
- package/dist/pipeline/dispatch-shared.d.ts +2 -1
- package/dist/pipeline/dispatch-shared.js +3 -6
- package/dist/pipeline/index.d.ts +2 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/multi-stream-apply-context.d.ts +1 -0
- package/dist/pipeline/multi-stream-apply-context.js +1 -0
- package/dist/pipeline/tenant-timezone-config-key.d.ts +1 -0
- package/dist/pipeline/tenant-timezone-config-key.js +6 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/manual-clock.d.ts +14 -0
- package/dist/testing/manual-clock.js +12 -0
- package/dist/time/index.d.ts +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +9 -4
- package/dist/upgrade-cli.js +13 -2
- package/package.json +4 -4
- package/src/changes.json +58 -0
- package/src/scripts/codemod/README.md +23 -0
- package/src/scripts/codemod/object-form.ts +709 -0
|
@@ -2,7 +2,7 @@ import { createInitialFeatureBuilderState } from "./feature-builder-state.js";
|
|
|
2
2
|
import { buildConfigEventsJobsMethods } from "./feature-config-events-jobs.js";
|
|
3
3
|
import { buildEntityHandlerMethods } from "./feature-entity-handlers.js";
|
|
4
4
|
import { buildUiExtensionsMethods } from "./feature-ui-extensions.js";
|
|
5
|
-
import { unwrapArrayForm } from "./object-form.js";
|
|
5
|
+
import { isObjectArgument, throwPositionalFormRemoved, unwrapArrayForm } from "./object-form.js";
|
|
6
6
|
// `TExports` lets the setup callback hand back a typed object that
|
|
7
7
|
// downstream features can import (e.g. `tenantFeature.exports.config`). The
|
|
8
8
|
// runtime always packs whatever setup returns into `featureDef.exports` —
|
|
@@ -15,10 +15,8 @@ import { unwrapArrayForm } from "./object-form.js";
|
|
|
15
15
|
// for `ctx.appendEvent({ type: eventDef.name, ... })` lights up. Apps
|
|
16
16
|
// that don't care can keep the default-string and use the wrapper-based
|
|
17
17
|
// strict-mode (string-literal types per call-site) like before.
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// canonical Object-Form for Designer/AI-generated code) — see the matching
|
|
21
|
-
// overloads on RequiresApi / FeatureRegistrar['optionalRequires'].
|
|
18
|
+
// optionalRequires accepts either variadic strings or a single `{ features }` object; requires
|
|
19
|
+
// takes only the object (see RequiresApi).
|
|
22
20
|
function resolveFeatureNamesArgs(args) {
|
|
23
21
|
return unwrapArrayForm(args, "features");
|
|
24
22
|
}
|
|
@@ -50,6 +48,8 @@ export function defineFeature(name, setup, settings) {
|
|
|
50
48
|
},
|
|
51
49
|
requires: (() => {
|
|
52
50
|
const fn = (...args) => {
|
|
51
|
+
if (!args.every(isObjectArgument))
|
|
52
|
+
throwPositionalFormRemoved(name, "requires");
|
|
53
53
|
state.requires.push(...resolveFeatureNamesArgs(args));
|
|
54
54
|
};
|
|
55
55
|
fn.projection = (tableName) => {
|
|
@@ -63,5 +63,5 @@ export declare function defineProjectionQueryHandler(name: string, projectionQua
|
|
|
63
63
|
};
|
|
64
64
|
escapeHatch?: EscapeHatchDeclaration;
|
|
65
65
|
}): QueryHandlerDef;
|
|
66
|
-
/**
|
|
67
|
-
export declare function
|
|
66
|
+
/** Backs `r.crud`: registers the standard entity CRUD handlers. Access stays explicit — no openToAll default. */
|
|
67
|
+
export declare function registerCrudHandlers(r: EntityCrudRegistrar, entityName: string, entity: EntityDefinition, options?: RegisterEntityCrudOptions): void;
|
|
@@ -551,21 +551,21 @@ function defaultCrudVerbs(entity) {
|
|
|
551
551
|
detail: true,
|
|
552
552
|
};
|
|
553
553
|
}
|
|
554
|
-
/**
|
|
555
|
-
export function
|
|
554
|
+
/** Backs `r.crud`: registers the standard entity CRUD handlers. Access stays explicit — no openToAll default. */
|
|
555
|
+
export function registerCrudHandlers(r, entityName, entity, options) {
|
|
556
556
|
const verbs = { ...defaultCrudVerbs(entity), ...options?.verbs };
|
|
557
557
|
if (verbs.restore && entity.softDelete !== true) {
|
|
558
558
|
throw new Error(`restore requested but entity "${entityName}" has no softDelete: true`);
|
|
559
559
|
}
|
|
560
560
|
if (options?.registerEntity !== false) {
|
|
561
|
-
r.entity(entityName, entity);
|
|
561
|
+
r.entity({ name: entityName, ...entity });
|
|
562
562
|
}
|
|
563
563
|
const writeOpts = options?.write;
|
|
564
564
|
const readOpts = options?.read;
|
|
565
565
|
const requireAccess = (verb, access) => {
|
|
566
566
|
if (!access) {
|
|
567
|
-
throw new Error(`
|
|
568
|
-
`Set write.access/read.access
|
|
567
|
+
throw new Error(`r.crud("${entityName}", …): no access rule resolved for verb "${verb}". ` +
|
|
568
|
+
`Set write.access/read.access in r.crud's options, or verbAccess.${verb}.`);
|
|
569
569
|
}
|
|
570
570
|
return access;
|
|
571
571
|
};
|
|
@@ -133,34 +133,34 @@ export declare function createTzField<R extends true | false = false>(overrides?
|
|
|
133
133
|
required: R;
|
|
134
134
|
};
|
|
135
135
|
/**
|
|
136
|
-
* Wall-
|
|
137
|
-
*
|
|
136
|
+
* Wall-clock time at a place as one atomic field. Use it for every date
|
|
137
|
+
* value tied to a location (pickup, delivery, meeting, schedule).
|
|
138
138
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
139
|
+
* One schema field maps to two DB columns (`<name>_utc TIMESTAMPTZ` +
|
|
140
|
+
* `<name>_tz TEXT`) and to three API fields on read (`{ at, tz, utc }`).
|
|
141
141
|
*
|
|
142
142
|
* ```ts
|
|
143
|
-
* r.entity(
|
|
143
|
+
* r.entity({
|
|
144
|
+
* name: "order",
|
|
144
145
|
* fields: {
|
|
145
146
|
* pickup: createLocatedTimestampField(),
|
|
146
147
|
* delivery: createLocatedTimestampField(),
|
|
147
148
|
* },
|
|
148
149
|
* });
|
|
149
150
|
*
|
|
150
|
-
* // Write
|
|
151
|
+
* // Write: the caller sends at+tz, the framework computes utc
|
|
151
152
|
* await create({ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" } });
|
|
152
153
|
*
|
|
153
|
-
* // Read
|
|
154
|
+
* // Read: the framework returns all three fields
|
|
154
155
|
* // → { pickup: { at: "10:00", tz: "Europe/Lisbon", utc: "09:00Z" } }
|
|
155
156
|
*
|
|
156
|
-
* // Sort/
|
|
157
|
+
* // Sort/filter on the UTC column
|
|
157
158
|
* orderBy(asc(orderTable.pickupUtc))
|
|
158
159
|
* where(between(orderTable.pickupUtc, start, end))
|
|
159
160
|
* ```
|
|
160
161
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* separater Berechnung.
|
|
162
|
+
* `at` is local to the place (what was entered comes back on the same day).
|
|
163
|
+
* A user-local view is computed separately from `utc`.
|
|
164
164
|
*/
|
|
165
165
|
export declare function createLocatedTimestampField<R extends true | false = false>(overrides?: Partial<Omit<LocatedTimestampFieldDef, "type" | "required" | keyof ResolvedPiiFlags>> & PersonalAnnotationsNoFind & {
|
|
166
166
|
required?: R;
|
package/dist/engine/factories.js
CHANGED
|
@@ -324,34 +324,34 @@ export function createTzField(overrides) {
|
|
|
324
324
|
}; // @cast-boundary engine-payload
|
|
325
325
|
}
|
|
326
326
|
/**
|
|
327
|
-
* Wall-
|
|
328
|
-
*
|
|
327
|
+
* Wall-clock time at a place as one atomic field. Use it for every date
|
|
328
|
+
* value tied to a location (pickup, delivery, meeting, schedule).
|
|
329
329
|
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
330
|
+
* One schema field maps to two DB columns (`<name>_utc TIMESTAMPTZ` +
|
|
331
|
+
* `<name>_tz TEXT`) and to three API fields on read (`{ at, tz, utc }`).
|
|
332
332
|
*
|
|
333
333
|
* ```ts
|
|
334
|
-
* r.entity(
|
|
334
|
+
* r.entity({
|
|
335
|
+
* name: "order",
|
|
335
336
|
* fields: {
|
|
336
337
|
* pickup: createLocatedTimestampField(),
|
|
337
338
|
* delivery: createLocatedTimestampField(),
|
|
338
339
|
* },
|
|
339
340
|
* });
|
|
340
341
|
*
|
|
341
|
-
* // Write
|
|
342
|
+
* // Write: the caller sends at+tz, the framework computes utc
|
|
342
343
|
* await create({ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" } });
|
|
343
344
|
*
|
|
344
|
-
* // Read
|
|
345
|
+
* // Read: the framework returns all three fields
|
|
345
346
|
* // → { pickup: { at: "10:00", tz: "Europe/Lisbon", utc: "09:00Z" } }
|
|
346
347
|
*
|
|
347
|
-
* // Sort/
|
|
348
|
+
* // Sort/filter on the UTC column
|
|
348
349
|
* orderBy(asc(orderTable.pickupUtc))
|
|
349
350
|
* where(between(orderTable.pickupUtc, start, end))
|
|
350
351
|
* ```
|
|
351
352
|
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* separater Berechnung.
|
|
353
|
+
* `at` is local to the place (what was entered comes back on the same day).
|
|
354
|
+
* A user-local view is computed separately from `utc`.
|
|
355
355
|
*/
|
|
356
356
|
export function createLocatedTimestampField(overrides) {
|
|
357
357
|
return {
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { SyntaxKind } from "ts-morph";
|
|
2
2
|
import { describeUnknownFieldType, findUnknownEntityFieldTypes } from "../entity-field-types.js";
|
|
3
3
|
import { sourceLocationFromNode } from "../source-location.js";
|
|
4
|
-
import { fail, isPlainObject, ok, readDataLiteralNode, readNameLiteral, readNameOrRef, } from "./shared.js";
|
|
4
|
+
import { fail, isPlainObject, isRawRefSentinel, ok, readDataLiteralNode, readNameLiteral, readNameOrRef, } from "./shared.js";
|
|
5
|
+
// `{ name: "x", ...xEntity }` — the definition is a reference the parser cannot
|
|
6
|
+
// inline, so it is kept as a raw-ref sentinel and re-rendered as the same spread.
|
|
7
|
+
// `backingTable` is runtime wiring (a table identifier), not part of the
|
|
8
|
+
// definition data, so it does not take part in the shape check.
|
|
9
|
+
function readSpreadOnlyDefinition(obj) {
|
|
10
|
+
const properties = obj
|
|
11
|
+
.getProperties()
|
|
12
|
+
.filter((property) => property.asKind(SyntaxKind.PropertyAssignment)?.getName() !== "backingTable");
|
|
13
|
+
const spread = properties.length === 2 ? properties[1]?.asKind(SyntaxKind.SpreadAssignment) : undefined;
|
|
14
|
+
if (!spread)
|
|
15
|
+
return undefined;
|
|
16
|
+
const reference = readDataLiteralNode(spread.getExpression());
|
|
17
|
+
return isRawRefSentinel(reference) ? reference : undefined;
|
|
18
|
+
}
|
|
5
19
|
export function extractEntity(call, sourceFile) {
|
|
6
20
|
const args = call.getArguments();
|
|
7
21
|
const first = args[0];
|
|
@@ -18,7 +32,7 @@ export function extractEntity(call, sourceFile) {
|
|
|
18
32
|
if (!nameInit) {
|
|
19
33
|
return fail("entity", sourceLocationFromNode(call, sourceFile), "object form requires a string-literal `name` property");
|
|
20
34
|
}
|
|
21
|
-
const definition = readDataLiteralNode(obj);
|
|
35
|
+
const definition = readSpreadOnlyDefinition(obj) ?? readDataLiteralNode(obj);
|
|
22
36
|
if (!isPlainObject(definition)) {
|
|
23
37
|
return fail("entity", sourceLocationFromNode(call, sourceFile), "definition could not be read as a plain object (contains functions or identifiers)");
|
|
24
38
|
}
|
|
@@ -202,15 +202,12 @@ function renderDescribe(p) {
|
|
|
202
202
|
return `r.describe(${JSON.stringify(p.text)});`;
|
|
203
203
|
}
|
|
204
204
|
function renderEntity(p) {
|
|
205
|
-
// A whole-value raw-ref (`
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// which keeps the reference verbatim alongside the name.
|
|
205
|
+
// A whole-value raw-ref (`eventEntity`) can't go through renderValue without
|
|
206
|
+
// losing `name` to the sentinel shortcut — spread the reference verbatim
|
|
207
|
+
// next to the name instead.
|
|
209
208
|
if (isRawRefSentinel(p.definition)) {
|
|
210
|
-
return `r.entity(${JSON.stringify(p.entityName)},
|
|
209
|
+
return `r.entity({ name: ${JSON.stringify(p.entityName)}, ...${p.definition.__raw} });`;
|
|
211
210
|
}
|
|
212
|
-
// Inline `name` into the definition object — canonical Object-Form
|
|
213
|
-
// is a single arg with name-as-property.
|
|
214
211
|
const merged = { name: p.entityName, ...p.definition };
|
|
215
212
|
return `r.entity(${renderValue(merged)});`;
|
|
216
213
|
}
|
|
@@ -25,6 +25,7 @@ export declare function buildConfigEventsJobsMethods<TName extends string>(state
|
|
|
25
25
|
readonly template?: Readonly<Record<string, NotificationTemplateBody>>;
|
|
26
26
|
readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
|
|
27
27
|
readonly localizedTemplate?: () => NotificationLocalizedTemplate;
|
|
28
|
+
readonly renamedFrom?: readonly string[];
|
|
28
29
|
}), definition?: {
|
|
29
30
|
readonly trigger?: {
|
|
30
31
|
readonly on: NameOrRef;
|
|
@@ -37,6 +38,7 @@ export declare function buildConfigEventsJobsMethods<TName extends string>(state
|
|
|
37
38
|
readonly template?: Readonly<Record<string, NotificationTemplateBody>>;
|
|
38
39
|
readonly meta?: Readonly<Record<string, NotificationTemplateMeta>>;
|
|
39
40
|
readonly localizedTemplate?: () => NotificationLocalizedTemplate;
|
|
41
|
+
readonly renamedFrom?: readonly string[];
|
|
40
42
|
}): void;
|
|
41
43
|
translations(def: TranslationsDef): void;
|
|
42
44
|
defineEvent: <const TInner extends string, TPayload>(eventName: TInner, schema: ZodType<TPayload>, options: {
|
|
@@ -217,6 +217,9 @@ export function buildConfigEventsJobsMethods(state, name) {
|
|
|
217
217
|
...(resolvedDefinition.localizedTemplate !== undefined
|
|
218
218
|
? { localizedTemplate: resolvedDefinition.localizedTemplate }
|
|
219
219
|
: {}),
|
|
220
|
+
...(resolvedDefinition.renamedFrom !== undefined
|
|
221
|
+
? { renamedFrom: resolvedDefinition.renamedFrom }
|
|
222
|
+
: {}),
|
|
220
223
|
};
|
|
221
224
|
},
|
|
222
225
|
translations(def) {
|
|
@@ -3,34 +3,15 @@ import type { ZodType } from "zod";
|
|
|
3
3
|
import { type QueryHandlerDefinition, type StreamHandlerDefinition, type WriteHandlerDefinition } from "./define-handler.js";
|
|
4
4
|
import type { RegisterEntityCrudOptions } from "./entity-handlers.js";
|
|
5
5
|
import type { FeatureBuilderState } from "./feature-builder-state.js";
|
|
6
|
-
import type { AccessRule,
|
|
6
|
+
import type { AccessRule, EntityDefinition, EntityObjectDefinition, EntityRef, EscapeHatchDeclaration, HandlerRef, KumikoEventTypeMap, NameOrRef, RateLimitDeclaration, RelationDefinition, StreamHandlerFn } from "./types/index.js";
|
|
7
7
|
export declare function buildEntityHandlerMethods<TName extends string>(state: FeatureBuilderState, name: TName): {
|
|
8
|
-
entity(
|
|
9
|
-
readonly name: string;
|
|
10
|
-
} & EntityDefinition), definition?: EntityDefinition, options?: {
|
|
11
|
-
readonly table?: unknown;
|
|
12
|
-
}): EntityRef;
|
|
8
|
+
entity(definition: EntityObjectDefinition): EntityRef;
|
|
13
9
|
relation(entityRefOrDefinition: NameOrRef | ({
|
|
14
10
|
readonly entity: NameOrRef;
|
|
15
11
|
readonly name: string;
|
|
16
12
|
} & RelationDefinition), relationName?: string, definition?: RelationDefinition): void;
|
|
17
|
-
writeHandler<TName_1 extends string, TSchema extends ZodType, TPrepared = undefined>(
|
|
18
|
-
|
|
19
|
-
rateLimit?: RateLimitDeclaration;
|
|
20
|
-
additionalRateLimits?: readonly PayloadRateLimitOption[];
|
|
21
|
-
description?: string;
|
|
22
|
-
agent?: AgentHandlerHints;
|
|
23
|
-
escapeHatch?: EscapeHatchDeclaration;
|
|
24
|
-
}): HandlerRef;
|
|
25
|
-
queryHandler<TName_1 extends string, TSchema extends ZodType>(nameOrDef: string | QueryHandlerDefinition<TName_1, TSchema>, schema?: TSchema, handler?: QueryHandlerFn<z.infer<TSchema>>, options?: {
|
|
26
|
-
access: AccessRule;
|
|
27
|
-
rateLimit?: RateLimitDeclaration;
|
|
28
|
-
outputSchema?: ZodType;
|
|
29
|
-
liveEntities?: readonly string[];
|
|
30
|
-
description?: string;
|
|
31
|
-
agent?: AgentHandlerHints;
|
|
32
|
-
escapeHatch?: EscapeHatchDeclaration;
|
|
33
|
-
}): HandlerRef;
|
|
13
|
+
writeHandler<TName_1 extends string, TSchema extends ZodType, TPrepared = undefined>(def: WriteHandlerDefinition<TName_1, TSchema, unknown, KumikoEventTypeMap, TPrepared>): HandlerRef;
|
|
14
|
+
queryHandler<TName_1 extends string, TSchema extends ZodType>(def: QueryHandlerDefinition<TName_1, TSchema>): HandlerRef;
|
|
34
15
|
streamHandler<TName_1 extends string, TSchema extends ZodType>(nameOrDef: string | StreamHandlerDefinition<TName_1, TSchema>, schema?: TSchema, handler?: StreamHandlerFn<z.infer<TSchema>>, options?: {
|
|
35
16
|
access: AccessRule;
|
|
36
17
|
rateLimit?: RateLimitDeclaration;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
2
|
import { toTableName } from "../db/table-builder.js";
|
|
3
3
|
import { isEntityConventionQueryHandler, isPagedQueryHandler, PAGED_QUERY_HANDLER_BRAND, } from "./define-handler.js";
|
|
4
|
-
import {
|
|
4
|
+
import { registerCrudHandlers } from "./entity-handlers.js";
|
|
5
5
|
import { resolveName } from "./handler-helpers.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isObjectArgument, throwPositionalFormRemoved } from "./object-form.js";
|
|
7
7
|
const CRUD_VERBS = new Set(["create", "update", "delete"]);
|
|
8
8
|
const CRUD_REGISTERED_VERBS = new Set([...CRUD_VERBS, "restore", "list", "detail"]);
|
|
9
9
|
// A silent overwrite would drop framework behaviour that rides on the r.crud
|
|
@@ -65,6 +65,13 @@ function transactionPhaseSlots(def) {
|
|
|
65
65
|
...(def.transaction && { transaction: def.transaction }),
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
// Typed callers cannot omit `access`; untyped ones (JS, designer output) must not get a
|
|
69
|
+
// handler that silently falls through to the boot-time default-deny.
|
|
70
|
+
function assertHandlerHasAccess(featureName, method, handlerName, access) {
|
|
71
|
+
if (access === undefined || access === null) {
|
|
72
|
+
throw new Error(`[Feature ${featureName}] r.${method}({ name: "${handlerName}" }) requires an access rule (access: { roles: [...] } or { openToAll: { reason } })`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
68
75
|
function requireInlineHandlerArgs(kind, schema, handler, options) {
|
|
69
76
|
if (!schema || !handler || !options?.access) {
|
|
70
77
|
throw new Error(`${kind} inline form requires schema + handler + options.access`);
|
|
@@ -75,13 +82,13 @@ function requireInlineHandlerArgs(kind, schema, handler, options) {
|
|
|
75
82
|
// methods that create or reference entities.
|
|
76
83
|
export function buildEntityHandlerMethods(state, name) {
|
|
77
84
|
const methods = {
|
|
78
|
-
entity(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
entity(definition) {
|
|
86
|
+
if (!isObjectArgument(definition))
|
|
87
|
+
throwPositionalFormRemoved(name, "entity");
|
|
88
|
+
const { name: entityName, backingTable, ...entityDefinition } = definition;
|
|
82
89
|
state.entities[entityName] = entityDefinition;
|
|
83
|
-
if (
|
|
84
|
-
state.entityTables[entityName] =
|
|
90
|
+
if (backingTable !== undefined)
|
|
91
|
+
state.entityTables[entityName] = backingTable;
|
|
85
92
|
return { name: entityName, table: entityDefinition.table ?? toTableName(entityName) };
|
|
86
93
|
},
|
|
87
94
|
relation(entityRefOrDefinition, relationName, definition) {
|
|
@@ -96,105 +103,75 @@ export function buildEntityHandlerMethods(state, name) {
|
|
|
96
103
|
state.relations[entityName] = {};
|
|
97
104
|
state.relations[entityName][resolvedRelationName] = resolvedDefinition;
|
|
98
105
|
},
|
|
99
|
-
writeHandler(
|
|
100
|
-
if (
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
};
|
|
134
|
-
tryMapEntity(state, name, def.name);
|
|
135
|
-
return { name: def.name };
|
|
136
|
-
}
|
|
137
|
-
const inline = requireInlineHandlerArgs("writeHandler", schema, handler, options);
|
|
138
|
-
assertHandlerNameFree(state, name, "write", nameOrDef);
|
|
139
|
-
state.writeHandlers[nameOrDef] = {
|
|
140
|
-
name: nameOrDef,
|
|
141
|
-
schema: inline.schema,
|
|
142
|
-
handler: inline.handler, // @cast-boundary engine-bridge
|
|
143
|
-
access: inline.options.access,
|
|
144
|
-
...agentSlots(inline.options),
|
|
145
|
-
...(inline.options.rateLimit && { rateLimit: inline.options.rateLimit }),
|
|
146
|
-
...(inline.options.additionalRateLimits && {
|
|
147
|
-
additionalRateLimits: inline.options.additionalRateLimits,
|
|
106
|
+
writeHandler(def) {
|
|
107
|
+
if (!isObjectArgument(def))
|
|
108
|
+
throwPositionalFormRemoved(name, "writeHandler");
|
|
109
|
+
assertHandlerHasAccess(name, "writeHandler", def.name, def.access);
|
|
110
|
+
assertHandlerNameFree(state, name, "write", def.name);
|
|
111
|
+
state.writeHandlers[def.name] = {
|
|
112
|
+
name: def.name,
|
|
113
|
+
schema: def.schema,
|
|
114
|
+
// @cast-boundary engine-bridge — typed Dev-API's handler is
|
|
115
|
+
// generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
|
|
116
|
+
// the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
|
|
117
|
+
// Function-arg variance: TS sees the typed handler as stricter
|
|
118
|
+
// than the loose storage shape and rejects direct assignment.
|
|
119
|
+
// The runtime value is identical — the cast crosses that boundary.
|
|
120
|
+
// `satisfies` does not work here (it asserts assignability, which
|
|
121
|
+
// is what fails). Explicit cast is the right tool.
|
|
122
|
+
handler: def.handler,
|
|
123
|
+
access: def.access,
|
|
124
|
+
...agentSlots(def),
|
|
125
|
+
...(def.excludedFields && { excludedFields: def.excludedFields }),
|
|
126
|
+
...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
|
|
127
|
+
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
128
|
+
...(def.additionalRateLimits && { additionalRateLimits: def.additionalRateLimits }),
|
|
129
|
+
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
130
|
+
...transactionPhaseSlots(def),
|
|
131
|
+
...(def.tenantlessAnonymous && { tenantlessAnonymous: true }),
|
|
132
|
+
// Forward the pipeline-build closure so boot-validators and
|
|
133
|
+
// Designer/AI tooling can inspect the step list. Absent on
|
|
134
|
+
// free-form handlers — defineWriteHandler only sets `perform`
|
|
135
|
+
// when the author used the pipeline form. Variance cast
|
|
136
|
+
// mirrors the handler-cast above: PipelineDef<output<TSchema>>
|
|
137
|
+
// is stricter than PipelineDef<unknown> for the same reason.
|
|
138
|
+
...(def.perform !== undefined && {
|
|
139
|
+
perform: def.perform,
|
|
148
140
|
}),
|
|
149
|
-
...(inline.options.escapeHatch && { escapeHatch: inline.options.escapeHatch }),
|
|
150
141
|
};
|
|
151
|
-
tryMapEntity(state, name,
|
|
152
|
-
return { name:
|
|
142
|
+
tryMapEntity(state, name, def.name);
|
|
143
|
+
return { name: def.name };
|
|
153
144
|
},
|
|
154
|
-
queryHandler(
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
tryMapEntity(state, name, def.name);
|
|
177
|
-
return { name: def.name };
|
|
178
|
-
}
|
|
179
|
-
const inline = requireInlineHandlerArgs("queryHandler", schema, handler, options);
|
|
180
|
-
assertHandlerNameFree(state, name, "query", nameOrDef);
|
|
181
|
-
state.queryHandlers[nameOrDef] = {
|
|
182
|
-
name: nameOrDef,
|
|
183
|
-
schema: inline.schema,
|
|
184
|
-
handler: inline.handler, // @cast-boundary engine-bridge
|
|
185
|
-
access: inline.options.access,
|
|
186
|
-
...agentSlots(inline.options),
|
|
187
|
-
...(inline.options.rateLimit && { rateLimit: inline.options.rateLimit }),
|
|
188
|
-
...(inline.options.outputSchema && { outputSchema: inline.options.outputSchema }),
|
|
189
|
-
...(inline.options.liveEntities && { liveEntities: inline.options.liveEntities }),
|
|
190
|
-
...(inline.options.escapeHatch && { escapeHatch: inline.options.escapeHatch }),
|
|
145
|
+
queryHandler(def) {
|
|
146
|
+
if (!isObjectArgument(def))
|
|
147
|
+
throwPositionalFormRemoved(name, "queryHandler");
|
|
148
|
+
assertHandlerHasAccess(name, "queryHandler", def.name, def.access);
|
|
149
|
+
assertHandlerNameFree(state, name, "query", def.name);
|
|
150
|
+
state.queryHandlers[def.name] = {
|
|
151
|
+
name: def.name,
|
|
152
|
+
schema: def.schema,
|
|
153
|
+
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
154
|
+
handler: def.handler, // @cast-boundary engine-bridge
|
|
155
|
+
access: def.access,
|
|
156
|
+
...agentSlots(def),
|
|
157
|
+
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
158
|
+
...(def.outputSchema && { outputSchema: def.outputSchema }),
|
|
159
|
+
...(def.liveEntities && { liveEntities: def.liveEntities }),
|
|
160
|
+
...(def.escapeHatch && { escapeHatch: def.escapeHatch }),
|
|
161
|
+
// Carry the definePagedQueryHandler brand through — this rebuild
|
|
162
|
+
// drops any field not explicitly listed.
|
|
163
|
+
...(isPagedQueryHandler(def) && { [PAGED_QUERY_HANDLER_BRAND]: true }),
|
|
164
|
+
...(isEntityConventionQueryHandler(def) && {
|
|
165
|
+
[ENTITY_CONVENTION_QUERY_BRAND]: true,
|
|
166
|
+
}),
|
|
191
167
|
};
|
|
192
|
-
tryMapEntity(state, name,
|
|
193
|
-
return { name:
|
|
168
|
+
tryMapEntity(state, name, def.name);
|
|
169
|
+
return { name: def.name };
|
|
194
170
|
},
|
|
195
171
|
streamHandler(nameOrDef, schema, handler, options) {
|
|
196
172
|
if (typeof nameOrDef === "object") {
|
|
197
173
|
const def = nameOrDef;
|
|
174
|
+
assertHandlerHasAccess(name, "streamHandler", def.name, def.access);
|
|
198
175
|
state.streamHandlers[def.name] = {
|
|
199
176
|
name: def.name,
|
|
200
177
|
schema: def.schema,
|
|
@@ -221,7 +198,7 @@ export function buildEntityHandlerMethods(state, name) {
|
|
|
221
198
|
return {
|
|
222
199
|
...methods,
|
|
223
200
|
crud(entityName, entity, options) {
|
|
224
|
-
|
|
201
|
+
registerCrudHandlers(methods, entityName, entity, options);
|
|
225
202
|
},
|
|
226
203
|
};
|
|
227
204
|
}
|
|
@@ -2,17 +2,12 @@ import { type WebSocketRouteDefinition } from "@cosmicdrift/kumiko-types/websock
|
|
|
2
2
|
import type { EntityTableMeta } from "../db/entity-table-meta.js";
|
|
3
3
|
import type { FeatureBuilderState } from "./feature-builder-state.js";
|
|
4
4
|
import type { HttpRouteDefinition } from "./types/http-route.js";
|
|
5
|
-
import type { BootCheckFn, EntityProjectionExtension,
|
|
5
|
+
import type { BootCheckFn, EntityProjectionExtension, ExtensionSelectorPanel, HookDefinition, MultiStreamProjectionDefinition, NameOrRef, ProjectionDefinition, RegistrarExtensionDef, SearchPayloadContributorFn, StoreTableOptions, TreeActionDef, TreeActionsHandle } from "./types/index.js";
|
|
6
6
|
import type { ContentCollectionDefinition, NavDefinition } from "./types/nav.js";
|
|
7
7
|
import type { ScreenDefinition } from "./types/screen.js";
|
|
8
8
|
import type { WorkspaceDefinition } from "./types/workspace.js";
|
|
9
9
|
export declare function buildUiExtensionsMethods<TName extends string>(state: FeatureBuilderState, name: TName): {
|
|
10
|
-
hook(
|
|
11
|
-
readonly allOf: NameOrRef;
|
|
12
|
-
}, fn: LifecycleHookFn | ValidationHookFn, options?: {
|
|
13
|
-
phase?: HookPhase;
|
|
14
|
-
escapeHatch?: EscapeHatchDeclaration;
|
|
15
|
-
}): void;
|
|
10
|
+
hook(definition: HookDefinition): void;
|
|
16
11
|
searchPayloadExtension(entityRef: NameOrRef, fn: SearchPayloadContributorFn): void;
|
|
17
12
|
extendsRegistrar(extensionName: string, def: RegistrarExtensionDef): void;
|
|
18
13
|
useExtension(extensionNameOrDefinition: string | ({
|
|
@@ -25,20 +20,20 @@ export declare function buildUiExtensionsMethods<TName extends string>(state: Fe
|
|
|
25
20
|
readonly panels?: readonly ExtensionSelectorPanel[];
|
|
26
21
|
}): void;
|
|
27
22
|
/**
|
|
28
|
-
* Marker
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
23
|
+
* Marker declaration: this feature provides a cross-feature API under
|
|
24
|
+
* the given name. The implementation is registered separately as a query
|
|
25
|
+
* or write handler under the QN pattern; r.exposesApi only feeds the
|
|
26
|
+
* boot check.
|
|
32
27
|
*
|
|
33
|
-
*
|
|
28
|
+
* Example:
|
|
34
29
|
* defineFeature("compliance-profiles", (r) => {
|
|
35
30
|
* r.exposesApi("compliance.forTenant");
|
|
36
31
|
* r.queryHandler({ name: "compliance:query:for-tenant", ... });
|
|
37
32
|
* });
|
|
38
33
|
* defineFeature("user-data-rights", (r) => {
|
|
39
|
-
* r.requires("compliance-profiles");
|
|
34
|
+
* r.requires({ features: ["compliance-profiles"] });
|
|
40
35
|
* r.usesApi("compliance.forTenant");
|
|
41
|
-
* //
|
|
36
|
+
* // in a handler: ctx.callQuery("compliance:query:for-tenant", ...)
|
|
42
37
|
* });
|
|
43
38
|
*/
|
|
44
39
|
exposesApi(apiName: string): void;
|