@cosmicdrift/kumiko-framework 0.2.2 → 0.3.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/CHANGELOG.md +54 -0
- package/package.json +124 -38
- package/src/__tests__/full-stack.integration.ts +2 -2
- package/src/api/auth-routes.ts +5 -5
- package/src/api/jwt.ts +2 -2
- package/src/api/route-registrars.ts +1 -1
- package/src/api/routes.ts +3 -3
- package/src/api/server.ts +6 -7
- package/src/auth/__tests__/roles.test.ts +24 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/roles.ts +42 -0
- package/src/compliance/__tests__/duration-spec.test.ts +72 -0
- package/src/compliance/__tests__/profiles.test.ts +308 -0
- package/src/compliance/__tests__/sub-processors.test.ts +139 -0
- package/src/compliance/duration-spec.ts +44 -0
- package/src/compliance/index.ts +31 -0
- package/src/compliance/override-schema.ts +136 -0
- package/src/compliance/profiles.ts +427 -0
- package/src/compliance/sub-processors.ts +152 -0
- package/src/db/__tests__/big-int-field.test.ts +131 -0
- package/src/db/assert-exists-in.ts +2 -2
- package/src/db/cursor.ts +3 -3
- package/src/db/event-store-executor.ts +19 -13
- package/src/db/located-timestamp.ts +1 -1
- package/src/db/money.ts +12 -2
- package/src/db/pg-error.ts +1 -1
- package/src/db/row-helpers.ts +1 -1
- package/src/db/table-builder.ts +20 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
- package/src/engine/__tests__/build-target.test.ts +135 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +4 -4
- package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
- package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
- package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
- package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
- package/src/engine/__tests__/raw-table.test.ts +2 -2
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
- package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
- package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
- package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
- package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
- package/src/engine/__tests__/steps-read.test.ts +142 -0
- package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
- package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
- package/src/engine/__tests__/steps-workflow.test.ts +198 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
- package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
- package/src/engine/boot-validator/api-ext.ts +77 -0
- package/src/engine/boot-validator/config-deps.ts +163 -0
- package/src/engine/boot-validator/entity-handler.ts +466 -0
- package/src/engine/boot-validator/index.ts +159 -0
- package/src/engine/boot-validator/ownership.ts +198 -0
- package/src/engine/boot-validator/pii-retention.ts +155 -0
- package/src/engine/boot-validator/screens-nav.ts +624 -0
- package/src/engine/boot-validator.ts +1 -1528
- package/src/engine/build-app-schema.ts +1 -1
- package/src/engine/build-target.ts +99 -0
- package/src/engine/codemod/index.ts +15 -0
- package/src/engine/codemod/pipeline-codemod.ts +641 -0
- package/src/engine/config-helpers.ts +9 -19
- package/src/engine/constants.ts +1 -1
- package/src/engine/define-feature.ts +127 -9
- package/src/engine/define-handler.ts +89 -3
- package/src/engine/define-roles.ts +2 -2
- package/src/engine/define-step.ts +28 -0
- package/src/engine/define-workflow.ts +110 -0
- package/src/engine/entity-handlers.ts +10 -9
- package/src/engine/event-helpers.ts +4 -4
- package/src/engine/extension-names.ts +105 -0
- package/src/engine/extensions/user-data.ts +106 -0
- package/src/engine/factories.ts +26 -16
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
- package/src/engine/feature-ast/extractors/index.ts +74 -0
- package/src/engine/feature-ast/extractors/round1.ts +110 -0
- package/src/engine/feature-ast/extractors/round2.ts +253 -0
- package/src/engine/feature-ast/extractors/round3.ts +471 -0
- package/src/engine/feature-ast/extractors/round4.ts +1365 -0
- package/src/engine/feature-ast/extractors/round5.ts +72 -0
- package/src/engine/feature-ast/extractors/round6.ts +66 -0
- package/src/engine/feature-ast/extractors/shared.ts +177 -0
- package/src/engine/feature-ast/parse.ts +13 -0
- package/src/engine/feature-ast/patch.ts +9 -1
- package/src/engine/feature-ast/patcher.ts +10 -3
- package/src/engine/feature-ast/patterns.ts +71 -1
- package/src/engine/feature-ast/render.ts +31 -1
- package/src/engine/index.ts +66 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
- package/src/engine/pattern-library/library.ts +78 -2
- package/src/engine/pipeline.ts +88 -0
- package/src/engine/projection-helpers.ts +1 -1
- package/src/engine/read-claim.ts +1 -1
- package/src/engine/registry.ts +30 -2
- package/src/engine/resolve-config-or-param.ts +4 -0
- package/src/engine/run-pipeline.ts +162 -0
- package/src/engine/schema-builder.ts +10 -4
- package/src/engine/state-machine.ts +1 -1
- package/src/engine/steps/_drizzle-boundary.ts +19 -0
- package/src/engine/steps/_duration-utils.ts +33 -0
- package/src/engine/steps/_no-return-guard.ts +21 -0
- package/src/engine/steps/_resolver-utils.ts +42 -0
- package/src/engine/steps/_step-dispatch-constants.ts +38 -0
- package/src/engine/steps/aggregate-append-event.ts +56 -0
- package/src/engine/steps/aggregate-create.ts +56 -0
- package/src/engine/steps/aggregate-update.ts +68 -0
- package/src/engine/steps/branch.ts +84 -0
- package/src/engine/steps/call-feature.ts +49 -0
- package/src/engine/steps/compute.ts +41 -0
- package/src/engine/steps/for-each.ts +111 -0
- package/src/engine/steps/mail-send.ts +44 -0
- package/src/engine/steps/read-find-many.ts +51 -0
- package/src/engine/steps/read-find-one.ts +58 -0
- package/src/engine/steps/retry.ts +87 -0
- package/src/engine/steps/return.ts +34 -0
- package/src/engine/steps/unsafe-projection-delete.ts +46 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
- package/src/engine/steps/wait-for-event.ts +71 -0
- package/src/engine/steps/wait.ts +69 -0
- package/src/engine/steps/webhook-send.ts +71 -0
- package/src/engine/system-user.ts +1 -1
- package/src/engine/types/feature.ts +143 -1
- package/src/engine/types/fields.ts +134 -10
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/identifiers.ts +1 -0
- package/src/engine/types/index.ts +15 -1
- package/src/engine/types/step.ts +334 -0
- package/src/engine/types/target-ref.ts +21 -0
- package/src/engine/types/tree-node.ts +130 -0
- package/src/engine/types/workspace.ts +7 -0
- package/src/engine/validate-projection-allowlist.ts +161 -0
- package/src/event-store/snapshot.ts +1 -1
- package/src/event-store/upcaster-dead-letter.ts +1 -1
- package/src/event-store/upcaster.ts +1 -1
- package/src/files/__tests__/read-stream.test.ts +105 -0
- package/src/files/__tests__/write-stream.test.ts +233 -0
- package/src/files/__tests__/zip-stream.test.ts +357 -0
- package/src/files/file-routes.ts +1 -1
- package/src/files/in-memory-provider.ts +38 -0
- package/src/files/index.ts +3 -0
- package/src/files/local-provider.ts +58 -1
- package/src/files/types.ts +36 -8
- package/src/files/zip-stream.ts +251 -0
- package/src/jobs/job-runner.ts +10 -10
- package/src/lifecycle/lifecycle.ts +0 -3
- package/src/logging/index.ts +1 -0
- package/src/logging/pino-logger.ts +11 -7
- package/src/logging/utils.ts +24 -0
- package/src/observability/prometheus-meter.ts +7 -5
- package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
- package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
- package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
- package/src/pipeline/append-event-core.ts +22 -6
- package/src/pipeline/dispatcher-utils.ts +188 -0
- package/src/pipeline/dispatcher.ts +63 -283
- package/src/pipeline/distributed-lock.ts +1 -1
- package/src/pipeline/entity-cache.ts +2 -2
- package/src/pipeline/event-consumer-state.ts +0 -13
- package/src/pipeline/event-dispatcher.ts +4 -4
- package/src/pipeline/index.ts +0 -2
- package/src/pipeline/lifecycle-pipeline.ts +6 -12
- package/src/pipeline/msp-rebuild.ts +5 -5
- package/src/pipeline/multi-stream-apply-context.ts +6 -7
- package/src/pipeline/projection-rebuild.ts +2 -2
- package/src/pipeline/projection-state.ts +0 -12
- package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
- package/src/rate-limit/resolver.ts +1 -1
- package/src/search/in-memory-adapter.ts +1 -1
- package/src/search/meilisearch-adapter.ts +3 -3
- package/src/search/types.ts +1 -1
- package/src/secrets/leak-guard.ts +2 -2
- package/src/stack/request-helper.ts +9 -5
- package/src/stack/test-stack.ts +1 -1
- package/src/testing/handler-context.ts +4 -4
- package/src/testing/http-cookies.ts +1 -1
- package/src/time/tz-context.ts +1 -2
- package/src/ui-types/index.ts +4 -0
- package/src/engine/feature-ast/extractors.ts +0 -2562
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { CallExpression, SourceFile } from "ts-morph";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
import type { ExposesApiPattern, ExtendsRegistrarPattern, UsesApiPattern } from "../patterns";
|
|
4
|
+
import { sourceLocationFromNode } from "../source-location";
|
|
5
|
+
import { type ExtractOutput, fail, ok } from "./shared";
|
|
6
|
+
|
|
7
|
+
export function extractExtendsRegistrar(
|
|
8
|
+
call: CallExpression,
|
|
9
|
+
sourceFile: SourceFile,
|
|
10
|
+
): ExtractOutput<ExtendsRegistrarPattern> {
|
|
11
|
+
const args = call.getArguments();
|
|
12
|
+
const nameArg = args[0]?.asKind(SyntaxKind.StringLiteral);
|
|
13
|
+
if (!nameArg) {
|
|
14
|
+
return fail(
|
|
15
|
+
"extendsRegistrar",
|
|
16
|
+
sourceLocationFromNode(call, sourceFile),
|
|
17
|
+
"first argument must be a string literal extension name",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const defArg = args[1];
|
|
21
|
+
if (!defArg) {
|
|
22
|
+
return fail(
|
|
23
|
+
"extendsRegistrar",
|
|
24
|
+
sourceLocationFromNode(call, sourceFile),
|
|
25
|
+
"expected a definition argument",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return ok({
|
|
29
|
+
kind: "extendsRegistrar",
|
|
30
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
31
|
+
extensionName: nameArg.getLiteralValue(),
|
|
32
|
+
defBody: sourceLocationFromNode(defArg, sourceFile),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function extractUsesApi(
|
|
37
|
+
call: CallExpression,
|
|
38
|
+
sourceFile: SourceFile,
|
|
39
|
+
): ExtractOutput<UsesApiPattern> {
|
|
40
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
|
|
41
|
+
if (!arg) {
|
|
42
|
+
return fail(
|
|
43
|
+
"usesApi",
|
|
44
|
+
sourceLocationFromNode(call, sourceFile),
|
|
45
|
+
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return ok({
|
|
49
|
+
kind: "usesApi",
|
|
50
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
51
|
+
apiName: arg.getLiteralValue(),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function extractExposesApi(
|
|
56
|
+
call: CallExpression,
|
|
57
|
+
sourceFile: SourceFile,
|
|
58
|
+
): ExtractOutput<ExposesApiPattern> {
|
|
59
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
|
|
60
|
+
if (!arg) {
|
|
61
|
+
return fail(
|
|
62
|
+
"exposesApi",
|
|
63
|
+
sourceLocationFromNode(call, sourceFile),
|
|
64
|
+
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return ok({
|
|
68
|
+
kind: "exposesApi",
|
|
69
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
70
|
+
apiName: arg.getLiteralValue(),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CallExpression, SourceFile } from "ts-morph";
|
|
2
|
+
import type { TreeActionDef } from "../../types/tree-node";
|
|
3
|
+
import type { TreeActionsPattern, TreePattern } from "../patterns";
|
|
4
|
+
import { sourceLocationFromNode } from "../source-location";
|
|
5
|
+
import {
|
|
6
|
+
type ExtractOutput,
|
|
7
|
+
fail,
|
|
8
|
+
findFunctionLiteral,
|
|
9
|
+
isPlainObject,
|
|
10
|
+
ok,
|
|
11
|
+
readDataLiteralNode,
|
|
12
|
+
} from "./shared";
|
|
13
|
+
|
|
14
|
+
export function extractTreeActions(
|
|
15
|
+
call: CallExpression,
|
|
16
|
+
sourceFile: SourceFile,
|
|
17
|
+
): ExtractOutput<TreeActionsPattern> {
|
|
18
|
+
const arg = call.getArguments()[0];
|
|
19
|
+
if (!arg) {
|
|
20
|
+
return fail(
|
|
21
|
+
"treeActions",
|
|
22
|
+
sourceLocationFromNode(call, sourceFile),
|
|
23
|
+
"expected an action-map object literal as first argument",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const definitions = readDataLiteralNode(arg);
|
|
27
|
+
if (!isPlainObject(definitions)) {
|
|
28
|
+
return fail(
|
|
29
|
+
"treeActions",
|
|
30
|
+
sourceLocationFromNode(call, sourceFile),
|
|
31
|
+
"action-map could not be read as a plain object",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return ok({
|
|
35
|
+
kind: "treeActions",
|
|
36
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
37
|
+
definitions: definitions as Readonly<Record<string, TreeActionDef>>,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function extractTree(
|
|
42
|
+
call: CallExpression,
|
|
43
|
+
sourceFile: SourceFile,
|
|
44
|
+
): ExtractOutput<TreePattern> {
|
|
45
|
+
const arg = call.getArguments()[0];
|
|
46
|
+
if (!arg) {
|
|
47
|
+
return fail(
|
|
48
|
+
"tree",
|
|
49
|
+
sourceLocationFromNode(call, sourceFile),
|
|
50
|
+
"expected a tree-provider function as first argument",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const fn = findFunctionLiteral(arg);
|
|
54
|
+
if (!fn) {
|
|
55
|
+
return fail(
|
|
56
|
+
"tree",
|
|
57
|
+
sourceLocationFromNode(call, sourceFile),
|
|
58
|
+
"first argument must be an inline arrow function or function expression",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return ok({
|
|
62
|
+
kind: "tree",
|
|
63
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
64
|
+
providerBody: sourceLocationFromNode(fn, sourceFile),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import type { CallExpression, Node } from "ts-morph";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
import type { ParseError } from "../parse";
|
|
4
|
+
|
|
5
|
+
export type ExtractOutput<TPattern> =
|
|
6
|
+
| { readonly kind: "pattern"; readonly pattern: TPattern }
|
|
7
|
+
| { readonly kind: "error"; readonly error: ParseError };
|
|
8
|
+
|
|
9
|
+
export function ok<TPattern>(pattern: TPattern): ExtractOutput<TPattern> {
|
|
10
|
+
return { kind: "pattern", pattern };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function fail(
|
|
14
|
+
methodName: string,
|
|
15
|
+
source: ParseError["source"],
|
|
16
|
+
reason: string,
|
|
17
|
+
): { readonly kind: "error"; readonly error: ParseError } {
|
|
18
|
+
return { kind: "error", error: { methodName, source, reason } };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readStringLiteralArgs(call: CallExpression): readonly string[] | undefined {
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
for (const arg of call.getArguments()) {
|
|
24
|
+
const literal = arg.asKind(SyntaxKind.StringLiteral);
|
|
25
|
+
if (!literal) return undefined;
|
|
26
|
+
out.push(literal.getLiteralValue());
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readBooleanProperty(
|
|
32
|
+
objectLiteral: Node,
|
|
33
|
+
propertyName: string,
|
|
34
|
+
): boolean | undefined {
|
|
35
|
+
const obj = objectLiteral.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
36
|
+
if (!obj) return undefined;
|
|
37
|
+
const prop = obj.getProperty(propertyName);
|
|
38
|
+
if (!prop) return undefined;
|
|
39
|
+
const assignment = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
40
|
+
if (!assignment) return undefined;
|
|
41
|
+
const initializer = assignment.getInitializer();
|
|
42
|
+
if (!initializer) return undefined;
|
|
43
|
+
const kind = initializer.getKind();
|
|
44
|
+
if (kind === SyntaxKind.TrueKeyword) return true;
|
|
45
|
+
if (kind === SyntaxKind.FalseKeyword) return false;
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readDataLiteralNode(node: Node): unknown {
|
|
50
|
+
const kind = node.getKind();
|
|
51
|
+
switch (kind) {
|
|
52
|
+
case SyntaxKind.StringLiteral:
|
|
53
|
+
return node.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
|
|
54
|
+
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
55
|
+
return node.asKindOrThrow(SyntaxKind.NoSubstitutionTemplateLiteral).getLiteralValue();
|
|
56
|
+
case SyntaxKind.NumericLiteral:
|
|
57
|
+
return Number(node.asKindOrThrow(SyntaxKind.NumericLiteral).getText());
|
|
58
|
+
case SyntaxKind.TrueKeyword:
|
|
59
|
+
return true;
|
|
60
|
+
case SyntaxKind.FalseKeyword:
|
|
61
|
+
return false;
|
|
62
|
+
case SyntaxKind.NullKeyword:
|
|
63
|
+
return null;
|
|
64
|
+
case SyntaxKind.PrefixUnaryExpression: {
|
|
65
|
+
const expr = node.asKindOrThrow(SyntaxKind.PrefixUnaryExpression);
|
|
66
|
+
if (expr.getOperatorToken() !== SyntaxKind.MinusToken) return undefined;
|
|
67
|
+
const inner = readDataLiteralNode(expr.getOperand());
|
|
68
|
+
if (typeof inner !== "number") return undefined;
|
|
69
|
+
return -inner;
|
|
70
|
+
}
|
|
71
|
+
case SyntaxKind.ArrayLiteralExpression: {
|
|
72
|
+
const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
73
|
+
const out: unknown[] = [];
|
|
74
|
+
for (const el of arr.getElements()) {
|
|
75
|
+
const value = readDataLiteralNode(el);
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
out.push(value);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
case SyntaxKind.ObjectLiteralExpression: {
|
|
82
|
+
const obj = node.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
83
|
+
const out: Record<string, unknown> = {};
|
|
84
|
+
for (const prop of obj.getProperties()) {
|
|
85
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
86
|
+
if (!propAssign) return undefined;
|
|
87
|
+
const initializer = propAssign.getInitializer();
|
|
88
|
+
if (!initializer) return undefined;
|
|
89
|
+
const value = readDataLiteralNode(initializer);
|
|
90
|
+
if (value === undefined) return undefined;
|
|
91
|
+
out[readPropertyKey(propAssign)] = value;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
case SyntaxKind.AsExpression:
|
|
96
|
+
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.AsExpression).getExpression());
|
|
97
|
+
case SyntaxKind.SatisfiesExpression:
|
|
98
|
+
return readDataLiteralNode(
|
|
99
|
+
node.asKindOrThrow(SyntaxKind.SatisfiesExpression).getExpression(),
|
|
100
|
+
);
|
|
101
|
+
case SyntaxKind.ParenthesizedExpression:
|
|
102
|
+
return readDataLiteralNode(
|
|
103
|
+
node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression(),
|
|
104
|
+
);
|
|
105
|
+
default:
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
111
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function readPropertyKey(propAssign: import("ts-morph").PropertyAssignment): string {
|
|
115
|
+
const nameNode = propAssign.getNameNode();
|
|
116
|
+
const literal = nameNode.asKind(SyntaxKind.StringLiteral);
|
|
117
|
+
if (literal) return literal.getLiteralValue();
|
|
118
|
+
return propAssign.getName();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function readNameOrRef(node: Node): string | undefined {
|
|
122
|
+
const literal = node.asKind(SyntaxKind.StringLiteral);
|
|
123
|
+
if (literal) return literal.getLiteralValue();
|
|
124
|
+
const obj = readDataLiteralNode(node);
|
|
125
|
+
if (isPlainObject(obj) && typeof obj["name"] === "string") return obj["name"];
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function findFunctionLiteral(node: Node): Node | undefined {
|
|
130
|
+
if (node.getKind() === SyntaxKind.ArrowFunction) return node;
|
|
131
|
+
if (node.getKind() === SyntaxKind.FunctionExpression) return node;
|
|
132
|
+
const paren = node.asKind(SyntaxKind.ParenthesizedExpression);
|
|
133
|
+
if (paren) return findFunctionLiteral(paren.getExpression());
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readNameOrRefOrList(node: Node): string | readonly string[] | undefined {
|
|
138
|
+
const single = readNameOrRef(node);
|
|
139
|
+
if (single) return single;
|
|
140
|
+
const arr = node.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
141
|
+
if (!arr) return undefined;
|
|
142
|
+
const out: string[] = [];
|
|
143
|
+
for (const el of arr.getElements()) {
|
|
144
|
+
const name = readNameOrRef(el);
|
|
145
|
+
if (!name) return undefined;
|
|
146
|
+
out.push(name);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function readVarargsOrArrayProp(
|
|
152
|
+
call: CallExpression,
|
|
153
|
+
arrayPropName: "features" | "keys",
|
|
154
|
+
): readonly string[] | undefined {
|
|
155
|
+
const args = call.getArguments();
|
|
156
|
+
if (args.length === 1) {
|
|
157
|
+
const obj = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
158
|
+
if (obj) {
|
|
159
|
+
const propInit = obj
|
|
160
|
+
.getProperty(arrayPropName)
|
|
161
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
162
|
+
?.getInitializer();
|
|
163
|
+
if (propInit) {
|
|
164
|
+
const arr = propInit.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
165
|
+
if (!arr) return undefined;
|
|
166
|
+
const out: string[] = [];
|
|
167
|
+
for (const el of arr.getElements()) {
|
|
168
|
+
const lit = el.asKind(SyntaxKind.StringLiteral);
|
|
169
|
+
if (!lit) return undefined;
|
|
170
|
+
out.push(lit.getLiteralValue());
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return readStringLiteralArgs(call);
|
|
177
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
extractEntity,
|
|
34
34
|
extractEntityHook,
|
|
35
35
|
extractEventMigration,
|
|
36
|
+
extractExposesApi,
|
|
36
37
|
extractExtendsRegistrar,
|
|
37
38
|
extractHook,
|
|
38
39
|
extractHttpRoute,
|
|
@@ -53,7 +54,10 @@ import {
|
|
|
53
54
|
extractSystemScope,
|
|
54
55
|
extractToggleable,
|
|
55
56
|
extractTranslations,
|
|
57
|
+
extractTree,
|
|
58
|
+
extractTreeActions,
|
|
56
59
|
extractUseExtension,
|
|
60
|
+
extractUsesApi,
|
|
57
61
|
extractWorkspace,
|
|
58
62
|
extractWriteHandler,
|
|
59
63
|
} from "./extractors";
|
|
@@ -346,6 +350,15 @@ function dispatchExtractor(
|
|
|
346
350
|
// Round 5 — opaque patterns
|
|
347
351
|
case "extendsRegistrar":
|
|
348
352
|
return extractExtendsRegistrar(call, sourceFile);
|
|
353
|
+
case "usesApi":
|
|
354
|
+
return extractUsesApi(call, sourceFile);
|
|
355
|
+
case "exposesApi":
|
|
356
|
+
return extractExposesApi(call, sourceFile);
|
|
357
|
+
// Round 6 — Visual-Tree patterns
|
|
358
|
+
case "treeActions":
|
|
359
|
+
return extractTreeActions(call, sourceFile);
|
|
360
|
+
case "tree":
|
|
361
|
+
return extractTree(call, sourceFile);
|
|
349
362
|
// Unknown method — UnknownPattern signal so Designer/AI surface it
|
|
350
363
|
// as "custom call" without losing the source location.
|
|
351
364
|
default:
|
|
@@ -81,7 +81,9 @@ export type PatternId =
|
|
|
81
81
|
| { readonly kind: "toggleable" }
|
|
82
82
|
| { readonly kind: "config" }
|
|
83
83
|
| { readonly kind: "translations" }
|
|
84
|
-
| { readonly kind: "authClaims" }
|
|
84
|
+
| { readonly kind: "authClaims" }
|
|
85
|
+
| { readonly kind: "treeActions" }
|
|
86
|
+
| { readonly kind: "tree" };
|
|
85
87
|
|
|
86
88
|
// =============================================================================
|
|
87
89
|
// Change ops — generic apply API
|
|
@@ -271,6 +273,10 @@ export const SINGLETON_KINDS: ReadonlySet<PatternId["kind"]> = new Set([
|
|
|
271
273
|
"config",
|
|
272
274
|
"translations",
|
|
273
275
|
"authClaims",
|
|
276
|
+
// Visual-Tree slots — at-most-one per feature, mirrors the registrar's
|
|
277
|
+
// only-once-guard in define-feature.ts.
|
|
278
|
+
"treeActions",
|
|
279
|
+
"tree",
|
|
274
280
|
]);
|
|
275
281
|
|
|
276
282
|
/**
|
|
@@ -323,6 +329,8 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
323
329
|
case "config":
|
|
324
330
|
case "translations":
|
|
325
331
|
case "authClaims":
|
|
332
|
+
case "treeActions":
|
|
333
|
+
case "tree":
|
|
326
334
|
return true;
|
|
327
335
|
|
|
328
336
|
case "entity":
|
|
@@ -93,7 +93,7 @@ export type AddWriteHandlerArgs = {
|
|
|
93
93
|
readonly handlerSource: string;
|
|
94
94
|
readonly access?: AccessRule;
|
|
95
95
|
readonly rateLimit?: RateLimitOption;
|
|
96
|
-
readonly
|
|
96
|
+
readonly unsafeSkipTransitionGuard?: boolean;
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
export type AddQueryHandlerArgs = {
|
|
@@ -364,7 +364,14 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
364
364
|
add({ kind: "screen", source: SYNTHETIC_LOC, definition, opaqueProps });
|
|
365
365
|
},
|
|
366
366
|
|
|
367
|
-
addWriteHandler({
|
|
367
|
+
addWriteHandler({
|
|
368
|
+
name,
|
|
369
|
+
schemaSource,
|
|
370
|
+
handlerSource,
|
|
371
|
+
access,
|
|
372
|
+
rateLimit,
|
|
373
|
+
unsafeSkipTransitionGuard,
|
|
374
|
+
}) {
|
|
368
375
|
add({
|
|
369
376
|
kind: "writeHandler",
|
|
370
377
|
source: SYNTHETIC_LOC,
|
|
@@ -373,7 +380,7 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
|
|
|
373
380
|
handlerBody: rawLoc(handlerSource),
|
|
374
381
|
...(access !== undefined && { access }),
|
|
375
382
|
...(rateLimit !== undefined && { rateLimit }),
|
|
376
|
-
...(
|
|
383
|
+
...(unsafeSkipTransitionGuard === true && { unsafeSkipTransitionGuard: true }),
|
|
377
384
|
});
|
|
378
385
|
},
|
|
379
386
|
|
|
@@ -26,6 +26,28 @@
|
|
|
26
26
|
// **Naming convention.** Pattern `kind` matches the r.* method name
|
|
27
27
|
// 1:1 (e.g. `r.writeHandler` → `kind: "writeHandler"`). No kebab/camel
|
|
28
28
|
// translation layer.
|
|
29
|
+
//
|
|
30
|
+
// **Adding a new FeaturePattern kind — full consumer cascade.** The
|
|
31
|
+
// extension-point is wider than just this file + the parser. Update
|
|
32
|
+
// ALL of these when introducing a new r.* API, otherwise tests/checks
|
|
33
|
+
// catch the drift but the call-site jumps across files:
|
|
34
|
+
// 1. patterns.ts (this file): Pattern type + add to FeaturePattern
|
|
35
|
+
// union + getEditability switch
|
|
36
|
+
// 2. feature-ast/extractors.ts: extract<Kind> function + import in
|
|
37
|
+
// patterns-import-block
|
|
38
|
+
// 3. feature-ast/parse.ts: dispatcher case + import
|
|
39
|
+
// 4. feature-ast/render.ts: render<Kind> function + import + switch
|
|
40
|
+
// case
|
|
41
|
+
// 5. feature-ast/patch.ts: PatternId variant; if singleton-per-feature
|
|
42
|
+
// add to SINGLETON_KINDS; callMatchesId case
|
|
43
|
+
// 6. pattern-library/library.ts: <kind>Schema + entry in
|
|
44
|
+
// PATTERN_LIBRARY map
|
|
45
|
+
// 7. pattern-library/__tests__/library.test.ts: ALL_KINDS array +
|
|
46
|
+
// makePlaceholderPattern case
|
|
47
|
+
// TS-exhaustiveness catches most omissions automatically (1, 3, 4, 5,
|
|
48
|
+
// 7-via-makePlaceholderPattern), but the runtime-checked maps in 6 +
|
|
49
|
+
// the ALL_KINDS array in 7 are silent if forgotten — pin them with the
|
|
50
|
+
// library.test.ts coverage tests.
|
|
29
51
|
|
|
30
52
|
import type { LifecycleHookType } from "../constants";
|
|
31
53
|
import type {
|
|
@@ -45,6 +67,7 @@ import type { NavDefinition } from "../types/nav";
|
|
|
45
67
|
import type { MspErrorMode } from "../types/projection";
|
|
46
68
|
import type { RelationDefinition } from "../types/relations";
|
|
47
69
|
import type { ScreenDefinition } from "../types/screen";
|
|
70
|
+
import type { TreeActionDef } from "../types/tree-node";
|
|
48
71
|
import type { WorkspaceDefinition } from "../types/workspace";
|
|
49
72
|
import type { SourceLocation } from "./source-location";
|
|
50
73
|
|
|
@@ -158,6 +181,17 @@ export type UseExtensionPattern = {
|
|
|
158
181
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
159
182
|
};
|
|
160
183
|
|
|
184
|
+
// r.treeActions({ ... }) — Schema-Map für Visual-Tree-Action-Verben.
|
|
185
|
+
// Static: Args sind Type-Samples (kein Runtime-Validator), Designer
|
|
186
|
+
// rendert das als nested form pro Action. Compile-Time-Validation
|
|
187
|
+
// passiert via setup-export-Handle (TreeActionsHandle), nicht über
|
|
188
|
+
// dieses Pattern — das hier ist reine Runtime-Repräsentation.
|
|
189
|
+
export type TreeActionsPattern = {
|
|
190
|
+
readonly kind: "treeActions";
|
|
191
|
+
readonly source: SourceLocation;
|
|
192
|
+
readonly definitions: Readonly<Record<string, TreeActionDef>>;
|
|
193
|
+
};
|
|
194
|
+
|
|
161
195
|
// =============================================================================
|
|
162
196
|
// Mixed patterns — header (name/access/etc.) is declarative, body
|
|
163
197
|
// (handler/hook/apply/transform fn) is opaque. Designer renders the
|
|
@@ -202,7 +236,7 @@ export type WriteHandlerPattern = {
|
|
|
202
236
|
readonly handlerBody: SourceLocation;
|
|
203
237
|
readonly access?: AccessRule;
|
|
204
238
|
readonly rateLimit?: RateLimitOption;
|
|
205
|
-
readonly
|
|
239
|
+
readonly unsafeSkipTransitionGuard?: boolean;
|
|
206
240
|
};
|
|
207
241
|
|
|
208
242
|
export type QueryHandlerPattern = {
|
|
@@ -262,6 +296,16 @@ export type AuthClaimsPattern = {
|
|
|
262
296
|
readonly fnBody: SourceLocation;
|
|
263
297
|
};
|
|
264
298
|
|
|
299
|
+
// r.tree(provider) — Top-Level-Tree-Provider-Function. Closure-only,
|
|
300
|
+
// kein Header-Form. Designer rendert als read-only Code-Block, AI-
|
|
301
|
+
// Patcher überschreibt span verbatim. Konsistent mit r.authClaims —
|
|
302
|
+
// auch da ist die Function-Body die einzige Information.
|
|
303
|
+
export type TreePattern = {
|
|
304
|
+
readonly kind: "tree";
|
|
305
|
+
readonly source: SourceLocation;
|
|
306
|
+
readonly providerBody: SourceLocation;
|
|
307
|
+
};
|
|
308
|
+
|
|
265
309
|
export type HttpRoutePattern = {
|
|
266
310
|
readonly kind: "httpRoute";
|
|
267
311
|
readonly source: SourceLocation;
|
|
@@ -318,6 +362,24 @@ export type ExtendsRegistrarPattern = {
|
|
|
318
362
|
readonly defBody: SourceLocation;
|
|
319
363
|
};
|
|
320
364
|
|
|
365
|
+
// r.usesApi("a.b") — declarative cross-feature handler-ID dependency.
|
|
366
|
+
// Boot-validation throws if no other feature exposes the handler. Single
|
|
367
|
+
// string argument; pattern is purely declarative.
|
|
368
|
+
export type UsesApiPattern = {
|
|
369
|
+
readonly kind: "usesApi";
|
|
370
|
+
readonly source: SourceLocation;
|
|
371
|
+
readonly apiName: string;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// r.exposesApi("a.b") — declarative announcement that this feature
|
|
375
|
+
// provides a handler matching the cross-feature contract `a.b`. Single
|
|
376
|
+
// string argument; pattern is purely declarative.
|
|
377
|
+
export type ExposesApiPattern = {
|
|
378
|
+
readonly kind: "exposesApi";
|
|
379
|
+
readonly source: SourceLocation;
|
|
380
|
+
readonly apiName: string;
|
|
381
|
+
};
|
|
382
|
+
|
|
321
383
|
// =============================================================================
|
|
322
384
|
// Catch-all — r.* calls the visitor doesn't recognise. Designer renders
|
|
323
385
|
// "unknown call (cannot edit)", AI patcher leaves them unchanged. When
|
|
@@ -354,6 +416,9 @@ export type FeaturePattern =
|
|
|
354
416
|
| ReferenceDataPattern
|
|
355
417
|
| ReadsConfigPattern
|
|
356
418
|
| UseExtensionPattern
|
|
419
|
+
| UsesApiPattern
|
|
420
|
+
| ExposesApiPattern
|
|
421
|
+
| TreeActionsPattern
|
|
357
422
|
// Mixed
|
|
358
423
|
| ScreenPattern
|
|
359
424
|
| WriteHandlerPattern
|
|
@@ -369,6 +434,7 @@ export type FeaturePattern =
|
|
|
369
434
|
| DefineEventPattern
|
|
370
435
|
| EventMigrationPattern
|
|
371
436
|
| ExtendsRegistrarPattern
|
|
437
|
+
| TreePattern
|
|
372
438
|
// Catch-all
|
|
373
439
|
| UnknownPattern;
|
|
374
440
|
|
|
@@ -406,6 +472,9 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
406
472
|
case "referenceData":
|
|
407
473
|
case "readsConfig":
|
|
408
474
|
case "useExtension":
|
|
475
|
+
case "usesApi":
|
|
476
|
+
case "exposesApi":
|
|
477
|
+
case "treeActions":
|
|
409
478
|
return "static";
|
|
410
479
|
case "screen":
|
|
411
480
|
case "writeHandler":
|
|
@@ -422,6 +491,7 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
422
491
|
return "mixed";
|
|
423
492
|
case "authClaims":
|
|
424
493
|
case "extendsRegistrar":
|
|
494
|
+
case "tree":
|
|
425
495
|
case "unknown":
|
|
426
496
|
return "opaque";
|
|
427
497
|
default: {
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
EntityHookPattern,
|
|
25
25
|
EntityPattern,
|
|
26
26
|
EventMigrationPattern,
|
|
27
|
+
ExposesApiPattern,
|
|
27
28
|
ExtendsRegistrarPattern,
|
|
28
29
|
FeaturePattern,
|
|
29
30
|
HookPattern,
|
|
@@ -45,8 +46,11 @@ import type {
|
|
|
45
46
|
SystemScopePattern,
|
|
46
47
|
ToggleablePattern,
|
|
47
48
|
TranslationsPattern,
|
|
49
|
+
TreeActionsPattern,
|
|
50
|
+
TreePattern,
|
|
48
51
|
UnknownPattern,
|
|
49
52
|
UseExtensionPattern,
|
|
53
|
+
UsesApiPattern,
|
|
50
54
|
WorkspacePattern,
|
|
51
55
|
WriteHandlerPattern,
|
|
52
56
|
} from "./patterns";
|
|
@@ -122,6 +126,14 @@ export function renderPattern(pattern: FeaturePattern): string {
|
|
|
122
126
|
return renderEventMigration(pattern);
|
|
123
127
|
case "extendsRegistrar":
|
|
124
128
|
return renderExtendsRegistrar(pattern);
|
|
129
|
+
case "usesApi":
|
|
130
|
+
return renderUsesApi(pattern);
|
|
131
|
+
case "exposesApi":
|
|
132
|
+
return renderExposesApi(pattern);
|
|
133
|
+
case "treeActions":
|
|
134
|
+
return renderTreeActions(pattern);
|
|
135
|
+
case "tree":
|
|
136
|
+
return renderTree(pattern);
|
|
125
137
|
case "unknown":
|
|
126
138
|
return renderUnknown(pattern);
|
|
127
139
|
default: {
|
|
@@ -340,7 +352,7 @@ function renderWriteHandler(p: WriteHandlerPattern): string {
|
|
|
340
352
|
lines.push(` handler: ${reindentBody(p.handlerBody.raw, PATTERN_INDENT)},`);
|
|
341
353
|
if (p.access !== undefined) lines.push(` access: ${renderValue(p.access)},`);
|
|
342
354
|
if (p.rateLimit !== undefined) lines.push(` rateLimit: ${renderValue(p.rateLimit)},`);
|
|
343
|
-
if (p.
|
|
355
|
+
if (p.unsafeSkipTransitionGuard === true) lines.push(" unsafeSkipTransitionGuard: true,");
|
|
344
356
|
lines.push("});");
|
|
345
357
|
return lines.join("\n");
|
|
346
358
|
}
|
|
@@ -408,6 +420,16 @@ function renderAuthClaims(p: AuthClaimsPattern): string {
|
|
|
408
420
|
return `r.authClaims(${p.fnBody.raw});`;
|
|
409
421
|
}
|
|
410
422
|
|
|
423
|
+
// Visual-Tree patterns. treeActions is a static object-literal (mirrors
|
|
424
|
+
// renderWorkspace), tree is opaque-only (mirrors renderAuthClaims).
|
|
425
|
+
function renderTreeActions(p: TreeActionsPattern): string {
|
|
426
|
+
return `r.treeActions(${renderValue(p.definitions)});`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function renderTree(p: TreePattern): string {
|
|
430
|
+
return `r.tree(${p.providerBody.raw});`;
|
|
431
|
+
}
|
|
432
|
+
|
|
411
433
|
function renderHttpRoute(p: HttpRoutePattern): string {
|
|
412
434
|
const lines: string[] = ["r.httpRoute({"];
|
|
413
435
|
lines.push(` method: ${JSON.stringify(p.method)},`);
|
|
@@ -473,6 +495,14 @@ function renderExtendsRegistrar(p: ExtendsRegistrarPattern): string {
|
|
|
473
495
|
return `r.extendsRegistrar(${JSON.stringify(p.extensionName)}, ${p.defBody.raw});`;
|
|
474
496
|
}
|
|
475
497
|
|
|
498
|
+
function renderUsesApi(p: UsesApiPattern): string {
|
|
499
|
+
return `r.usesApi(${JSON.stringify(p.apiName)});`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderExposesApi(p: ExposesApiPattern): string {
|
|
503
|
+
return `r.exposesApi(${JSON.stringify(p.apiName)});`;
|
|
504
|
+
}
|
|
505
|
+
|
|
476
506
|
function renderUnknown(p: UnknownPattern): string {
|
|
477
507
|
// Round-trip preservation only: emit the raw call text from the
|
|
478
508
|
// SourceLocation so the rendered file stays semantically identical
|