@cosmicdrift/kumiko-types 0.159.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/LICENSE +57 -0
- package/package.json +95 -0
- package/src/db-connection.ts +11 -0
- package/src/event-type-map.ts +37 -0
- package/src/geo-tz.ts +32 -0
- package/src/http-route.ts +72 -0
- package/src/identifiers.ts +47 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +7 -0
- package/src/observability/index.ts +29 -0
- package/src/observability/metric.ts +56 -0
- package/src/observability/provider.ts +32 -0
- package/src/observability/span.ts +58 -0
- package/src/relations.ts +52 -0
- package/src/search-adapter.ts +39 -0
- package/src/target-ref.ts +21 -0
- package/src/tree-node.ts +129 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
Parameters
|
|
4
|
+
|
|
5
|
+
Licensor: Marc Frost
|
|
6
|
+
|
|
7
|
+
Licensed Work: @cosmicdrift/kumiko-framework
|
|
8
|
+
The Licensed Work is © 2026 Marc Frost.
|
|
9
|
+
|
|
10
|
+
Additional Use Grant:
|
|
11
|
+
You may use the Licensed Work in production for any purpose, including
|
|
12
|
+
commercially, EXCEPT for the Restricted Use.
|
|
13
|
+
|
|
14
|
+
"Restricted Use" is defined as using the Licensed Work to provide a platform
|
|
15
|
+
or service to third parties that allows them to host, deploy, or run their
|
|
16
|
+
own applications built with the Licensed Work. This includes, but is not
|
|
17
|
+
limited to: managed hosting services, software-as-a-service (SaaS) platforms,
|
|
18
|
+
platform-as-a-service (PaaS), developer platforms, or any multi-tenant
|
|
19
|
+
managed offering of the Licensed Work.
|
|
20
|
+
|
|
21
|
+
This restriction does not apply to the Licensor, any entity controlled by,
|
|
22
|
+
controlling, or under common control with the Licensor ("Affiliates"), or
|
|
23
|
+
contractors acting on their behalf. The Licensor remains free to use the
|
|
24
|
+
Licensed Work for any purpose, including for the operation of kumiko.rocks.
|
|
25
|
+
|
|
26
|
+
Change Date: 2030-05-05
|
|
27
|
+
Change License: Apache License, Version 2.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Terms
|
|
31
|
+
|
|
32
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
33
|
+
redistribute, and make non-production use of the Licensed Work. The Licensor may
|
|
34
|
+
make an Additional Use Grant, above, permitting limited production use.
|
|
35
|
+
|
|
36
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
37
|
+
available distribution of the Licensed Work under this License, whichever comes
|
|
38
|
+
first, this License will convert to the Change License.
|
|
39
|
+
|
|
40
|
+
This Business Source License governs use of the Licensed Work in all cases, except
|
|
41
|
+
as to any use that is explicitly granted in the Additional Use Grant above or
|
|
42
|
+
under the Change License after the Change Date.
|
|
43
|
+
|
|
44
|
+
If your use of the Licensed Work does not comply with the requirements of this
|
|
45
|
+
License, you must cease use of the Licensed Work immediately.
|
|
46
|
+
|
|
47
|
+
All copies of the Licensed Work, and all derivative works thereof, must include
|
|
48
|
+
this License.
|
|
49
|
+
|
|
50
|
+
This License does not grant you any right, title, or interest in any trademark,
|
|
51
|
+
logo, or branding of the Licensor, except as required to comply with this License.
|
|
52
|
+
|
|
53
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
|
|
54
|
+
“AS IS” BASIS. LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
|
|
55
|
+
WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
56
|
+
TITLE, AND NON-INFRINGEMENT. IN NO EVENT WILL LICENSOR BE LIABLE FOR ANY DAMAGES
|
|
57
|
+
ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF THE LICENSED WORK.
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cosmicdrift/kumiko-types",
|
|
3
|
+
"version": "0.159.0",
|
|
4
|
+
"description": "Framework-Type-Definitions für Kumiko — FeatureDefinition, BootCheck-Types und die reinen Engine-Types, ohne Runtime-Code. Erlaubt Downstream-Konsumenten, gegen die Type-Contracts zu bauen, ohne das ganze Framework-Package zu importieren.",
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
|
+
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/CosmicDriftGameStudio/kumiko-framework.git",
|
|
10
|
+
"directory": "packages/types"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/CosmicDriftGameStudio/kumiko-framework/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://kumiko.rocks",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"kumiko": {
|
|
18
|
+
"runtime": "client"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./src/index.ts",
|
|
23
|
+
"default": "./src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"./identifiers": {
|
|
26
|
+
"types": "./src/identifiers.ts",
|
|
27
|
+
"default": "./src/identifiers.ts"
|
|
28
|
+
},
|
|
29
|
+
"./target-ref": {
|
|
30
|
+
"types": "./src/target-ref.ts",
|
|
31
|
+
"default": "./src/target-ref.ts"
|
|
32
|
+
},
|
|
33
|
+
"./event-type-map": {
|
|
34
|
+
"types": "./src/event-type-map.ts",
|
|
35
|
+
"default": "./src/event-type-map.ts"
|
|
36
|
+
},
|
|
37
|
+
"./http-route": {
|
|
38
|
+
"types": "./src/http-route.ts",
|
|
39
|
+
"default": "./src/http-route.ts"
|
|
40
|
+
},
|
|
41
|
+
"./relations": {
|
|
42
|
+
"types": "./src/relations.ts",
|
|
43
|
+
"default": "./src/relations.ts"
|
|
44
|
+
},
|
|
45
|
+
"./tree-node": {
|
|
46
|
+
"types": "./src/tree-node.ts",
|
|
47
|
+
"default": "./src/tree-node.ts"
|
|
48
|
+
},
|
|
49
|
+
"./logger": {
|
|
50
|
+
"types": "./src/logger.ts",
|
|
51
|
+
"default": "./src/logger.ts"
|
|
52
|
+
},
|
|
53
|
+
"./search-adapter": {
|
|
54
|
+
"types": "./src/search-adapter.ts",
|
|
55
|
+
"default": "./src/search-adapter.ts"
|
|
56
|
+
},
|
|
57
|
+
"./geo-tz": {
|
|
58
|
+
"types": "./src/geo-tz.ts",
|
|
59
|
+
"default": "./src/geo-tz.ts"
|
|
60
|
+
},
|
|
61
|
+
"./observability": {
|
|
62
|
+
"types": "./src/observability/index.ts",
|
|
63
|
+
"default": "./src/observability/index.ts"
|
|
64
|
+
},
|
|
65
|
+
"./observability/metric": {
|
|
66
|
+
"types": "./src/observability/metric.ts",
|
|
67
|
+
"default": "./src/observability/metric.ts"
|
|
68
|
+
},
|
|
69
|
+
"./observability/provider": {
|
|
70
|
+
"types": "./src/observability/provider.ts",
|
|
71
|
+
"default": "./src/observability/provider.ts"
|
|
72
|
+
},
|
|
73
|
+
"./observability/span": {
|
|
74
|
+
"types": "./src/observability/span.ts",
|
|
75
|
+
"default": "./src/observability/span.ts"
|
|
76
|
+
},
|
|
77
|
+
"./db-connection": {
|
|
78
|
+
"types": "./src/db-connection.ts",
|
|
79
|
+
"default": "./src/db-connection.ts"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"hono": "^4.12.18",
|
|
84
|
+
"postgres": "^3.4.9"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"registry": "https://registry.npmjs.org",
|
|
88
|
+
"access": "public"
|
|
89
|
+
},
|
|
90
|
+
"files": [
|
|
91
|
+
"src",
|
|
92
|
+
"README.md",
|
|
93
|
+
"LICENSE"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type postgres from "postgres";
|
|
2
|
+
|
|
3
|
+
// biome-ignore lint/suspicious/noExplicitAny: Bun.SQL global type
|
|
4
|
+
export type DbConnection = ReturnType<typeof postgres> | any;
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: postgres-js namespace lookup
|
|
6
|
+
export type DbTx = postgres.TransactionSql<any> | any;
|
|
7
|
+
export type DbRunner = DbConnection | DbTx;
|
|
8
|
+
export type DbRow = Record<string, unknown>;
|
|
9
|
+
export type PgClient = ReturnType<typeof postgres>;
|
|
10
|
+
|
|
11
|
+
export type PgListenClient = ReturnType<typeof postgres>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Cross-Feature Compile-Time-Type-Map.
|
|
2
|
+
//
|
|
3
|
+
// Zweck: ctx.appendEvent / ctx.queryProjection / dispatcher.write gegen ein
|
|
4
|
+
// statisch bekanntes Schema-Bild prüfen, statt erst zur Boot- oder Runtime
|
|
5
|
+
// (zod-validate) zu scheitern. Designer/AI-Layer profitiert dadurch sofort:
|
|
6
|
+
// Autocomplete kennt alle Event-Typen aller geladenen Features, payload-
|
|
7
|
+
// Shape-Mismatches werden im Editor angezeigt, nicht erst beim Boot.
|
|
8
|
+
//
|
|
9
|
+
// Befüllung erfolgt per Feature über `declare module "@cosmicdrift/kumiko-framework/engine"`
|
|
10
|
+
// — entweder hand-geschrieben (für stabile Frameworks-Internals) oder vom
|
|
11
|
+
// Codegen-Skript erzeugt (für apps/bundled-features). Empty defaults sind
|
|
12
|
+
// kein Bug: ein Feature ohne Augmentation ist runtime-pluggable und nutzt
|
|
13
|
+
// die Fallback-Overload mit `unknown` payload.
|
|
14
|
+
//
|
|
15
|
+
// Pattern für hand-geschriebene Augmentation am File-Top:
|
|
16
|
+
//
|
|
17
|
+
// declare module "@cosmicdrift/kumiko-framework/engine" {
|
|
18
|
+
// interface KumikoEventTypeMap {
|
|
19
|
+
// "users:user.created": z.infer<typeof userCreatedSchema>;
|
|
20
|
+
// }
|
|
21
|
+
// }
|
|
22
|
+
|
|
23
|
+
// MUST be `interface` (not `type`): only interfaces support TS declaration-
|
|
24
|
+
// merging. Apps/features extend these via `declare module "@cosmicdrift/kumiko-framework/engine"`
|
|
25
|
+
// blocks. A `type X = {}` alias would silently break that augmentation channel.
|
|
26
|
+
|
|
27
|
+
// biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker — augmented per feature
|
|
28
|
+
export interface KumikoEventTypeMap {}
|
|
29
|
+
|
|
30
|
+
// biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
|
|
31
|
+
export interface KumikoEntityTypeMap {}
|
|
32
|
+
|
|
33
|
+
// biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
|
|
34
|
+
export interface KumikoHandlerPayloadMap {}
|
|
35
|
+
|
|
36
|
+
// biome-ignore lint/suspicious/noEmptyInterface: declaration-merging marker
|
|
37
|
+
export interface KumikoHandlerResultMap {}
|
package/src/geo-tz.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// GeoTzProvider — optional adapter for geo-position → IANA zone. The framework
|
|
2
|
+
// defines ONLY the interface + injection seam (ctx.tz.fromCoordinates /
|
|
3
|
+
// fromAddress); a concrete implementation ships in a separate package
|
|
4
|
+
// (e.g. a geo-tz-based offline package). v1 default: no provider — the
|
|
5
|
+
// fromCoordinates/fromAddress methods throw clearly instead of silently guessing.
|
|
6
|
+
//
|
|
7
|
+
// `fromCoordinates` is the PRIMARY method: offline geo-tz libs resolve
|
|
8
|
+
// lat/lng → zone (exact, offline, free) — they don't know postal addresses.
|
|
9
|
+
// `fromAddress` is optional, for providers backed by a geocoding API (address →
|
|
10
|
+
// zone, online). This split keeps the interface compatible with both
|
|
11
|
+
// provider classes instead of forcing an address shape the offline case
|
|
12
|
+
// can't serve at all.
|
|
13
|
+
|
|
14
|
+
export type GeoCoordinates = {
|
|
15
|
+
readonly latitude: number;
|
|
16
|
+
readonly longitude: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type GeoAddress = {
|
|
20
|
+
readonly street?: string;
|
|
21
|
+
readonly city?: string;
|
|
22
|
+
readonly region?: string;
|
|
23
|
+
readonly postalCode?: string;
|
|
24
|
+
readonly country?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type GeoTzProvider = {
|
|
28
|
+
/** Geo-coordinates → IANA zone (offline lat/lng → tz). */
|
|
29
|
+
readonly fromCoordinates: (coords: GeoCoordinates) => string | Promise<string>;
|
|
30
|
+
/** Optional: postal address → IANA zone (geocoding API provider). */
|
|
31
|
+
readonly fromAddress?: (address: GeoAddress) => string | Promise<string>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// HTTP-Route-Definition — feature-deklarierte HTTP-Endpoints außerhalb
|
|
2
|
+
// der /api/write|query|batch-Pipeline. Use-Case: RSS/Atom-Feeds, OpenAPI-
|
|
3
|
+
// Specs, OG-Image-Generators, Webhook-Receiver — alles wo der Feature-
|
|
4
|
+
// Author das Wire-Format selbst kontrolliert.
|
|
5
|
+
//
|
|
6
|
+
// Pattern symmetrisch zu r.queryHandler / r.writeHandler: Definition als
|
|
7
|
+
// Teil des Features (nicht des App-Bootstrapping). Phase-3 Multi-Tenant
|
|
8
|
+
// wird trivial weil tenant-context via host-resolution greift.
|
|
9
|
+
//
|
|
10
|
+
// Escape-hatch bleibt: runProdApp.extraRoutes für hand-rolled Routes die
|
|
11
|
+
// nichts mit einem Feature zu tun haben (z.B. plattform-spezifische
|
|
12
|
+
// Static-Serving-Logic).
|
|
13
|
+
|
|
14
|
+
import type { Context } from "hono";
|
|
15
|
+
|
|
16
|
+
/** Subset von HTTP-Methoden den wir aktiv unterstützen. Hono spricht
|
|
17
|
+
* alle, aber das hier sind die einzigen die ein Feature-Author
|
|
18
|
+
* realistisch deklariert. */
|
|
19
|
+
export type HttpRouteMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
|
|
20
|
+
|
|
21
|
+
/** Dependencies die der Handler vom Framework bekommt. App-Author kann
|
|
22
|
+
* die App selbst aufrufen (`deps.app.fetch(...)` für intern-call) oder
|
|
23
|
+
* direkt per dispatcher Daten ziehen. Db/Redis sind die rohen Connections
|
|
24
|
+
* — wer Tenant-Scope braucht muss durch dispatcher.query gehen.
|
|
25
|
+
*
|
|
26
|
+
* Hono-typing: `Context<any, any>` weil das Hono-Type-Param-Setup nur
|
|
27
|
+
* intern relevant ist. Concrete Hono-app wird im Boot-Path zugewiesen. */
|
|
28
|
+
export type HttpRouteHandlerDeps = {
|
|
29
|
+
/** Die Hono-app — Handler kann via app.fetch(...) interne Routes
|
|
30
|
+
* ansprechen (z.B. /api/query mit der vollen Auth-/Anonymous-Chain). */
|
|
31
|
+
// biome-ignore lint/suspicious/noExplicitAny: Hono's generic-Param ist im Framework-Boundary unsichtbar
|
|
32
|
+
readonly app: import("hono").Hono<any, any>;
|
|
33
|
+
/** Run a query handler in-process, forcing a SPECIFIC tenant — WITHOUT
|
|
34
|
+
* going through the public /api/query HTTP layer (no header parsing, no
|
|
35
|
+
* anonymousAccess tenant resolution). The synthesized caller carries
|
|
36
|
+
* anonymous-level access ONLY (same role a real anonymous request would
|
|
37
|
+
* have, no more) — the primitive forces the tenant, not the privilege
|
|
38
|
+
* level, so it stays safe to call from any `anonymous: true` route
|
|
39
|
+
* without risking a field-level disclosure a real anonymous caller
|
|
40
|
+
* couldn't already get. Use this whenever the route needs a tenant
|
|
41
|
+
* other than the one the request resolves to (e.g. always
|
|
42
|
+
* SYSTEM_TENANT_ID regardless of the visited host) — spoofing that via
|
|
43
|
+
* an internal X-Tenant header on `app.fetch(...)` is indistinguishable
|
|
44
|
+
* from an external client and gets rejected by resolverTrust:
|
|
45
|
+
* "authoritative" anonymousAccess configs (see auth-middleware.ts). */
|
|
46
|
+
readonly systemQuery: (
|
|
47
|
+
type: string,
|
|
48
|
+
payload: unknown,
|
|
49
|
+
tenantId: import("./identifiers").TenantId,
|
|
50
|
+
) => Promise<unknown>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type HttpRouteHandler = (
|
|
54
|
+
// biome-ignore lint/suspicious/noExplicitAny: Hono Context-Generics sind im Framework-Boundary unsichtbar
|
|
55
|
+
c: Context<any, any>,
|
|
56
|
+
deps: HttpRouteHandlerDeps,
|
|
57
|
+
) => Response | Promise<Response>;
|
|
58
|
+
|
|
59
|
+
export type HttpRouteDefinition = {
|
|
60
|
+
/** HTTP-Methode — bei Hono-Mount via app.{get,post,...}(path). */
|
|
61
|
+
readonly method: HttpRouteMethod;
|
|
62
|
+
/** URL-Pfad (Hono-Pattern, z.B. "/feed.xml" oder "/og/:tenantId.png"). */
|
|
63
|
+
readonly path: string;
|
|
64
|
+
/** Wenn true, bypasses die /api/*-Auth-Middleware. Default false —
|
|
65
|
+
* Routes liegen außerhalb /api/* und sehen die Auth-Middleware
|
|
66
|
+
* ohnehin nicht; das Flag ist semantisch (= "diese Route ist
|
|
67
|
+
* bewusst öffentlich") für Boot-Validator + Doku. */
|
|
68
|
+
readonly anonymous?: boolean;
|
|
69
|
+
/** Hono-Handler. Bekommt Hono-Context + Framework-Deps; returnt
|
|
70
|
+
* Response (sync oder async). */
|
|
71
|
+
readonly handler: HttpRouteHandler;
|
|
72
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Domain-identifier type aliases. Used everywhere a tenantId/userId/aggregateId
|
|
2
|
+
// travels through the framework. One declaration per concept so future
|
|
3
|
+
// representation changes (branded types, UUID validation, opaque wrappers)
|
|
4
|
+
// land in a single place.
|
|
5
|
+
|
|
6
|
+
// Tenant identifier — UUID string today. May become branded/opaque later
|
|
7
|
+
// without touching call sites.
|
|
8
|
+
export type TenantId = string;
|
|
9
|
+
|
|
10
|
+
// Lowercase UUID (any RFC-4122 variant). Strict enough to keep client-
|
|
11
|
+
// supplied junk (e.g. SQL fragments, path-traversal probes) out of the
|
|
12
|
+
// pipeline; loose enough that v4 / v7 / nil all match. Any caller that
|
|
13
|
+
// already holds a TenantId from a trusted source (JWT payload, server
|
|
14
|
+
// config) skips this — the helper is for **untrusted input** crossing
|
|
15
|
+
// the system boundary.
|
|
16
|
+
const TENANT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
17
|
+
|
|
18
|
+
// Validates a candidate string against the tenantId format and returns it
|
|
19
|
+
// as a TenantId, or `null` when it doesn't match. Use at every system
|
|
20
|
+
// boundary that admits untrusted input (HTTP headers, cookies, query
|
|
21
|
+
// params). Returning null instead of throwing keeps the caller in charge
|
|
22
|
+
// of the rejection shape — middleware returns 400, batch jobs may filter
|
|
23
|
+
// + log, and unit tests don't need a try/catch.
|
|
24
|
+
export function parseTenantId(value: unknown): TenantId | null {
|
|
25
|
+
if (typeof value !== "string") return null;
|
|
26
|
+
if (!TENANT_ID_REGEX.test(value)) return null;
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// "System-scope" tenant marker: handlers carry this tenantId when the event
|
|
31
|
+
// doesn't belong to any particular tenant (reference data, cross-tenant
|
|
32
|
+
// jobs, global config). The concrete UUID is a valid v4 (not all-zeroes —
|
|
33
|
+
// Postgres' UUID type rejects invalid variants), chosen to be easy to
|
|
34
|
+
// eyeball in logs. Central constant so call sites don't re-type the string
|
|
35
|
+
// and the isSystemTenant() check stays in sync.
|
|
36
|
+
export const SYSTEM_TENANT_ID: TenantId = "00000000-0000-4000-8000-000000000000";
|
|
37
|
+
|
|
38
|
+
export function isSystemTenant(tenantId: TenantId | null | undefined): boolean {
|
|
39
|
+
return !tenantId || tenantId === SYSTEM_TENANT_ID;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Primary-key identifier for any entity row. Two shapes coexist because of
|
|
43
|
+
// the entity-def `idType` switch: classic CRUD entities keep `serial` (number),
|
|
44
|
+
// while tenant + ES aggregates run on `uuid` (string). Call sites that pass
|
|
45
|
+
// the id through to the DB layer stay agnostic; only code that formats ids
|
|
46
|
+
// for URLs, logs, or cache keys needs `String(id)` — JS coerces both safely.
|
|
47
|
+
export type EntityId = number | string;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Logger = {
|
|
2
|
+
info(msg: string, data?: Record<string, unknown>): void;
|
|
3
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
4
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
5
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
6
|
+
child(context: Record<string, unknown>): Logger;
|
|
7
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Barrel for observability types. Split into span/metric/provider files so
|
|
2
|
+
// each module stays focused; consumers still import from "./types".
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
Counter,
|
|
6
|
+
Gauge,
|
|
7
|
+
Histogram,
|
|
8
|
+
Meter,
|
|
9
|
+
MetricDefinition,
|
|
10
|
+
MetricLabels,
|
|
11
|
+
MetricsHandle,
|
|
12
|
+
MetricType,
|
|
13
|
+
} from "./metric";
|
|
14
|
+
export type {
|
|
15
|
+
ObservabilityOptions,
|
|
16
|
+
ObservabilityProvider,
|
|
17
|
+
SamplingConfig,
|
|
18
|
+
SensitiveFilterConfig,
|
|
19
|
+
} from "./provider";
|
|
20
|
+
export type {
|
|
21
|
+
SerializedTraceContext,
|
|
22
|
+
Span,
|
|
23
|
+
SpanAttributes,
|
|
24
|
+
SpanAttributeValue,
|
|
25
|
+
SpanKind,
|
|
26
|
+
SpanStatus,
|
|
27
|
+
StartSpanOptions,
|
|
28
|
+
Tracer,
|
|
29
|
+
} from "./span";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Metric + Meter contract. Feature code interacts through ctx.metrics
|
|
2
|
+
// (MetricsHandle); the framework wires Counter/Histogram/Gauge behind that.
|
|
3
|
+
|
|
4
|
+
export type MetricLabels = Record<string, string | number | boolean>;
|
|
5
|
+
|
|
6
|
+
export type MetricType = "counter" | "histogram" | "gauge";
|
|
7
|
+
|
|
8
|
+
export type MetricDefinition = {
|
|
9
|
+
// Fully-qualified name (with kumiko_<feature>_ prefix already applied).
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly type: MetricType;
|
|
12
|
+
readonly description?: string;
|
|
13
|
+
// Declared label keys. Inc/observe calls with unknown label keys throw.
|
|
14
|
+
readonly labels?: readonly string[];
|
|
15
|
+
// Buckets only for histogram. If omitted, provider-default is used.
|
|
16
|
+
readonly buckets?: readonly number[];
|
|
17
|
+
readonly unit?: string;
|
|
18
|
+
// If true, the framework auto-injects tenant_id from the active ctx.
|
|
19
|
+
// Default false — adding tenant_id multiplies cardinality by tenant count.
|
|
20
|
+
readonly tenantLabel?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface Counter {
|
|
24
|
+
inc(value?: number, labels?: MetricLabels): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Histogram {
|
|
28
|
+
observe(value: number, labels?: MetricLabels): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Gauge {
|
|
32
|
+
set(value: number, labels?: MetricLabels): void;
|
|
33
|
+
inc(value?: number, labels?: MetricLabels): void;
|
|
34
|
+
dec(value?: number, labels?: MetricLabels): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface Meter {
|
|
38
|
+
// Called once per metric during boot. Duplicate names throw.
|
|
39
|
+
registerMetric(def: MetricDefinition): void;
|
|
40
|
+
// Lookup by fully-qualified name. Unknown names throw — typed access only.
|
|
41
|
+
counter(name: string): Counter;
|
|
42
|
+
histogram(name: string): Histogram;
|
|
43
|
+
gauge(name: string): Gauge;
|
|
44
|
+
// List of registered definitions — used by ctx.metrics to validate labels.
|
|
45
|
+
definitions(): ReadonlyMap<string, MetricDefinition>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Public handle for feature code — ctx.metrics.
|
|
49
|
+
// Name resolution uses the fully-qualified name (kumiko_<feature>_<short>).
|
|
50
|
+
// The registrar resolves the short name from the calling feature context
|
|
51
|
+
// at boot time; at handler time the map is ready.
|
|
52
|
+
export interface MetricsHandle {
|
|
53
|
+
inc(name: string, labels?: MetricLabels, value?: number): void;
|
|
54
|
+
observe(name: string, value: number, labels?: MetricLabels): void;
|
|
55
|
+
set(name: string, value: number, labels?: MetricLabels): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Observability provider contract — the "plug" that implementations (noop,
|
|
2
|
+
// console, otlp, prometheus) fulfil. Configuration types that consumers
|
|
3
|
+
// (buildServer, setupTestStack) hand in also live here.
|
|
4
|
+
|
|
5
|
+
import type { Meter } from "./metric";
|
|
6
|
+
import type { Tracer } from "./span";
|
|
7
|
+
|
|
8
|
+
export type SamplingConfig = {
|
|
9
|
+
// Base sampling rate 0..1. Default 1 in v1 (sample everything).
|
|
10
|
+
readonly tracing?: number;
|
|
11
|
+
readonly alwaysOnError?: boolean;
|
|
12
|
+
readonly alwaysOnSlow?: { readonly thresholdMs: number };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SensitiveFilterConfig = {
|
|
16
|
+
readonly redactedHeaders: readonly string[];
|
|
17
|
+
readonly redactedQueryParams: readonly string[];
|
|
18
|
+
readonly redactedAttributeKeyPatterns: readonly RegExp[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ObservabilityOptions = {
|
|
22
|
+
readonly sampling?: SamplingConfig;
|
|
23
|
+
readonly sensitiveFilter?: Partial<SensitiveFilterConfig>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export interface ObservabilityProvider {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly tracer: Tracer;
|
|
29
|
+
readonly meter: Meter;
|
|
30
|
+
// Graceful flush. Called from framework lifecycle shutdown.
|
|
31
|
+
shutdown(): Promise<void>;
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Span + Tracer contract. Provider implementations (noop, recording → console/otlp)
|
|
2
|
+
// all speak this — everything else (middleware, dispatcher, db, redis, jobs)
|
|
3
|
+
// is provider-agnostic.
|
|
4
|
+
|
|
5
|
+
export type SpanAttributeValue = string | number | boolean;
|
|
6
|
+
export type SpanAttributes = Record<string, SpanAttributeValue>;
|
|
7
|
+
|
|
8
|
+
export type SpanStatus = "unset" | "ok" | "error";
|
|
9
|
+
|
|
10
|
+
export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer";
|
|
11
|
+
|
|
12
|
+
// Serialized form of a trace context — what we pass across process boundaries
|
|
13
|
+
// (outbox row, BullMQ job payload). Matches W3C trace-context spec loosely,
|
|
14
|
+
// but staying minimal — a full W3C parser is v2.
|
|
15
|
+
export type SerializedTraceContext = {
|
|
16
|
+
readonly traceId: string;
|
|
17
|
+
readonly spanId: string;
|
|
18
|
+
readonly baggage?: Readonly<Record<string, string>>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type StartSpanOptions = {
|
|
22
|
+
// Either a live Span (normal in-process parent) or a serialized trace
|
|
23
|
+
// context (cross-process — outbox row, job payload). Both carry traceId
|
|
24
|
+
// and spanId, so the tracer reads them uniformly. Omitted → fall back to
|
|
25
|
+
// the AsyncLocalStorage active span.
|
|
26
|
+
readonly parent?: Span | SerializedTraceContext;
|
|
27
|
+
readonly attributes?: SpanAttributes;
|
|
28
|
+
readonly kind?: SpanKind;
|
|
29
|
+
readonly startTime?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface Span {
|
|
33
|
+
readonly traceId: string;
|
|
34
|
+
readonly spanId: string;
|
|
35
|
+
readonly parentSpanId: string | undefined;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
setAttribute(key: string, value: SpanAttributeValue): void;
|
|
38
|
+
setAttributes(attrs: SpanAttributes): void;
|
|
39
|
+
setStatus(status: SpanStatus, message?: string): void;
|
|
40
|
+
recordException(error: Error): void;
|
|
41
|
+
end(endTime?: number): void;
|
|
42
|
+
// Whether end() has been called. Idempotency guard for auto-wrappers.
|
|
43
|
+
readonly ended: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface Tracer {
|
|
47
|
+
startSpan(name: string, options?: StartSpanOptions): Span;
|
|
48
|
+
// Runs fn inside the span context (AsyncLocalStorage), ends the span
|
|
49
|
+
// automatically — including on thrown errors, where the error is recorded
|
|
50
|
+
// and status set to "error" before re-throwing.
|
|
51
|
+
withSpan<T>(
|
|
52
|
+
name: string,
|
|
53
|
+
optionsOrFn: StartSpanOptions | ((span: Span) => Promise<T>),
|
|
54
|
+
fn?: (span: Span) => Promise<T>,
|
|
55
|
+
): Promise<T>;
|
|
56
|
+
// Current active span from AsyncLocalStorage, or undefined.
|
|
57
|
+
getActiveSpan(): Span | undefined;
|
|
58
|
+
}
|
package/src/relations.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Keep in sync with OnDeleteStrategies in packages/framework/src/engine/constants.ts.
|
|
2
|
+
type OnDeleteStrategy = "cascade" | "restrict" | "setNull" | "nothing";
|
|
3
|
+
|
|
4
|
+
// --- Relations ---
|
|
5
|
+
|
|
6
|
+
export type BelongsToRelation = {
|
|
7
|
+
readonly type: "belongsTo";
|
|
8
|
+
readonly target: string;
|
|
9
|
+
readonly foreignKey: string;
|
|
10
|
+
readonly searchInclude?: readonly string[];
|
|
11
|
+
// onDelete is declared on the parent-side (hasMany / manyToMany) because
|
|
12
|
+
// that's where the "what happens to my children?" decision lives. A
|
|
13
|
+
// belongsTo node just points at a parent — the parent's onDelete drives
|
|
14
|
+
// the cleanup.
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type HasManyRelation = {
|
|
18
|
+
readonly type: "hasMany";
|
|
19
|
+
readonly target: string;
|
|
20
|
+
readonly foreignKey: string;
|
|
21
|
+
readonly onDelete?: OnDeleteStrategy;
|
|
22
|
+
// When true, a nested payload under this relation's key (e.g.
|
|
23
|
+
// `{ tasks: [{ ... }] }` on a `project:create` write) is auto-expanded
|
|
24
|
+
// into child writes: parent first, then one child-write per entry with
|
|
25
|
+
// the foreign key set to the parent's new id — all in the same TX.
|
|
26
|
+
// Opt-in (default false) so legacy hasMany relations that were declared
|
|
27
|
+
// purely for cascade-delete or UI-nav semantics don't silently gain a
|
|
28
|
+
// client-writable path. Children are never inferred from payload-shape
|
|
29
|
+
// alone; only relations with this flag unlock nested-write.
|
|
30
|
+
//
|
|
31
|
+
// Scope v1: depth=1, create-only, hasMany-only. Update-nested,
|
|
32
|
+
// delete-nested, and belongsTo/m2m auto-expansion are explicit future
|
|
33
|
+
// work — when they arrive, they'll take the same flag so the opt-in
|
|
34
|
+
// stays a single, consistent surface.
|
|
35
|
+
readonly nestedWrite?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ManyToManyRelation = {
|
|
39
|
+
readonly type: "manyToMany";
|
|
40
|
+
readonly target: string;
|
|
41
|
+
readonly through: {
|
|
42
|
+
readonly table: string;
|
|
43
|
+
readonly sourceKey: string;
|
|
44
|
+
readonly targetKey: string;
|
|
45
|
+
};
|
|
46
|
+
readonly searchInclude?: readonly string[];
|
|
47
|
+
readonly onDelete?: OnDeleteStrategy;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type RelationDefinition = BelongsToRelation | HasManyRelation | ManyToManyRelation;
|
|
51
|
+
|
|
52
|
+
export type EntityRelations = Readonly<Record<string, RelationDefinition>>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EntityId, TenantId } from "./identifiers";
|
|
2
|
+
|
|
3
|
+
export type SearchAdapterConfig = {
|
|
4
|
+
searchableFields: readonly string[];
|
|
5
|
+
rankingFields?: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SearchDocument = {
|
|
9
|
+
entityType: string;
|
|
10
|
+
entityId: EntityId;
|
|
11
|
+
weight: number;
|
|
12
|
+
fields: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SearchResult = {
|
|
16
|
+
entityType: string;
|
|
17
|
+
entityId: EntityId;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type SearchOptions = {
|
|
21
|
+
limit?: number;
|
|
22
|
+
filterType?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type SearchAdapter = {
|
|
26
|
+
configure(tenantId: TenantId, config: SearchAdapterConfig): Promise<void>;
|
|
27
|
+
index(tenantId: TenantId, doc: SearchDocument): Promise<void>;
|
|
28
|
+
search(tenantId: TenantId, query: string, options?: SearchOptions): Promise<SearchResult[]>;
|
|
29
|
+
remove(tenantId: TenantId, entityType: string, entityId: EntityId): Promise<void>;
|
|
30
|
+
// Bulk variants. Default implementations loop over the single-doc methods —
|
|
31
|
+
// adapters should override when the backend supports a real batch call
|
|
32
|
+
// (Meilisearch, Elasticsearch, Typesense all do). Cuts a batch-write from
|
|
33
|
+
// N sequential HTTP + waitTask round-trips to one.
|
|
34
|
+
indexBatch?(tenantId: TenantId, docs: readonly SearchDocument[]): Promise<void>;
|
|
35
|
+
removeBatch?(
|
|
36
|
+
tenantId: TenantId,
|
|
37
|
+
items: readonly { entityType: string; entityId: EntityId }[],
|
|
38
|
+
): Promise<void>;
|
|
39
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// TargetRef — runtime-Repräsentation eines typed buildTarget-Outputs.
|
|
2
|
+
// Wird vom Visual-Tree-Component (renderer-web) an einen Target-Resolver
|
|
3
|
+
// dispatcht; der Resolver findet die Editor-Maske via featureId.
|
|
4
|
+
//
|
|
5
|
+
// **Compile-time-Safety:** TargetRef wird niemals hand-getippt. Stattdessen
|
|
6
|
+
// erzeugt der typed buildTarget-Builder (engine/build-target.ts) einen
|
|
7
|
+
// TargetRef, dessen action + args gegen die treeActions-Map des Ziel-
|
|
8
|
+
// Features validiert sind.
|
|
9
|
+
//
|
|
10
|
+
// **Runtime:** args sind hier untyped (Record<string, unknown>), weil
|
|
11
|
+
// TargetRef die erased-runtime-Version ist. Der Resolver kennt das
|
|
12
|
+
// Ziel-Feature und kann args entsprechend casten — ähnlich wie Event-
|
|
13
|
+
// Payloads im Event-Store.
|
|
14
|
+
//
|
|
15
|
+
// Siehe docs/plans/architecture/visual-tree.md A5.
|
|
16
|
+
|
|
17
|
+
export type TargetRef = {
|
|
18
|
+
readonly featureId: string;
|
|
19
|
+
readonly action: string;
|
|
20
|
+
readonly args?: Readonly<Record<string, unknown>>;
|
|
21
|
+
};
|
package/src/tree-node.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// TreeNode — single Knoten im Client-navProvider-Tree. Provider liefern
|
|
2
|
+
// entweder statische readonly TreeNode[] oder dynamische TreeChildrenSubscribe.
|
|
3
|
+
//
|
|
4
|
+
// **Mental-Modell** (VS-Code-Explorer):
|
|
5
|
+
// [icon] [label] [...hover-actions]
|
|
6
|
+
// optional ein target zum Klicken (öffnet Editor-Maske via
|
|
7
|
+
// Target-Resolver) und optional children als nested tree.
|
|
8
|
+
//
|
|
9
|
+
// **State** markiert Visual-Modus für Skeleton-Pattern:
|
|
10
|
+
// - "filled" (default) — schwarz, Knoten hat Inhalt
|
|
11
|
+
// - "stub" — hellgrau, existing aber leer (Designer-Stub-File)
|
|
12
|
+
// - "empty" — Platzhalter für "+ create"-Affordance
|
|
13
|
+
// - "loading" — Children werden gerade aufgelöst
|
|
14
|
+
// - "error" — Provider hat Fehler emittiert
|
|
15
|
+
// Provider die kein Skeleton-Pattern brauchen müssen state nicht setzen.
|
|
16
|
+
//
|
|
17
|
+
// **Subscribe-Form** für dynamic Children: Provider erhält emit(),
|
|
18
|
+
// gibt unsubscribe() zurück. Initial-Emit synchron oder async, weitere
|
|
19
|
+
// Emits beliebig oft (z.B. wenn Entity-Row neu erscheint via SSE).
|
|
20
|
+
// Spielt natürlich mit existing SSE-Frame: ein Provider kann intern
|
|
21
|
+
// auf Entity-Update-Events abonnieren und bei Änderung emit() aufrufen.
|
|
22
|
+
|
|
23
|
+
import type { TargetRef } from "./target-ref";
|
|
24
|
+
|
|
25
|
+
export type TreeNodeState = "filled" | "stub" | "empty" | "loading" | "error";
|
|
26
|
+
|
|
27
|
+
export type TreeAction = {
|
|
28
|
+
// Icon-Key — vom Renderer-Icon-Registry interpretiert. Konvention
|
|
29
|
+
// matched NavDefinition.icon: unbekannte Icons surface als missing-icon
|
|
30
|
+
// im UI, nicht als Boot-Failure.
|
|
31
|
+
readonly icon: string;
|
|
32
|
+
// i18n-Translation-Key oder roher String. Vom Renderer aufgelöst, Engine
|
|
33
|
+
// behandelt opak (mirrors NavDefinition.label, WorkspaceDefinition.label).
|
|
34
|
+
readonly label: string;
|
|
35
|
+
// Klick-Ziel der Action. Pflicht — Action ohne target ist semantisch
|
|
36
|
+
// sinnlos (Hover-Icon das nichts tut).
|
|
37
|
+
readonly target: TargetRef;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type TreeNode = {
|
|
41
|
+
// i18n-Translation-Key oder roher String. Vom Renderer beim Rendern
|
|
42
|
+
// aufgelöst (siehe TreeAction.label).
|
|
43
|
+
readonly label: string;
|
|
44
|
+
// Optional. Icon links neben dem Label. Selbe Konvention wie
|
|
45
|
+
// TreeAction.icon — Renderer-Icon-Registry-Lookup.
|
|
46
|
+
readonly icon?: string;
|
|
47
|
+
// Visueller State für Skeleton-Pattern. Default "filled" (kein Eintrag
|
|
48
|
+
// ⇒ schwarz/normal). Wert-Semantik im Header-Comment dieser Datei.
|
|
49
|
+
readonly state?: TreeNodeState;
|
|
50
|
+
// Optional Klick-Ziel. Fehlt → reiner Container-Knoten (nur ausklappbar,
|
|
51
|
+
// nicht klickbar). Vorhanden → Klick öffnet die Editor-Maske via
|
|
52
|
+
// Target-Resolver in renderer-web.
|
|
53
|
+
readonly target?: TargetRef;
|
|
54
|
+
// Hover-Actions rechts (Add/Refresh/Delete/etc.). Werden in der
|
|
55
|
+
// Sidebar-Row erst bei Hover sichtbar — VS-Code-Pattern. Engine
|
|
56
|
+
// ordnet die Actions in der Reihenfolge an, in der sie hier stehen.
|
|
57
|
+
readonly actions?: readonly TreeAction[];
|
|
58
|
+
// Statische Children oder dynamic Subscribe-Function. Subscribe wird
|
|
59
|
+
// erst beim Ausklappen aufgerufen (lazy); die Function-Form erlaubt
|
|
60
|
+
// SSE-gefütterte Live-Updates wenn neue Entity-Rows reinkommen.
|
|
61
|
+
readonly children?: readonly TreeNode[] | TreeChildrenSubscribe;
|
|
62
|
+
// Provider-deklarierte „+ create"-Action für Knoten mit `state: "empty"`.
|
|
63
|
+
// Tree-Component zeigt automatisch ein „+"-Icon und dispatcht
|
|
64
|
+
// `createAction.target` bei Klick — Provider weiß was „leer befüllen"
|
|
65
|
+
// für ihn bedeutet (z.B. „neuer Page-Slug" vs „neue Entity-Row"),
|
|
66
|
+
// Convention könnte das nicht raten. Konsistent zu `state` (auch
|
|
67
|
+
// Provider-explizit). Siehe visual-tree.md V.1.1-Decision D3.
|
|
68
|
+
readonly createAction?: TreeAction;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Subscribe<T> — Provider implementiert: emit(initial); ...emit(updated);
|
|
72
|
+
// und gibt unsubscribe-Function zurück. Caller (Tree-Component) ruft
|
|
73
|
+
// unsubscribe auf wenn Knoten unmounted/eingeklappt wird.
|
|
74
|
+
//
|
|
75
|
+
// **V.1.4 emitError**: optional callback für async-error-Pfade (fetch-
|
|
76
|
+
// fail, SSE-disconnect). Provider die explizit Errors signalisieren
|
|
77
|
+
// wollen rufen `emitError(e)` statt empty-emit; VisualTree zeigt
|
|
78
|
+
// Error-Banner mit Retry-Button. Sync-Throws im Provider-Body werden
|
|
79
|
+
// vom useEffect-try/catch abgefangen — emitError ist nur für async.
|
|
80
|
+
export type Subscribe<T> = (
|
|
81
|
+
emit: (value: T) => void,
|
|
82
|
+
emitError?: (error: Error) => void,
|
|
83
|
+
) => () => void;
|
|
84
|
+
|
|
85
|
+
// TreeChildrenSubscribe — Lazy-Variante für dynamic Children. Wird
|
|
86
|
+
// erst aufgerufen wenn der Knoten im UI ausgeklappt wird. Kein ctx-
|
|
87
|
+
// Argument: Provider sind session-bound; Backend liest tenantId aus
|
|
88
|
+
// session bei jedem fetch/dispatch. V.1.1 hatte ein ctx mit tenantId,
|
|
89
|
+
// das aber im Browser nie echten Tenant trug (war auf SYSTEM_TENANT_ID
|
|
90
|
+
// gepinnt) und vom einzigen V.1.2-Consumer (text-content) ignoriert
|
|
91
|
+
// wurde. SR2-Rip 2026-05-18: Dead-API entfernt; wenn später ein
|
|
92
|
+
// Provider tenant-aware-rendern muss (z.B. cross-tenant-Dashboards
|
|
93
|
+
// für SystemAdmin), wird ctx mit echtem Tenant-Source aus dem Auth-
|
|
94
|
+
// Layer re-introduziert. YAGNI bis dahin.
|
|
95
|
+
export type TreeChildrenSubscribe = () => Subscribe<readonly TreeNode[]>;
|
|
96
|
+
|
|
97
|
+
// TreeActionDef — Schema-Eintrag pro Action in der treeActions-Map
|
|
98
|
+
// eines Features. Phase 0: Args sind ein optionales Type-Sample
|
|
99
|
+
// (kein Validator zur Laufzeit — Validation passiert compile-time
|
|
100
|
+
// via buildTarget-Generic, runtime via Editor-Panel-Schema).
|
|
101
|
+
//
|
|
102
|
+
// Lebt hier (nicht in build-target.ts) weil es konzeptuell zur
|
|
103
|
+
// Visual-Tree-Domäne gehört, nicht zum Builder. build-target.ts
|
|
104
|
+
// importiert den Type von hier.
|
|
105
|
+
export type TreeActionDef<TArgs = Record<string, unknown>> = {
|
|
106
|
+
readonly args?: TArgs;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// TreeActionsHandle<T> — Return-Type von r.treeActions(...). Trägt
|
|
110
|
+
// den literal-typed Action-Map durch das Feature-Export-System
|
|
111
|
+
// (siehe FeatureDefinition.exports + Memory `[EventDef-Exports-
|
|
112
|
+
// Pattern]`). Das ist die compile-time Bridge zu buildTarget:
|
|
113
|
+
//
|
|
114
|
+
// const handle = r.treeActions({ edit: { args: { slug: "" as string } } });
|
|
115
|
+
// // handle.id → TFeature (literal feature name)
|
|
116
|
+
// // handle.treeActions → { edit: { args: { slug: string } } } (literal-typed)
|
|
117
|
+
// buildTarget({ target: handle, action: "edit", args: { slug: "x" } });
|
|
118
|
+
// // ^^^^^^^^^^^^^^ ^^^^^^^^
|
|
119
|
+
// // literal-validated typed-validated
|
|
120
|
+
//
|
|
121
|
+
// Runtime-Lookup geht über FeatureDefinition.treeActions (erased Map),
|
|
122
|
+
// Compile-Time-Validation über diesen Handle.
|
|
123
|
+
export type TreeActionsHandle<
|
|
124
|
+
TFeature extends string,
|
|
125
|
+
TActions extends Record<string, TreeActionDef>,
|
|
126
|
+
> = {
|
|
127
|
+
readonly id: TFeature;
|
|
128
|
+
readonly treeActions: TActions;
|
|
129
|
+
};
|