@cosmicdrift/kumiko-framework 0.155.0 → 0.156.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/README.md +9 -38
- package/package.json +2 -2
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/raw-table.integration.test.ts +17 -40
- package/src/crypto/__tests__/pii-field-encryption.test.ts +77 -0
- package/src/crypto/index.ts +1 -0
- package/src/crypto/pii-field-encryption.ts +19 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +27 -1
- package/src/db/__tests__/blind-index.integration.test.ts +16 -0
- package/src/db/__tests__/collect-table-metas.test.ts +6 -6
- package/src/db/__tests__/feature-table-sources.test.ts +7 -7
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.test.ts +39 -0
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/entity-table-meta.ts +1 -1
- package/src/db/feature-table-sources.ts +5 -11
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/queries/shadow-swap.ts +85 -3
- package/src/db/tenant-db.ts +8 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +102 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +16 -0
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/raw-table.test.ts +134 -84
- package/src/engine/__tests__/registry.test.ts +40 -0
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +52 -2
- package/src/engine/boot-validator/action-wiring.ts +2 -2
- package/src/engine/boot-validator/config-deps.ts +35 -0
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/pii-retention.ts +35 -3
- package/src/engine/build-config-feature-schema.ts +3 -3
- package/src/engine/config-helpers.ts +2 -0
- package/src/engine/define-feature.ts +2 -6
- package/src/engine/entity-handlers.ts +2 -4
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +170 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +31 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +6 -1
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +222 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +32 -24
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/parse.ts +3 -3
- package/src/engine/feature-ast/render.ts +16 -11
- package/src/engine/feature-builder-state.ts +0 -3
- package/src/engine/feature-config-events-jobs.ts +3 -6
- package/src/engine/feature-entity-handlers.ts +9 -1
- package/src/engine/feature-ui-extensions.ts +10 -40
- package/src/engine/field-access.ts +13 -2
- package/src/engine/object-form.ts +15 -0
- package/src/engine/registry-facade.ts +4 -0
- package/src/engine/registry-ingest.ts +19 -29
- package/src/engine/registry-state.ts +1 -7
- package/src/engine/registry-validate.ts +5 -28
- package/src/engine/registry.ts +0 -2
- package/src/engine/types/config.ts +7 -0
- package/src/engine/types/feature.ts +40 -68
- package/src/engine/types/fields.ts +6 -0
- package/src/engine/types/index.ts +0 -3
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/pipeline/msp-rebuild.ts +4 -0
- package/src/pipeline/projection-rebuild.ts +35 -0
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +8 -1
- package/src/stack/test-stack.ts +17 -2
- package/src/ui-types/index.ts +1 -0
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -172
- package/src/engine/feature-ast/extractors/round4.ts +0 -1227
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { CallExpression, Node, SourceFile } from "ts-morph";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
import type { AccessRule, RateLimitOption } from "../../types/handlers";
|
|
4
|
+
import type { QueryHandlerPattern, WriteHandlerPattern } from "../patterns";
|
|
5
|
+
import type { SourceLocation } from "../source-location";
|
|
6
|
+
import { sourceLocationFromNode } from "../source-location";
|
|
7
|
+
import { readOptionalAccessRule, readOptionalRateLimit } from "./hooks";
|
|
8
|
+
import {
|
|
9
|
+
type ExtractOutput,
|
|
10
|
+
fail,
|
|
11
|
+
findFunctionLiteral,
|
|
12
|
+
isPlainObject,
|
|
13
|
+
isRawRefSentinel,
|
|
14
|
+
ok,
|
|
15
|
+
readBooleanProperty,
|
|
16
|
+
readDataLiteralNode,
|
|
17
|
+
} from "./shared";
|
|
18
|
+
|
|
19
|
+
export type ParsedHandlerCall = {
|
|
20
|
+
readonly source: SourceLocation;
|
|
21
|
+
readonly handlerName?: string;
|
|
22
|
+
readonly schemaSource?: SourceLocation;
|
|
23
|
+
readonly handlerBody?: SourceLocation;
|
|
24
|
+
readonly access?: AccessRule;
|
|
25
|
+
readonly rateLimit?: RateLimitOption;
|
|
26
|
+
readonly unsafeSkipTransitionGuard?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolves an argument standing in for a handler-call's object-form body:
|
|
31
|
+
* either it's already an object literal, or a bare identifier declared
|
|
32
|
+
* locally (same file only) with one as its initializer. Anything else
|
|
33
|
+
* (imported binding, factory call, ...) is not resolvable here.
|
|
34
|
+
*/
|
|
35
|
+
function resolveObjectLiteralArg(node: Node) {
|
|
36
|
+
const direct = node.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
37
|
+
if (direct) return direct;
|
|
38
|
+
const identifier = node.asKind(SyntaxKind.Identifier);
|
|
39
|
+
if (!identifier) return undefined;
|
|
40
|
+
const varDecl = node.getSourceFile().getVariableDeclaration(identifier.getText());
|
|
41
|
+
return varDecl?.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseHandlerCall(
|
|
45
|
+
call: CallExpression,
|
|
46
|
+
sourceFile: SourceFile,
|
|
47
|
+
methodName: "writeHandler" | "queryHandler",
|
|
48
|
+
): ExtractOutput<ParsedHandlerCall> {
|
|
49
|
+
const args = call.getArguments();
|
|
50
|
+
const first = args[0];
|
|
51
|
+
if (!first) {
|
|
52
|
+
return fail(
|
|
53
|
+
methodName,
|
|
54
|
+
sourceLocationFromNode(call, sourceFile),
|
|
55
|
+
"expected at least one argument",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const obj = args.length === 1 ? resolveObjectLiteralArg(first) : undefined;
|
|
60
|
+
if (obj) {
|
|
61
|
+
const nameLiteral = obj
|
|
62
|
+
.getProperty("name")
|
|
63
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
64
|
+
?.getInitializer()
|
|
65
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
66
|
+
if (!nameLiteral) {
|
|
67
|
+
return fail(
|
|
68
|
+
methodName,
|
|
69
|
+
sourceLocationFromNode(call, sourceFile),
|
|
70
|
+
"object form requires a string-literal `name` property",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const schemaInit = obj
|
|
74
|
+
.getProperty("schema")
|
|
75
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
76
|
+
?.getInitializer();
|
|
77
|
+
if (!schemaInit) {
|
|
78
|
+
return fail(
|
|
79
|
+
methodName,
|
|
80
|
+
sourceLocationFromNode(call, sourceFile),
|
|
81
|
+
"object form requires a `schema` property",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const handlerInit = obj
|
|
85
|
+
.getProperty("handler")
|
|
86
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
87
|
+
?.getInitializer();
|
|
88
|
+
if (!handlerInit) {
|
|
89
|
+
return fail(
|
|
90
|
+
methodName,
|
|
91
|
+
sourceLocationFromNode(call, sourceFile),
|
|
92
|
+
"object form requires a `handler` property",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
96
|
+
if (!fn) {
|
|
97
|
+
return fail(
|
|
98
|
+
methodName,
|
|
99
|
+
sourceLocationFromNode(call, sourceFile),
|
|
100
|
+
"handler must be an inline arrow function or function expression",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const accessInit = obj
|
|
104
|
+
.getProperty("access")
|
|
105
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
106
|
+
?.getInitializer();
|
|
107
|
+
const access = accessInit ? readOptionalAccessRule(readDataLiteralNode(accessInit)) : undefined;
|
|
108
|
+
const rateLimitInit = obj
|
|
109
|
+
.getProperty("rateLimit")
|
|
110
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
111
|
+
?.getInitializer();
|
|
112
|
+
const rateLimit = rateLimitInit
|
|
113
|
+
? readOptionalRateLimit(readDataLiteralNode(rateLimitInit))
|
|
114
|
+
: undefined;
|
|
115
|
+
const skip = readBooleanProperty(obj, "unsafeSkipTransitionGuard");
|
|
116
|
+
return ok({
|
|
117
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
118
|
+
handlerName: nameLiteral.getLiteralValue(),
|
|
119
|
+
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
120
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
121
|
+
...(access !== undefined && { access }),
|
|
122
|
+
...(rateLimit !== undefined && { rateLimit }),
|
|
123
|
+
...(skip === true && { unsafeSkipTransitionGuard: true }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// A single reference standing in for the whole handler argument set
|
|
128
|
+
// (`r.writeHandler(eventCreateHandler)`, `r.queryHandler(someQuery())`)
|
|
129
|
+
// that resolveObjectLiteralArg above couldn't turn into an object literal
|
|
130
|
+
// (imported binding, factory call, ...). Keep it recognised as this kind
|
|
131
|
+
// instead of ParseErroring — see #1007. Opaque: renderWriteHandler/
|
|
132
|
+
// renderQueryHandler re-emit `source.raw` verbatim when handlerName is
|
|
133
|
+
// undefined.
|
|
134
|
+
if (args.length === 1 && isRawRefSentinel(readDataLiteralNode(first))) {
|
|
135
|
+
return ok({ source: sourceLocationFromNode(call, sourceFile) });
|
|
136
|
+
}
|
|
137
|
+
const nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
138
|
+
if (!nameLiteral) {
|
|
139
|
+
return fail(
|
|
140
|
+
methodName,
|
|
141
|
+
sourceLocationFromNode(call, sourceFile),
|
|
142
|
+
"first argument must be a string literal handler name (or use the object form)",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const schemaArg = args[1];
|
|
146
|
+
if (!schemaArg) {
|
|
147
|
+
return fail(
|
|
148
|
+
methodName,
|
|
149
|
+
sourceLocationFromNode(call, sourceFile),
|
|
150
|
+
"expected a Zod schema as second argument",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const handlerArg = args[2];
|
|
154
|
+
if (!handlerArg) {
|
|
155
|
+
return fail(
|
|
156
|
+
methodName,
|
|
157
|
+
sourceLocationFromNode(call, sourceFile),
|
|
158
|
+
"expected a handler function as third argument",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const fn = findFunctionLiteral(handlerArg);
|
|
162
|
+
if (!fn) {
|
|
163
|
+
return fail(
|
|
164
|
+
methodName,
|
|
165
|
+
sourceLocationFromNode(call, sourceFile),
|
|
166
|
+
"third argument must be an inline arrow function or function expression",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const optionsArg = args[3];
|
|
170
|
+
let access: AccessRule | undefined;
|
|
171
|
+
let rateLimit: RateLimitOption | undefined;
|
|
172
|
+
if (optionsArg) {
|
|
173
|
+
const options = readDataLiteralNode(optionsArg);
|
|
174
|
+
if (isPlainObject(options)) {
|
|
175
|
+
access = readOptionalAccessRule(options["access"]);
|
|
176
|
+
rateLimit = readOptionalRateLimit(options["rateLimit"]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return ok({
|
|
180
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
181
|
+
handlerName: nameLiteral.getLiteralValue(),
|
|
182
|
+
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
183
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
184
|
+
...(access !== undefined && { access }),
|
|
185
|
+
...(rateLimit !== undefined && { rateLimit }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function extractWriteHandler(
|
|
190
|
+
call: CallExpression,
|
|
191
|
+
sourceFile: SourceFile,
|
|
192
|
+
): ExtractOutput<WriteHandlerPattern> {
|
|
193
|
+
const parsed = parseHandlerCall(call, sourceFile, "writeHandler");
|
|
194
|
+
if (parsed.kind === "error") return parsed;
|
|
195
|
+
return ok({
|
|
196
|
+
kind: "writeHandler",
|
|
197
|
+
source: parsed.pattern.source,
|
|
198
|
+
handlerName: parsed.pattern.handlerName,
|
|
199
|
+
schemaSource: parsed.pattern.schemaSource,
|
|
200
|
+
handlerBody: parsed.pattern.handlerBody,
|
|
201
|
+
...(parsed.pattern.access !== undefined && { access: parsed.pattern.access }),
|
|
202
|
+
...(parsed.pattern.rateLimit !== undefined && { rateLimit: parsed.pattern.rateLimit }),
|
|
203
|
+
...(parsed.pattern.unsafeSkipTransitionGuard === true && { unsafeSkipTransitionGuard: true }),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function extractQueryHandler(
|
|
208
|
+
call: CallExpression,
|
|
209
|
+
sourceFile: SourceFile,
|
|
210
|
+
): ExtractOutput<QueryHandlerPattern> {
|
|
211
|
+
const parsed = parseHandlerCall(call, sourceFile, "queryHandler");
|
|
212
|
+
if (parsed.kind === "error") return parsed;
|
|
213
|
+
return ok({
|
|
214
|
+
kind: "queryHandler",
|
|
215
|
+
source: parsed.pattern.source,
|
|
216
|
+
handlerName: parsed.pattern.handlerName,
|
|
217
|
+
schemaSource: parsed.pattern.schemaSource,
|
|
218
|
+
handlerBody: parsed.pattern.handlerBody,
|
|
219
|
+
...(parsed.pattern.access !== undefined && { access: parsed.pattern.access }),
|
|
220
|
+
...(parsed.pattern.rateLimit !== undefined && { rateLimit: parsed.pattern.rateLimit }),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import type { CallExpression, Node, SourceFile } from "ts-morph";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
import type { LifecycleHookType } from "../../constants";
|
|
4
|
+
import type { AccessRule, RateLimitOption } from "../../types/handlers";
|
|
5
|
+
import type { HookPhase } from "../../types/hooks";
|
|
6
|
+
import type { AuthClaimsPattern, HookPattern } from "../patterns";
|
|
7
|
+
import { sourceLocationFromNode } from "../source-location";
|
|
8
|
+
import {
|
|
9
|
+
type ExtractOutput,
|
|
10
|
+
fail,
|
|
11
|
+
findFunctionLiteral,
|
|
12
|
+
isPlainObject,
|
|
13
|
+
ok,
|
|
14
|
+
readDataLiteralNode,
|
|
15
|
+
readNameOrRef,
|
|
16
|
+
readNameOrRefOrList,
|
|
17
|
+
} from "./shared";
|
|
18
|
+
|
|
19
|
+
export function isHookType(value: string): value is LifecycleHookType | "validation" {
|
|
20
|
+
return (
|
|
21
|
+
value === "preSave" ||
|
|
22
|
+
value === "postSave" ||
|
|
23
|
+
value === "preDelete" ||
|
|
24
|
+
value === "postDelete" ||
|
|
25
|
+
value === "preQuery" ||
|
|
26
|
+
value === "validation"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readOptionalPhase(node: Node | undefined): HookPhase | undefined {
|
|
31
|
+
if (!node) return undefined;
|
|
32
|
+
const obj = readDataLiteralNode(node);
|
|
33
|
+
if (!isPlainObject(obj)) return undefined;
|
|
34
|
+
const phase = obj["phase"];
|
|
35
|
+
if (phase === "inTransaction" || phase === "afterCommit") return phase as HookPhase;
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readOptionalAccessRule(value: unknown): AccessRule | undefined {
|
|
40
|
+
if (!isPlainObject(value)) return undefined;
|
|
41
|
+
if (Array.isArray(value["roles"]) && value["roles"].every((r) => typeof r === "string")) {
|
|
42
|
+
return { roles: value["roles"] as readonly string[] };
|
|
43
|
+
}
|
|
44
|
+
if (value["openToAll"] === true) {
|
|
45
|
+
return { openToAll: true };
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readOptionalRateLimit(value: unknown): RateLimitOption | undefined {
|
|
51
|
+
if (!isPlainObject(value)) return undefined;
|
|
52
|
+
if (typeof value["per"] !== "string") return undefined;
|
|
53
|
+
if (typeof value["limit"] !== "number") return undefined;
|
|
54
|
+
if (typeof value["windowSeconds"] !== "number") return undefined;
|
|
55
|
+
return value as unknown as RateLimitOption;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// r.hook's target: a NameOrRef, a list of them, or an entity-wide
|
|
59
|
+
// `{ allOf: entityRef }` (replaces the old r.entityHook(type, entity, fn)).
|
|
60
|
+
// Checked first so a malformed `{ allOf }` doesn't silently fall through
|
|
61
|
+
// to being read as some other object shape.
|
|
62
|
+
function readHookTarget(
|
|
63
|
+
node: Node,
|
|
64
|
+
): string | readonly string[] | { readonly allOf: string } | undefined {
|
|
65
|
+
const obj = node.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
66
|
+
if (obj) {
|
|
67
|
+
const allOfProp = obj.getProperty("allOf")?.asKind(SyntaxKind.PropertyAssignment);
|
|
68
|
+
if (allOfProp) {
|
|
69
|
+
const initializer = allOfProp.getInitializer();
|
|
70
|
+
const entityName = initializer && readNameOrRef(initializer);
|
|
71
|
+
return entityName ? { allOf: entityName } : undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return readNameOrRefOrList(node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function extractHook(
|
|
78
|
+
call: CallExpression,
|
|
79
|
+
sourceFile: SourceFile,
|
|
80
|
+
): ExtractOutput<HookPattern> {
|
|
81
|
+
const args = call.getArguments();
|
|
82
|
+
const first = args[0];
|
|
83
|
+
if (!first) {
|
|
84
|
+
return fail("hook", sourceLocationFromNode(call, sourceFile), "expected at least one argument");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
88
|
+
if (obj && args.length === 1) {
|
|
89
|
+
const typeInit = obj
|
|
90
|
+
.getProperty("type")
|
|
91
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
92
|
+
?.getInitializer()
|
|
93
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
94
|
+
if (!typeInit) {
|
|
95
|
+
return fail(
|
|
96
|
+
"hook",
|
|
97
|
+
sourceLocationFromNode(call, sourceFile),
|
|
98
|
+
"object form requires a string-literal `type` property",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const hookType = typeInit.getLiteralValue();
|
|
102
|
+
if (!isHookType(hookType)) {
|
|
103
|
+
return fail(
|
|
104
|
+
"hook",
|
|
105
|
+
sourceLocationFromNode(call, sourceFile),
|
|
106
|
+
`hook type "${hookType}" is not one of the lifecycle types or "validation"`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const targetInit = obj
|
|
110
|
+
.getProperty("target")
|
|
111
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
112
|
+
?.getInitializer();
|
|
113
|
+
if (!targetInit) {
|
|
114
|
+
return fail(
|
|
115
|
+
"hook",
|
|
116
|
+
sourceLocationFromNode(call, sourceFile),
|
|
117
|
+
"object form requires a `target` property",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const target = readHookTarget(targetInit);
|
|
121
|
+
if (!target) {
|
|
122
|
+
return fail(
|
|
123
|
+
"hook",
|
|
124
|
+
sourceLocationFromNode(call, sourceFile),
|
|
125
|
+
"target must be a string literal, an inline { name } object, or an array",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const handlerInit = obj
|
|
129
|
+
.getProperty("handler")
|
|
130
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
131
|
+
?.getInitializer();
|
|
132
|
+
if (!handlerInit) {
|
|
133
|
+
return fail(
|
|
134
|
+
"hook",
|
|
135
|
+
sourceLocationFromNode(call, sourceFile),
|
|
136
|
+
"object form requires a `handler` property",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
140
|
+
if (!fn) {
|
|
141
|
+
return fail(
|
|
142
|
+
"hook",
|
|
143
|
+
sourceLocationFromNode(call, sourceFile),
|
|
144
|
+
"handler must be an inline arrow function or function expression",
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const phase = readOptionalPhase(obj);
|
|
148
|
+
return ok({
|
|
149
|
+
kind: "hook",
|
|
150
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
151
|
+
hookType,
|
|
152
|
+
target,
|
|
153
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
154
|
+
...(phase !== undefined && { phase }),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const typeArg = first.asKind(SyntaxKind.StringLiteral);
|
|
159
|
+
if (!typeArg) {
|
|
160
|
+
return fail(
|
|
161
|
+
"hook",
|
|
162
|
+
sourceLocationFromNode(call, sourceFile),
|
|
163
|
+
"first argument must be a string literal hook type (or use the object form)",
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const hookType = typeArg.getLiteralValue();
|
|
167
|
+
if (!isHookType(hookType)) {
|
|
168
|
+
return fail(
|
|
169
|
+
"hook",
|
|
170
|
+
sourceLocationFromNode(call, sourceFile),
|
|
171
|
+
`hook type "${hookType}" is not one of the lifecycle types or "validation"`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const targetArg = args[1];
|
|
175
|
+
if (!targetArg) {
|
|
176
|
+
return fail(
|
|
177
|
+
"hook",
|
|
178
|
+
sourceLocationFromNode(call, sourceFile),
|
|
179
|
+
"expected a target (NameOrRef or array) as second argument",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const target = readHookTarget(targetArg);
|
|
183
|
+
if (!target) {
|
|
184
|
+
return fail(
|
|
185
|
+
"hook",
|
|
186
|
+
sourceLocationFromNode(call, sourceFile),
|
|
187
|
+
"target must be a string literal, an inline { name } object, or an array",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const fnArg = args[2];
|
|
191
|
+
if (!fnArg) {
|
|
192
|
+
return fail(
|
|
193
|
+
"hook",
|
|
194
|
+
sourceLocationFromNode(call, sourceFile),
|
|
195
|
+
"expected a hook function as third argument",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const fn = findFunctionLiteral(fnArg);
|
|
199
|
+
if (!fn) {
|
|
200
|
+
return fail(
|
|
201
|
+
"hook",
|
|
202
|
+
sourceLocationFromNode(call, sourceFile),
|
|
203
|
+
"third argument must be an inline arrow function or function expression",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const phase = readOptionalPhase(args[3]);
|
|
207
|
+
return ok({
|
|
208
|
+
kind: "hook",
|
|
209
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
210
|
+
hookType,
|
|
211
|
+
target,
|
|
212
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
213
|
+
...(phase !== undefined && { phase }),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// guard:dup-ok — intentionale Parallele zu extractTree (round6); verschiedene Feature-AST-Extraktoren by design
|
|
218
|
+
export function extractAuthClaims(
|
|
219
|
+
call: CallExpression,
|
|
220
|
+
sourceFile: SourceFile,
|
|
221
|
+
): ExtractOutput<AuthClaimsPattern> {
|
|
222
|
+
const arg = call.getArguments()[0];
|
|
223
|
+
if (!arg) {
|
|
224
|
+
return fail(
|
|
225
|
+
"authClaims",
|
|
226
|
+
sourceLocationFromNode(call, sourceFile),
|
|
227
|
+
"expected a function as first argument",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
const fn = findFunctionLiteral(arg);
|
|
231
|
+
if (!fn) {
|
|
232
|
+
return fail(
|
|
233
|
+
"authClaims",
|
|
234
|
+
sourceLocationFromNode(call, sourceFile),
|
|
235
|
+
"first argument must be an inline arrow function or function expression",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return ok({
|
|
239
|
+
kind: "authClaims",
|
|
240
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
241
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
export {
|
|
2
|
+
extractDefineEvent,
|
|
3
|
+
extractNotification,
|
|
4
|
+
} from "./events";
|
|
5
|
+
export {
|
|
6
|
+
extractQueryHandler,
|
|
7
|
+
extractWriteHandler,
|
|
8
|
+
type ParsedHandlerCall,
|
|
9
|
+
parseHandlerCall,
|
|
10
|
+
} from "./handlers";
|
|
11
|
+
export {
|
|
12
|
+
extractAuthClaims,
|
|
13
|
+
extractHook,
|
|
14
|
+
isHookType,
|
|
15
|
+
readOptionalAccessRule,
|
|
16
|
+
readOptionalPhase,
|
|
17
|
+
readOptionalRateLimit,
|
|
18
|
+
} from "./hooks";
|
|
19
|
+
export {
|
|
20
|
+
extractHttpRoute,
|
|
21
|
+
extractJob,
|
|
22
|
+
isHttpRouteMethod,
|
|
23
|
+
} from "./jobs-routes";
|
|
24
|
+
export {
|
|
25
|
+
collectScreenOpaqueProps,
|
|
26
|
+
extractMultiStreamProjection,
|
|
27
|
+
extractProjection,
|
|
28
|
+
extractScreen,
|
|
29
|
+
readApplyBodies,
|
|
30
|
+
readScreenStatic,
|
|
31
|
+
} from "./projections-screens";
|
|
1
32
|
export {
|
|
2
33
|
extractDescribe,
|
|
3
34
|
extractOptionalRequires,
|
|
@@ -25,34 +56,11 @@ export {
|
|
|
25
56
|
type NamedOptionsResult,
|
|
26
57
|
readNamedOptions,
|
|
27
58
|
} from "./round3";
|
|
28
|
-
export {
|
|
29
|
-
collectScreenOpaqueProps,
|
|
30
|
-
extractAuthClaims,
|
|
31
|
-
extractDefineEvent,
|
|
32
|
-
extractHook,
|
|
33
|
-
extractHttpRoute,
|
|
34
|
-
extractJob,
|
|
35
|
-
extractMultiStreamProjection,
|
|
36
|
-
extractNotification,
|
|
37
|
-
extractProjection,
|
|
38
|
-
extractQueryHandler,
|
|
39
|
-
extractScreen,
|
|
40
|
-
extractWriteHandler,
|
|
41
|
-
isHookType,
|
|
42
|
-
isHttpRouteMethod,
|
|
43
|
-
type ParsedHandlerCall,
|
|
44
|
-
parseHandlerCall,
|
|
45
|
-
readApplyBodies,
|
|
46
|
-
readOptionalAccessRule,
|
|
47
|
-
readOptionalPhase,
|
|
48
|
-
readOptionalRateLimit,
|
|
49
|
-
readScreenStatic,
|
|
50
|
-
} from "./round4";
|
|
51
59
|
export {
|
|
52
60
|
extractEnvSchema,
|
|
53
61
|
extractExposesApi,
|
|
54
62
|
extractExtendsRegistrar,
|
|
55
|
-
|
|
63
|
+
extractRawTable,
|
|
56
64
|
extractUsesApi,
|
|
57
65
|
} from "./round5";
|
|
58
66
|
export { extractTreeActions } from "./round6";
|