@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,1365 @@
|
|
|
1
|
+
import type { CallExpression, Node, SourceFile } from "ts-morph";
|
|
2
|
+
import { SyntaxKind } from "ts-morph";
|
|
3
|
+
import type { LifecycleHookType } from "../../constants";
|
|
4
|
+
import type { JobDefinition, RunIn } from "../../types/config";
|
|
5
|
+
import type { AccessRule, RateLimitOption } from "../../types/handlers";
|
|
6
|
+
import type { HookPhase } from "../../types/hooks";
|
|
7
|
+
import type { HttpRouteMethod } from "../../types/http-route";
|
|
8
|
+
import type { MspErrorMode } from "../../types/projection";
|
|
9
|
+
import type { ScreenDefinition } from "../../types/screen";
|
|
10
|
+
import type {
|
|
11
|
+
AuthClaimsPattern,
|
|
12
|
+
DefineEventPattern,
|
|
13
|
+
EntityHookPattern,
|
|
14
|
+
EventMigrationPattern,
|
|
15
|
+
HookPattern,
|
|
16
|
+
HttpRoutePattern,
|
|
17
|
+
JobPattern,
|
|
18
|
+
MultiStreamProjectionPattern,
|
|
19
|
+
NotificationPattern,
|
|
20
|
+
OpaquePropMap,
|
|
21
|
+
ProjectionPattern,
|
|
22
|
+
QueryHandlerPattern,
|
|
23
|
+
ScreenPattern,
|
|
24
|
+
WriteHandlerPattern,
|
|
25
|
+
} from "../patterns";
|
|
26
|
+
import { SCREEN_OPAQUE_MARKER } from "../patterns";
|
|
27
|
+
import type { SourceLocation } from "../source-location";
|
|
28
|
+
import { sourceLocationFromNode } from "../source-location";
|
|
29
|
+
import {
|
|
30
|
+
type ExtractOutput,
|
|
31
|
+
fail,
|
|
32
|
+
findFunctionLiteral,
|
|
33
|
+
isPlainObject,
|
|
34
|
+
ok,
|
|
35
|
+
readBooleanProperty,
|
|
36
|
+
readDataLiteralNode,
|
|
37
|
+
readNameOrRef,
|
|
38
|
+
readNameOrRefOrList,
|
|
39
|
+
readPropertyKey,
|
|
40
|
+
} from "./shared";
|
|
41
|
+
|
|
42
|
+
export function isHookType(value: string): value is LifecycleHookType | "validation" {
|
|
43
|
+
return (
|
|
44
|
+
value === "preSave" ||
|
|
45
|
+
value === "postSave" ||
|
|
46
|
+
value === "preDelete" ||
|
|
47
|
+
value === "postDelete" ||
|
|
48
|
+
value === "preQuery" ||
|
|
49
|
+
value === "validation"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isHttpRouteMethod(value: string): value is HttpRouteMethod {
|
|
54
|
+
return (
|
|
55
|
+
value === "GET" ||
|
|
56
|
+
value === "POST" ||
|
|
57
|
+
value === "PUT" ||
|
|
58
|
+
value === "PATCH" ||
|
|
59
|
+
value === "DELETE" ||
|
|
60
|
+
value === "HEAD" ||
|
|
61
|
+
value === "OPTIONS"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readOptionalPhase(node: Node | undefined): HookPhase | undefined {
|
|
66
|
+
if (!node) return undefined;
|
|
67
|
+
const obj = readDataLiteralNode(node);
|
|
68
|
+
if (!isPlainObject(obj)) return undefined;
|
|
69
|
+
const phase = obj["phase"];
|
|
70
|
+
if (phase === "inTransaction" || phase === "afterCommit") return phase as HookPhase;
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function readOptionalAccessRule(value: unknown): AccessRule | undefined {
|
|
75
|
+
if (!isPlainObject(value)) return undefined;
|
|
76
|
+
if (Array.isArray(value["roles"]) && value["roles"].every((r) => typeof r === "string")) {
|
|
77
|
+
return { roles: value["roles"] as readonly string[] };
|
|
78
|
+
}
|
|
79
|
+
if (value["openToAll"] === true) {
|
|
80
|
+
return { openToAll: true };
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readOptionalRateLimit(value: unknown): RateLimitOption | undefined {
|
|
86
|
+
if (!isPlainObject(value)) return undefined;
|
|
87
|
+
if (typeof value["per"] !== "string") return undefined;
|
|
88
|
+
if (typeof value["limit"] !== "number") return undefined;
|
|
89
|
+
if (typeof value["windowSeconds"] !== "number") return undefined;
|
|
90
|
+
return value as unknown as RateLimitOption;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function extractHook(
|
|
94
|
+
call: CallExpression,
|
|
95
|
+
sourceFile: SourceFile,
|
|
96
|
+
): ExtractOutput<HookPattern> {
|
|
97
|
+
const args = call.getArguments();
|
|
98
|
+
const first = args[0];
|
|
99
|
+
if (!first) {
|
|
100
|
+
return fail("hook", sourceLocationFromNode(call, sourceFile), "expected at least one argument");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
104
|
+
if (obj && args.length === 1) {
|
|
105
|
+
const typeInit = obj
|
|
106
|
+
.getProperty("type")
|
|
107
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
108
|
+
?.getInitializer()
|
|
109
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
110
|
+
if (!typeInit) {
|
|
111
|
+
return fail(
|
|
112
|
+
"hook",
|
|
113
|
+
sourceLocationFromNode(call, sourceFile),
|
|
114
|
+
"object form requires a string-literal `type` property",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const hookType = typeInit.getLiteralValue();
|
|
118
|
+
if (!isHookType(hookType)) {
|
|
119
|
+
return fail(
|
|
120
|
+
"hook",
|
|
121
|
+
sourceLocationFromNode(call, sourceFile),
|
|
122
|
+
`hook type "${hookType}" is not one of the lifecycle types or "validation"`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const targetInit = obj
|
|
126
|
+
.getProperty("target")
|
|
127
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
128
|
+
?.getInitializer();
|
|
129
|
+
if (!targetInit) {
|
|
130
|
+
return fail(
|
|
131
|
+
"hook",
|
|
132
|
+
sourceLocationFromNode(call, sourceFile),
|
|
133
|
+
"object form requires a `target` property",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const target = readNameOrRefOrList(targetInit);
|
|
137
|
+
if (!target) {
|
|
138
|
+
return fail(
|
|
139
|
+
"hook",
|
|
140
|
+
sourceLocationFromNode(call, sourceFile),
|
|
141
|
+
"target must be a string literal, an inline { name } object, or an array",
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const handlerInit = obj
|
|
145
|
+
.getProperty("handler")
|
|
146
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
147
|
+
?.getInitializer();
|
|
148
|
+
if (!handlerInit) {
|
|
149
|
+
return fail(
|
|
150
|
+
"hook",
|
|
151
|
+
sourceLocationFromNode(call, sourceFile),
|
|
152
|
+
"object form requires a `handler` property",
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
156
|
+
if (!fn) {
|
|
157
|
+
return fail(
|
|
158
|
+
"hook",
|
|
159
|
+
sourceLocationFromNode(call, sourceFile),
|
|
160
|
+
"handler must be an inline arrow function or function expression",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const phase = readOptionalPhase(obj);
|
|
164
|
+
return ok({
|
|
165
|
+
kind: "hook",
|
|
166
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
167
|
+
hookType,
|
|
168
|
+
target,
|
|
169
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
170
|
+
...(phase !== undefined && { phase }),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const typeArg = first.asKind(SyntaxKind.StringLiteral);
|
|
175
|
+
if (!typeArg) {
|
|
176
|
+
return fail(
|
|
177
|
+
"hook",
|
|
178
|
+
sourceLocationFromNode(call, sourceFile),
|
|
179
|
+
"first argument must be a string literal hook type (or use the object form)",
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const hookType = typeArg.getLiteralValue();
|
|
183
|
+
if (!isHookType(hookType)) {
|
|
184
|
+
return fail(
|
|
185
|
+
"hook",
|
|
186
|
+
sourceLocationFromNode(call, sourceFile),
|
|
187
|
+
`hook type "${hookType}" is not one of the lifecycle types or "validation"`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const targetArg = args[1];
|
|
191
|
+
if (!targetArg) {
|
|
192
|
+
return fail(
|
|
193
|
+
"hook",
|
|
194
|
+
sourceLocationFromNode(call, sourceFile),
|
|
195
|
+
"expected a target (NameOrRef or array) as second argument",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const target = readNameOrRefOrList(targetArg);
|
|
199
|
+
if (!target) {
|
|
200
|
+
return fail(
|
|
201
|
+
"hook",
|
|
202
|
+
sourceLocationFromNode(call, sourceFile),
|
|
203
|
+
"target must be a string literal, an inline { name } object, or an array",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const fnArg = args[2];
|
|
207
|
+
if (!fnArg) {
|
|
208
|
+
return fail(
|
|
209
|
+
"hook",
|
|
210
|
+
sourceLocationFromNode(call, sourceFile),
|
|
211
|
+
"expected a hook function as third argument",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const fn = findFunctionLiteral(fnArg);
|
|
215
|
+
if (!fn) {
|
|
216
|
+
return fail(
|
|
217
|
+
"hook",
|
|
218
|
+
sourceLocationFromNode(call, sourceFile),
|
|
219
|
+
"third argument must be an inline arrow function or function expression",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const phase = readOptionalPhase(args[3]);
|
|
223
|
+
return ok({
|
|
224
|
+
kind: "hook",
|
|
225
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
226
|
+
hookType,
|
|
227
|
+
target,
|
|
228
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
229
|
+
...(phase !== undefined && { phase }),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function isEntityHookType(value: string): value is "postSave" | "preDelete" | "postDelete" {
|
|
234
|
+
return value === "postSave" || value === "preDelete" || value === "postDelete";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function extractEntityHook(
|
|
238
|
+
call: CallExpression,
|
|
239
|
+
sourceFile: SourceFile,
|
|
240
|
+
): ExtractOutput<EntityHookPattern> {
|
|
241
|
+
const args = call.getArguments();
|
|
242
|
+
const first = args[0];
|
|
243
|
+
if (!first) {
|
|
244
|
+
return fail(
|
|
245
|
+
"entityHook",
|
|
246
|
+
sourceLocationFromNode(call, sourceFile),
|
|
247
|
+
"expected at least one argument",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
252
|
+
if (obj && args.length === 1) {
|
|
253
|
+
const typeInit = obj
|
|
254
|
+
.getProperty("type")
|
|
255
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
256
|
+
?.getInitializer()
|
|
257
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
258
|
+
if (!typeInit) {
|
|
259
|
+
return fail(
|
|
260
|
+
"entityHook",
|
|
261
|
+
sourceLocationFromNode(call, sourceFile),
|
|
262
|
+
"object form requires a string-literal `type` property",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const hookType = typeInit.getLiteralValue();
|
|
266
|
+
if (!isEntityHookType(hookType)) {
|
|
267
|
+
return fail(
|
|
268
|
+
"entityHook",
|
|
269
|
+
sourceLocationFromNode(call, sourceFile),
|
|
270
|
+
`entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const entityInit = obj
|
|
274
|
+
.getProperty("entity")
|
|
275
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
276
|
+
?.getInitializer();
|
|
277
|
+
if (!entityInit) {
|
|
278
|
+
return fail(
|
|
279
|
+
"entityHook",
|
|
280
|
+
sourceLocationFromNode(call, sourceFile),
|
|
281
|
+
"object form requires an `entity` property",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const entityName = readNameOrRef(entityInit);
|
|
285
|
+
if (!entityName) {
|
|
286
|
+
return fail(
|
|
287
|
+
"entityHook",
|
|
288
|
+
sourceLocationFromNode(call, sourceFile),
|
|
289
|
+
"`entity` must be a string literal or inline { name } object",
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const handlerInit = obj
|
|
293
|
+
.getProperty("handler")
|
|
294
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
295
|
+
?.getInitializer();
|
|
296
|
+
if (!handlerInit) {
|
|
297
|
+
return fail(
|
|
298
|
+
"entityHook",
|
|
299
|
+
sourceLocationFromNode(call, sourceFile),
|
|
300
|
+
"object form requires a `handler` property",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
304
|
+
if (!fn) {
|
|
305
|
+
return fail(
|
|
306
|
+
"entityHook",
|
|
307
|
+
sourceLocationFromNode(call, sourceFile),
|
|
308
|
+
"handler must be an inline arrow function or function expression",
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const phase = readOptionalPhase(obj);
|
|
312
|
+
return ok({
|
|
313
|
+
kind: "entityHook",
|
|
314
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
315
|
+
hookType,
|
|
316
|
+
entityName,
|
|
317
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
318
|
+
...(phase !== undefined && { phase }),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const typeArg = first.asKind(SyntaxKind.StringLiteral);
|
|
323
|
+
if (!typeArg) {
|
|
324
|
+
return fail(
|
|
325
|
+
"entityHook",
|
|
326
|
+
sourceLocationFromNode(call, sourceFile),
|
|
327
|
+
"first argument must be a string literal hook type (or use the object form)",
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const hookType = typeArg.getLiteralValue();
|
|
331
|
+
if (!isEntityHookType(hookType)) {
|
|
332
|
+
return fail(
|
|
333
|
+
"entityHook",
|
|
334
|
+
sourceLocationFromNode(call, sourceFile),
|
|
335
|
+
`entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const entityArg = args[1];
|
|
339
|
+
if (!entityArg) {
|
|
340
|
+
return fail(
|
|
341
|
+
"entityHook",
|
|
342
|
+
sourceLocationFromNode(call, sourceFile),
|
|
343
|
+
"expected an entity reference as second argument",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
const entityName = readNameOrRef(entityArg);
|
|
347
|
+
if (!entityName) {
|
|
348
|
+
return fail(
|
|
349
|
+
"entityHook",
|
|
350
|
+
sourceLocationFromNode(call, sourceFile),
|
|
351
|
+
"second argument must be a string literal or inline { name } object",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
const fnArg = args[2];
|
|
355
|
+
if (!fnArg) {
|
|
356
|
+
return fail(
|
|
357
|
+
"entityHook",
|
|
358
|
+
sourceLocationFromNode(call, sourceFile),
|
|
359
|
+
"expected a hook function as third argument",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const fn = findFunctionLiteral(fnArg);
|
|
363
|
+
if (!fn) {
|
|
364
|
+
return fail(
|
|
365
|
+
"entityHook",
|
|
366
|
+
sourceLocationFromNode(call, sourceFile),
|
|
367
|
+
"third argument must be an inline arrow function or function expression",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const phase = readOptionalPhase(args[3]);
|
|
371
|
+
return ok({
|
|
372
|
+
kind: "entityHook",
|
|
373
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
374
|
+
hookType,
|
|
375
|
+
entityName,
|
|
376
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
377
|
+
...(phase !== undefined && { phase }),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function extractAuthClaims(
|
|
382
|
+
call: CallExpression,
|
|
383
|
+
sourceFile: SourceFile,
|
|
384
|
+
): ExtractOutput<AuthClaimsPattern> {
|
|
385
|
+
const arg = call.getArguments()[0];
|
|
386
|
+
if (!arg) {
|
|
387
|
+
return fail(
|
|
388
|
+
"authClaims",
|
|
389
|
+
sourceLocationFromNode(call, sourceFile),
|
|
390
|
+
"expected a function as first argument",
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const fn = findFunctionLiteral(arg);
|
|
394
|
+
if (!fn) {
|
|
395
|
+
return fail(
|
|
396
|
+
"authClaims",
|
|
397
|
+
sourceLocationFromNode(call, sourceFile),
|
|
398
|
+
"first argument must be an inline arrow function or function expression",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return ok({
|
|
402
|
+
kind: "authClaims",
|
|
403
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
404
|
+
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export type ParsedHandlerCall = {
|
|
409
|
+
readonly source: SourceLocation;
|
|
410
|
+
readonly handlerName: string;
|
|
411
|
+
readonly schemaSource: SourceLocation;
|
|
412
|
+
readonly handlerBody: SourceLocation;
|
|
413
|
+
readonly access?: AccessRule;
|
|
414
|
+
readonly rateLimit?: RateLimitOption;
|
|
415
|
+
readonly unsafeSkipTransitionGuard?: boolean;
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
export function parseHandlerCall(
|
|
419
|
+
call: CallExpression,
|
|
420
|
+
sourceFile: SourceFile,
|
|
421
|
+
methodName: "writeHandler" | "queryHandler",
|
|
422
|
+
): ExtractOutput<ParsedHandlerCall> {
|
|
423
|
+
const args = call.getArguments();
|
|
424
|
+
const first = args[0];
|
|
425
|
+
if (!first) {
|
|
426
|
+
return fail(
|
|
427
|
+
methodName,
|
|
428
|
+
sourceLocationFromNode(call, sourceFile),
|
|
429
|
+
"expected at least one argument",
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
434
|
+
if (obj && args.length === 1) {
|
|
435
|
+
const nameLiteral = obj
|
|
436
|
+
.getProperty("name")
|
|
437
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
438
|
+
?.getInitializer()
|
|
439
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
440
|
+
if (!nameLiteral) {
|
|
441
|
+
return fail(
|
|
442
|
+
methodName,
|
|
443
|
+
sourceLocationFromNode(call, sourceFile),
|
|
444
|
+
"object form requires a string-literal `name` property",
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const schemaInit = obj
|
|
448
|
+
.getProperty("schema")
|
|
449
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
450
|
+
?.getInitializer();
|
|
451
|
+
if (!schemaInit) {
|
|
452
|
+
return fail(
|
|
453
|
+
methodName,
|
|
454
|
+
sourceLocationFromNode(call, sourceFile),
|
|
455
|
+
"object form requires a `schema` property",
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
const handlerInit = obj
|
|
459
|
+
.getProperty("handler")
|
|
460
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
461
|
+
?.getInitializer();
|
|
462
|
+
if (!handlerInit) {
|
|
463
|
+
return fail(
|
|
464
|
+
methodName,
|
|
465
|
+
sourceLocationFromNode(call, sourceFile),
|
|
466
|
+
"object form requires a `handler` property",
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
470
|
+
if (!fn) {
|
|
471
|
+
return fail(
|
|
472
|
+
methodName,
|
|
473
|
+
sourceLocationFromNode(call, sourceFile),
|
|
474
|
+
"handler must be an inline arrow function or function expression",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
const accessInit = obj
|
|
478
|
+
.getProperty("access")
|
|
479
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
480
|
+
?.getInitializer();
|
|
481
|
+
const access = accessInit ? readOptionalAccessRule(readDataLiteralNode(accessInit)) : undefined;
|
|
482
|
+
const rateLimitInit = obj
|
|
483
|
+
.getProperty("rateLimit")
|
|
484
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
485
|
+
?.getInitializer();
|
|
486
|
+
const rateLimit = rateLimitInit
|
|
487
|
+
? readOptionalRateLimit(readDataLiteralNode(rateLimitInit))
|
|
488
|
+
: undefined;
|
|
489
|
+
const skip = readBooleanProperty(obj, "unsafeSkipTransitionGuard");
|
|
490
|
+
return ok({
|
|
491
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
492
|
+
handlerName: nameLiteral.getLiteralValue(),
|
|
493
|
+
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
494
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
495
|
+
...(access !== undefined && { access }),
|
|
496
|
+
...(rateLimit !== undefined && { rateLimit }),
|
|
497
|
+
...(skip === true && { unsafeSkipTransitionGuard: true }),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
502
|
+
if (!nameLiteral) {
|
|
503
|
+
return fail(
|
|
504
|
+
methodName,
|
|
505
|
+
sourceLocationFromNode(call, sourceFile),
|
|
506
|
+
"first argument must be a string literal handler name (or use the object form)",
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const schemaArg = args[1];
|
|
510
|
+
if (!schemaArg) {
|
|
511
|
+
return fail(
|
|
512
|
+
methodName,
|
|
513
|
+
sourceLocationFromNode(call, sourceFile),
|
|
514
|
+
"expected a Zod schema as second argument",
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const handlerArg = args[2];
|
|
518
|
+
if (!handlerArg) {
|
|
519
|
+
return fail(
|
|
520
|
+
methodName,
|
|
521
|
+
sourceLocationFromNode(call, sourceFile),
|
|
522
|
+
"expected a handler function as third argument",
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const fn = findFunctionLiteral(handlerArg);
|
|
526
|
+
if (!fn) {
|
|
527
|
+
return fail(
|
|
528
|
+
methodName,
|
|
529
|
+
sourceLocationFromNode(call, sourceFile),
|
|
530
|
+
"third argument must be an inline arrow function or function expression",
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const optionsArg = args[3];
|
|
534
|
+
let access: AccessRule | undefined;
|
|
535
|
+
let rateLimit: RateLimitOption | undefined;
|
|
536
|
+
if (optionsArg) {
|
|
537
|
+
const options = readDataLiteralNode(optionsArg);
|
|
538
|
+
if (isPlainObject(options)) {
|
|
539
|
+
access = readOptionalAccessRule(options["access"]);
|
|
540
|
+
rateLimit = readOptionalRateLimit(options["rateLimit"]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return ok({
|
|
544
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
545
|
+
handlerName: nameLiteral.getLiteralValue(),
|
|
546
|
+
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
547
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
548
|
+
...(access !== undefined && { access }),
|
|
549
|
+
...(rateLimit !== undefined && { rateLimit }),
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function extractWriteHandler(
|
|
554
|
+
call: CallExpression,
|
|
555
|
+
sourceFile: SourceFile,
|
|
556
|
+
): ExtractOutput<WriteHandlerPattern> {
|
|
557
|
+
const parsed = parseHandlerCall(call, sourceFile, "writeHandler");
|
|
558
|
+
if (parsed.kind === "error") return parsed;
|
|
559
|
+
return ok({
|
|
560
|
+
kind: "writeHandler",
|
|
561
|
+
source: parsed.pattern.source,
|
|
562
|
+
handlerName: parsed.pattern.handlerName,
|
|
563
|
+
schemaSource: parsed.pattern.schemaSource,
|
|
564
|
+
handlerBody: parsed.pattern.handlerBody,
|
|
565
|
+
...(parsed.pattern.access !== undefined && { access: parsed.pattern.access }),
|
|
566
|
+
...(parsed.pattern.rateLimit !== undefined && { rateLimit: parsed.pattern.rateLimit }),
|
|
567
|
+
...(parsed.pattern.unsafeSkipTransitionGuard === true && { unsafeSkipTransitionGuard: true }),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function extractQueryHandler(
|
|
572
|
+
call: CallExpression,
|
|
573
|
+
sourceFile: SourceFile,
|
|
574
|
+
): ExtractOutput<QueryHandlerPattern> {
|
|
575
|
+
const parsed = parseHandlerCall(call, sourceFile, "queryHandler");
|
|
576
|
+
if (parsed.kind === "error") return parsed;
|
|
577
|
+
return ok({
|
|
578
|
+
kind: "queryHandler",
|
|
579
|
+
source: parsed.pattern.source,
|
|
580
|
+
handlerName: parsed.pattern.handlerName,
|
|
581
|
+
schemaSource: parsed.pattern.schemaSource,
|
|
582
|
+
handlerBody: parsed.pattern.handlerBody,
|
|
583
|
+
...(parsed.pattern.access !== undefined && { access: parsed.pattern.access }),
|
|
584
|
+
...(parsed.pattern.rateLimit !== undefined && { rateLimit: parsed.pattern.rateLimit }),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function extractJob(
|
|
589
|
+
call: CallExpression,
|
|
590
|
+
sourceFile: SourceFile,
|
|
591
|
+
): ExtractOutput<JobPattern> {
|
|
592
|
+
const args = call.getArguments();
|
|
593
|
+
const first = args[0];
|
|
594
|
+
if (!first) {
|
|
595
|
+
return fail("job", sourceLocationFromNode(call, sourceFile), "expected at least one argument");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
599
|
+
if (obj && args.length === 1) {
|
|
600
|
+
const nameInit = obj
|
|
601
|
+
.getProperty("name")
|
|
602
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
603
|
+
?.getInitializer()
|
|
604
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
605
|
+
if (!nameInit) {
|
|
606
|
+
return fail(
|
|
607
|
+
"job",
|
|
608
|
+
sourceLocationFromNode(call, sourceFile),
|
|
609
|
+
"object form requires a string-literal `name` property",
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const handlerInit = obj
|
|
613
|
+
.getProperty("handler")
|
|
614
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
615
|
+
?.getInitializer();
|
|
616
|
+
if (!handlerInit) {
|
|
617
|
+
return fail(
|
|
618
|
+
"job",
|
|
619
|
+
sourceLocationFromNode(call, sourceFile),
|
|
620
|
+
"object form requires a `handler` property",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
624
|
+
if (!fn) {
|
|
625
|
+
return fail(
|
|
626
|
+
"job",
|
|
627
|
+
sourceLocationFromNode(call, sourceFile),
|
|
628
|
+
"handler must be an inline arrow function or function expression",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const optionsBag: Record<string, unknown> = {};
|
|
632
|
+
for (const prop of obj.getProperties()) {
|
|
633
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
634
|
+
if (!propAssign) continue;
|
|
635
|
+
const key = readPropertyKey(propAssign);
|
|
636
|
+
if (key === "name" || key === "handler") continue;
|
|
637
|
+
const init = propAssign.getInitializer();
|
|
638
|
+
if (!init) continue;
|
|
639
|
+
const value = readDataLiteralNode(init);
|
|
640
|
+
if (value === undefined) {
|
|
641
|
+
return fail(
|
|
642
|
+
"job",
|
|
643
|
+
sourceLocationFromNode(call, sourceFile),
|
|
644
|
+
`option "${key}" could not be read as a plain value`,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
optionsBag[key] = value;
|
|
648
|
+
}
|
|
649
|
+
return ok({
|
|
650
|
+
kind: "job",
|
|
651
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
652
|
+
jobName: nameInit.getLiteralValue(),
|
|
653
|
+
options: optionsBag as Omit<JobDefinition, "name" | "handler">,
|
|
654
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
659
|
+
if (!nameArg) {
|
|
660
|
+
return fail(
|
|
661
|
+
"job",
|
|
662
|
+
sourceLocationFromNode(call, sourceFile),
|
|
663
|
+
"first argument must be a string literal job name (or use the object form)",
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
const optionsArg = args[1];
|
|
667
|
+
if (!optionsArg) {
|
|
668
|
+
return fail(
|
|
669
|
+
"job",
|
|
670
|
+
sourceLocationFromNode(call, sourceFile),
|
|
671
|
+
"expected an options object as second argument",
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
const options = readDataLiteralNode(optionsArg);
|
|
675
|
+
if (!isPlainObject(options)) {
|
|
676
|
+
return fail(
|
|
677
|
+
"job",
|
|
678
|
+
sourceLocationFromNode(call, sourceFile),
|
|
679
|
+
"options could not be read as a plain object",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const handlerArg = args[2];
|
|
683
|
+
if (!handlerArg) {
|
|
684
|
+
return fail(
|
|
685
|
+
"job",
|
|
686
|
+
sourceLocationFromNode(call, sourceFile),
|
|
687
|
+
"expected a handler function as third argument",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const fn = findFunctionLiteral(handlerArg);
|
|
691
|
+
if (!fn) {
|
|
692
|
+
return fail(
|
|
693
|
+
"job",
|
|
694
|
+
sourceLocationFromNode(call, sourceFile),
|
|
695
|
+
"third argument must be an inline arrow function or function expression",
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
return ok({
|
|
699
|
+
kind: "job",
|
|
700
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
701
|
+
jobName: nameArg.getLiteralValue(),
|
|
702
|
+
options: options as Omit<JobDefinition, "name" | "handler">,
|
|
703
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function extractHttpRoute(
|
|
708
|
+
call: CallExpression,
|
|
709
|
+
sourceFile: SourceFile,
|
|
710
|
+
): ExtractOutput<HttpRoutePattern> {
|
|
711
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
712
|
+
if (!arg) {
|
|
713
|
+
return fail(
|
|
714
|
+
"httpRoute",
|
|
715
|
+
sourceLocationFromNode(call, sourceFile),
|
|
716
|
+
"argument must be an inline HttpRouteDefinition object",
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
const methodLiteral = arg
|
|
720
|
+
.getProperty("method")
|
|
721
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
722
|
+
?.getInitializer()
|
|
723
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
724
|
+
if (!methodLiteral) {
|
|
725
|
+
return fail(
|
|
726
|
+
"httpRoute",
|
|
727
|
+
sourceLocationFromNode(call, sourceFile),
|
|
728
|
+
"method must be a string literal",
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const methodValue = methodLiteral.getLiteralValue();
|
|
732
|
+
if (!isHttpRouteMethod(methodValue)) {
|
|
733
|
+
return fail(
|
|
734
|
+
"httpRoute",
|
|
735
|
+
sourceLocationFromNode(call, sourceFile),
|
|
736
|
+
`method must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS (got "${methodValue}")`,
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const pathLiteral = arg
|
|
740
|
+
.getProperty("path")
|
|
741
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
742
|
+
?.getInitializer()
|
|
743
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
744
|
+
if (!pathLiteral) {
|
|
745
|
+
return fail(
|
|
746
|
+
"httpRoute",
|
|
747
|
+
sourceLocationFromNode(call, sourceFile),
|
|
748
|
+
"path must be a string literal",
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
const handlerInit = arg
|
|
752
|
+
.getProperty("handler")
|
|
753
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
754
|
+
?.getInitializer();
|
|
755
|
+
if (!handlerInit) {
|
|
756
|
+
return fail(
|
|
757
|
+
"httpRoute",
|
|
758
|
+
sourceLocationFromNode(call, sourceFile),
|
|
759
|
+
"missing `handler` property",
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const fn = findFunctionLiteral(handlerInit);
|
|
763
|
+
if (!fn) {
|
|
764
|
+
return fail(
|
|
765
|
+
"httpRoute",
|
|
766
|
+
sourceLocationFromNode(call, sourceFile),
|
|
767
|
+
"handler must be an inline arrow function or function expression",
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const anonymous = readBooleanProperty(arg, "anonymous");
|
|
771
|
+
return ok({
|
|
772
|
+
kind: "httpRoute",
|
|
773
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
774
|
+
method: methodValue,
|
|
775
|
+
path: pathLiteral.getLiteralValue(),
|
|
776
|
+
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
777
|
+
...(anonymous === true && { anonymous: true }),
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export function extractDefineEvent(
|
|
782
|
+
call: CallExpression,
|
|
783
|
+
sourceFile: SourceFile,
|
|
784
|
+
): ExtractOutput<DefineEventPattern> {
|
|
785
|
+
const args = call.getArguments();
|
|
786
|
+
const first = args[0];
|
|
787
|
+
if (!first) {
|
|
788
|
+
return fail(
|
|
789
|
+
"defineEvent",
|
|
790
|
+
sourceLocationFromNode(call, sourceFile),
|
|
791
|
+
"expected at least one argument",
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
796
|
+
if (obj && args.length === 1) {
|
|
797
|
+
const nameInit = obj
|
|
798
|
+
.getProperty("name")
|
|
799
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
800
|
+
?.getInitializer()
|
|
801
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
802
|
+
if (!nameInit) {
|
|
803
|
+
return fail(
|
|
804
|
+
"defineEvent",
|
|
805
|
+
sourceLocationFromNode(call, sourceFile),
|
|
806
|
+
"object form requires a string-literal `name` property",
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
const schemaInit = obj
|
|
810
|
+
.getProperty("schema")
|
|
811
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
812
|
+
?.getInitializer();
|
|
813
|
+
if (!schemaInit) {
|
|
814
|
+
return fail(
|
|
815
|
+
"defineEvent",
|
|
816
|
+
sourceLocationFromNode(call, sourceFile),
|
|
817
|
+
"object form requires a `schema` property",
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
let version: number | undefined;
|
|
821
|
+
const versionInit = obj
|
|
822
|
+
.getProperty("version")
|
|
823
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
824
|
+
?.getInitializer();
|
|
825
|
+
if (versionInit) {
|
|
826
|
+
const v = readDataLiteralNode(versionInit);
|
|
827
|
+
if (typeof v === "number") version = v;
|
|
828
|
+
}
|
|
829
|
+
return ok({
|
|
830
|
+
kind: "defineEvent",
|
|
831
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
832
|
+
eventName: nameInit.getLiteralValue(),
|
|
833
|
+
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
834
|
+
...(version !== undefined && { version }),
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
839
|
+
if (!nameArg) {
|
|
840
|
+
return fail(
|
|
841
|
+
"defineEvent",
|
|
842
|
+
sourceLocationFromNode(call, sourceFile),
|
|
843
|
+
"first argument must be a string literal event name (or use the object form)",
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const schemaArg = args[1];
|
|
847
|
+
if (!schemaArg) {
|
|
848
|
+
return fail(
|
|
849
|
+
"defineEvent",
|
|
850
|
+
sourceLocationFromNode(call, sourceFile),
|
|
851
|
+
"expected a Zod schema as second argument",
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
let version: number | undefined;
|
|
855
|
+
const optionsArg = args[2];
|
|
856
|
+
if (optionsArg) {
|
|
857
|
+
const options = readDataLiteralNode(optionsArg);
|
|
858
|
+
if (isPlainObject(options) && typeof options["version"] === "number") {
|
|
859
|
+
version = options["version"];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return ok({
|
|
863
|
+
kind: "defineEvent",
|
|
864
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
865
|
+
eventName: nameArg.getLiteralValue(),
|
|
866
|
+
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
867
|
+
...(version !== undefined && { version }),
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function extractEventMigration(
|
|
872
|
+
call: CallExpression,
|
|
873
|
+
sourceFile: SourceFile,
|
|
874
|
+
): ExtractOutput<EventMigrationPattern> {
|
|
875
|
+
const args = call.getArguments();
|
|
876
|
+
const first = args[0];
|
|
877
|
+
if (!first) {
|
|
878
|
+
return fail(
|
|
879
|
+
"eventMigration",
|
|
880
|
+
sourceLocationFromNode(call, sourceFile),
|
|
881
|
+
"expected at least one argument",
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
886
|
+
if (obj && args.length === 1) {
|
|
887
|
+
const eventInit = obj
|
|
888
|
+
.getProperty("event")
|
|
889
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
890
|
+
?.getInitializer()
|
|
891
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
892
|
+
if (!eventInit) {
|
|
893
|
+
return fail(
|
|
894
|
+
"eventMigration",
|
|
895
|
+
sourceLocationFromNode(call, sourceFile),
|
|
896
|
+
"object form requires a string-literal `event` property",
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
const fromInit = obj
|
|
900
|
+
.getProperty("fromVersion")
|
|
901
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
902
|
+
?.getInitializer();
|
|
903
|
+
const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
|
|
904
|
+
if (typeof fromVersion !== "number") {
|
|
905
|
+
return fail(
|
|
906
|
+
"eventMigration",
|
|
907
|
+
sourceLocationFromNode(call, sourceFile),
|
|
908
|
+
"fromVersion must be a numeric literal",
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
const toInit = obj
|
|
912
|
+
.getProperty("toVersion")
|
|
913
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
914
|
+
?.getInitializer();
|
|
915
|
+
const toVersion = toInit ? readDataLiteralNode(toInit) : undefined;
|
|
916
|
+
if (typeof toVersion !== "number") {
|
|
917
|
+
return fail(
|
|
918
|
+
"eventMigration",
|
|
919
|
+
sourceLocationFromNode(call, sourceFile),
|
|
920
|
+
"toVersion must be a numeric literal",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const transformInit = obj
|
|
924
|
+
.getProperty("transform")
|
|
925
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
926
|
+
?.getInitializer();
|
|
927
|
+
if (!transformInit) {
|
|
928
|
+
return fail(
|
|
929
|
+
"eventMigration",
|
|
930
|
+
sourceLocationFromNode(call, sourceFile),
|
|
931
|
+
"object form requires a `transform` property",
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
const fn = findFunctionLiteral(transformInit);
|
|
935
|
+
if (!fn) {
|
|
936
|
+
return fail(
|
|
937
|
+
"eventMigration",
|
|
938
|
+
sourceLocationFromNode(call, sourceFile),
|
|
939
|
+
"transform must be an inline arrow function or function expression",
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return ok({
|
|
943
|
+
kind: "eventMigration",
|
|
944
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
945
|
+
eventName: eventInit.getLiteralValue(),
|
|
946
|
+
fromVersion,
|
|
947
|
+
toVersion,
|
|
948
|
+
transformBody: sourceLocationFromNode(fn, sourceFile),
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
953
|
+
if (!nameArg) {
|
|
954
|
+
return fail(
|
|
955
|
+
"eventMigration",
|
|
956
|
+
sourceLocationFromNode(call, sourceFile),
|
|
957
|
+
"first argument must be a string literal event name (or use the object form)",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const fromArg = args[1];
|
|
961
|
+
const fromVersion = fromArg ? readDataLiteralNode(fromArg) : undefined;
|
|
962
|
+
if (typeof fromVersion !== "number") {
|
|
963
|
+
return fail(
|
|
964
|
+
"eventMigration",
|
|
965
|
+
sourceLocationFromNode(call, sourceFile),
|
|
966
|
+
"fromVersion must be a numeric literal",
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
const toArg = args[2];
|
|
970
|
+
const toVersion = toArg ? readDataLiteralNode(toArg) : undefined;
|
|
971
|
+
if (typeof toVersion !== "number") {
|
|
972
|
+
return fail(
|
|
973
|
+
"eventMigration",
|
|
974
|
+
sourceLocationFromNode(call, sourceFile),
|
|
975
|
+
"toVersion must be a numeric literal",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
const transformArg = args[3];
|
|
979
|
+
if (!transformArg) {
|
|
980
|
+
return fail(
|
|
981
|
+
"eventMigration",
|
|
982
|
+
sourceLocationFromNode(call, sourceFile),
|
|
983
|
+
"expected a transform function as fourth argument",
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
const fn = findFunctionLiteral(transformArg);
|
|
987
|
+
if (!fn) {
|
|
988
|
+
return fail(
|
|
989
|
+
"eventMigration",
|
|
990
|
+
sourceLocationFromNode(call, sourceFile),
|
|
991
|
+
"transform must be an inline arrow function or function expression",
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
return ok({
|
|
995
|
+
kind: "eventMigration",
|
|
996
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
997
|
+
eventName: nameArg.getLiteralValue(),
|
|
998
|
+
fromVersion,
|
|
999
|
+
toVersion,
|
|
1000
|
+
transformBody: sourceLocationFromNode(fn, sourceFile),
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
export function extractNotification(
|
|
1005
|
+
call: CallExpression,
|
|
1006
|
+
sourceFile: SourceFile,
|
|
1007
|
+
): ExtractOutput<NotificationPattern> {
|
|
1008
|
+
const args = call.getArguments();
|
|
1009
|
+
const first = args[0];
|
|
1010
|
+
if (!first) {
|
|
1011
|
+
return fail(
|
|
1012
|
+
"notification",
|
|
1013
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1014
|
+
"expected at least one argument",
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
let nameLiteral: ReturnType<typeof first.asKind<SyntaxKind.StringLiteral>>;
|
|
1019
|
+
let defObj: ReturnType<typeof first.asKind<SyntaxKind.ObjectLiteralExpression>>;
|
|
1020
|
+
|
|
1021
|
+
const firstObj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1022
|
+
if (firstObj && args.length === 1) {
|
|
1023
|
+
nameLiteral = firstObj
|
|
1024
|
+
.getProperty("name")
|
|
1025
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1026
|
+
?.getInitializer()
|
|
1027
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
1028
|
+
if (!nameLiteral) {
|
|
1029
|
+
return fail(
|
|
1030
|
+
"notification",
|
|
1031
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1032
|
+
"object form requires a string-literal `name` property",
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
defObj = firstObj;
|
|
1036
|
+
} else {
|
|
1037
|
+
nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
1038
|
+
if (!nameLiteral) {
|
|
1039
|
+
return fail(
|
|
1040
|
+
"notification",
|
|
1041
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1042
|
+
"first argument must be a string literal notification name (or use the object form)",
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
defObj = args[1]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1046
|
+
if (!defObj) {
|
|
1047
|
+
return fail(
|
|
1048
|
+
"notification",
|
|
1049
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1050
|
+
"second argument must be an inline definition object",
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const nameArg = nameLiteral;
|
|
1055
|
+
const triggerObj = defObj
|
|
1056
|
+
.getProperty("trigger")
|
|
1057
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1058
|
+
?.getInitializer()
|
|
1059
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1060
|
+
if (!triggerObj) {
|
|
1061
|
+
return fail(
|
|
1062
|
+
"notification",
|
|
1063
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1064
|
+
"missing or non-object `trigger` property",
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
const onInit = triggerObj
|
|
1068
|
+
.getProperty("on")
|
|
1069
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1070
|
+
?.getInitializer();
|
|
1071
|
+
const onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
1072
|
+
if (!onName) {
|
|
1073
|
+
return fail(
|
|
1074
|
+
"notification",
|
|
1075
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1076
|
+
"trigger.on must be a string literal or inline { name } object",
|
|
1077
|
+
);
|
|
1078
|
+
}
|
|
1079
|
+
const recipientInit = defObj
|
|
1080
|
+
.getProperty("recipient")
|
|
1081
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1082
|
+
?.getInitializer();
|
|
1083
|
+
const recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
|
|
1084
|
+
if (!recipientFn) {
|
|
1085
|
+
return fail(
|
|
1086
|
+
"notification",
|
|
1087
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1088
|
+
"recipient must be an inline arrow function or function expression",
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
const dataInit = defObj
|
|
1092
|
+
.getProperty("data")
|
|
1093
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1094
|
+
?.getInitializer();
|
|
1095
|
+
const dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
|
|
1096
|
+
if (!dataFn) {
|
|
1097
|
+
return fail(
|
|
1098
|
+
"notification",
|
|
1099
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1100
|
+
"data must be an inline arrow function or function expression",
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
let templates: Record<string, SourceLocation> | undefined;
|
|
1104
|
+
const templatesObj = defObj
|
|
1105
|
+
.getProperty("templates")
|
|
1106
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1107
|
+
?.getInitializer()
|
|
1108
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1109
|
+
if (templatesObj) {
|
|
1110
|
+
templates = {};
|
|
1111
|
+
for (const prop of templatesObj.getProperties()) {
|
|
1112
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
1113
|
+
if (!propAssign) continue;
|
|
1114
|
+
const init = propAssign.getInitializer();
|
|
1115
|
+
if (!init) continue;
|
|
1116
|
+
const tfn = findFunctionLiteral(init);
|
|
1117
|
+
if (!tfn) continue;
|
|
1118
|
+
templates[readPropertyKey(propAssign)] = sourceLocationFromNode(tfn, sourceFile);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return ok({
|
|
1122
|
+
kind: "notification",
|
|
1123
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
1124
|
+
notificationName: nameArg.getLiteralValue(),
|
|
1125
|
+
trigger: { on: onName },
|
|
1126
|
+
recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
|
|
1127
|
+
dataBody: sourceLocationFromNode(dataFn, sourceFile),
|
|
1128
|
+
...(templates !== undefined && { templates }),
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export function readApplyBodies(
|
|
1133
|
+
defObj: ReturnType<Node["asKind"]>,
|
|
1134
|
+
sourceFile: SourceFile,
|
|
1135
|
+
): Record<string, SourceLocation> | undefined {
|
|
1136
|
+
if (!defObj) return undefined;
|
|
1137
|
+
const obj = defObj.asKind?.(SyntaxKind.ObjectLiteralExpression);
|
|
1138
|
+
if (!obj) return undefined;
|
|
1139
|
+
const applyObj = obj
|
|
1140
|
+
.getProperty("apply")
|
|
1141
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1142
|
+
?.getInitializer()
|
|
1143
|
+
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1144
|
+
if (!applyObj) return undefined;
|
|
1145
|
+
const out: Record<string, SourceLocation> = {};
|
|
1146
|
+
for (const prop of applyObj.getProperties()) {
|
|
1147
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
1148
|
+
if (!propAssign) return undefined;
|
|
1149
|
+
const init = propAssign.getInitializer();
|
|
1150
|
+
if (!init) return undefined;
|
|
1151
|
+
const fn = findFunctionLiteral(init);
|
|
1152
|
+
if (!fn) return undefined;
|
|
1153
|
+
out[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
|
|
1154
|
+
}
|
|
1155
|
+
return out;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
export function extractProjection(
|
|
1159
|
+
call: CallExpression,
|
|
1160
|
+
sourceFile: SourceFile,
|
|
1161
|
+
): ExtractOutput<ProjectionPattern> {
|
|
1162
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1163
|
+
if (!arg) {
|
|
1164
|
+
return fail(
|
|
1165
|
+
"projection",
|
|
1166
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1167
|
+
"argument must be an inline ProjectionDefinition object",
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
const nameLit = arg
|
|
1171
|
+
.getProperty("name")
|
|
1172
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1173
|
+
?.getInitializer()
|
|
1174
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
1175
|
+
if (!nameLit) {
|
|
1176
|
+
return fail(
|
|
1177
|
+
"projection",
|
|
1178
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1179
|
+
"name must be a string literal",
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
const sourceInit = arg
|
|
1183
|
+
.getProperty("source")
|
|
1184
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1185
|
+
?.getInitializer();
|
|
1186
|
+
if (!sourceInit) {
|
|
1187
|
+
return fail(
|
|
1188
|
+
"projection",
|
|
1189
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1190
|
+
"missing `source` property",
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
const sourceEntity = readNameOrRefOrList(sourceInit);
|
|
1194
|
+
if (!sourceEntity) {
|
|
1195
|
+
return fail(
|
|
1196
|
+
"projection",
|
|
1197
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1198
|
+
"source must be a string literal or array of string literals",
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
const applyBodies = readApplyBodies(arg, sourceFile);
|
|
1202
|
+
if (!applyBodies) {
|
|
1203
|
+
return fail(
|
|
1204
|
+
"projection",
|
|
1205
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1206
|
+
"apply must be an inline object map of event-type → function",
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
return ok({
|
|
1210
|
+
kind: "projection",
|
|
1211
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
1212
|
+
name: nameLit.getLiteralValue(),
|
|
1213
|
+
sourceEntity,
|
|
1214
|
+
applyBodies,
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export function extractMultiStreamProjection(
|
|
1219
|
+
call: CallExpression,
|
|
1220
|
+
sourceFile: SourceFile,
|
|
1221
|
+
): ExtractOutput<MultiStreamProjectionPattern> {
|
|
1222
|
+
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1223
|
+
if (!arg) {
|
|
1224
|
+
return fail(
|
|
1225
|
+
"multiStreamProjection",
|
|
1226
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1227
|
+
"argument must be an inline MultiStreamProjectionDefinition object",
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const nameLit = arg
|
|
1231
|
+
.getProperty("name")
|
|
1232
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1233
|
+
?.getInitializer()
|
|
1234
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
1235
|
+
if (!nameLit) {
|
|
1236
|
+
return fail(
|
|
1237
|
+
"multiStreamProjection",
|
|
1238
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1239
|
+
"name must be a string literal",
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const applyBodies = readApplyBodies(arg, sourceFile);
|
|
1243
|
+
if (!applyBodies) {
|
|
1244
|
+
return fail(
|
|
1245
|
+
"multiStreamProjection",
|
|
1246
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1247
|
+
"apply must be an inline object map of event-type → function",
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
const errorModeInit = arg
|
|
1251
|
+
.getProperty("errorMode")
|
|
1252
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1253
|
+
?.getInitializer();
|
|
1254
|
+
const errorMode = errorModeInit ? readDataLiteralNode(errorModeInit) : undefined;
|
|
1255
|
+
const runInLit = arg
|
|
1256
|
+
.getProperty("runIn")
|
|
1257
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1258
|
+
?.getInitializer()
|
|
1259
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
1260
|
+
const runIn = runInLit ? (runInLit.getLiteralValue() as RunIn) : undefined;
|
|
1261
|
+
const deliveryLit = arg
|
|
1262
|
+
.getProperty("delivery")
|
|
1263
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1264
|
+
?.getInitializer()
|
|
1265
|
+
?.asKind(SyntaxKind.StringLiteral);
|
|
1266
|
+
const delivery = deliveryLit
|
|
1267
|
+
? (deliveryLit.getLiteralValue() as "shared" | "per-instance")
|
|
1268
|
+
: undefined;
|
|
1269
|
+
return ok({
|
|
1270
|
+
kind: "multiStreamProjection",
|
|
1271
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
1272
|
+
name: nameLit.getLiteralValue(),
|
|
1273
|
+
applyBodies,
|
|
1274
|
+
...(isPlainObject(errorMode) && { errorMode: errorMode as MspErrorMode }),
|
|
1275
|
+
...(runIn !== undefined && { runIn }),
|
|
1276
|
+
...(delivery !== undefined && { delivery }),
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
export function collectScreenOpaqueProps(
|
|
1281
|
+
node: Node,
|
|
1282
|
+
path: string,
|
|
1283
|
+
sourceFile: SourceFile,
|
|
1284
|
+
out: Record<string, SourceLocation>,
|
|
1285
|
+
): void {
|
|
1286
|
+
const fn = findFunctionLiteral(node);
|
|
1287
|
+
if (fn) {
|
|
1288
|
+
out[path] = sourceLocationFromNode(fn, sourceFile);
|
|
1289
|
+
} else if (node.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
1290
|
+
for (const prop of node.getProperties()) {
|
|
1291
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
1292
|
+
if (!propAssign) continue;
|
|
1293
|
+
const init = propAssign.getInitializer();
|
|
1294
|
+
if (!init) continue;
|
|
1295
|
+
const key = readPropertyKey(propAssign);
|
|
1296
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
1297
|
+
collectScreenOpaqueProps(init, childPath, sourceFile, out);
|
|
1298
|
+
}
|
|
1299
|
+
} else if (node.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
1300
|
+
node.getElements().forEach((el, idx) => {
|
|
1301
|
+
collectScreenOpaqueProps(el, `${path}.${idx}`, sourceFile, out);
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
export function readScreenStatic(node: Node): unknown {
|
|
1307
|
+
if (findFunctionLiteral(node)) return SCREEN_OPAQUE_MARKER;
|
|
1308
|
+
const obj = node.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1309
|
+
if (obj) {
|
|
1310
|
+
const out: Record<string, unknown> = {};
|
|
1311
|
+
for (const prop of obj.getProperties()) {
|
|
1312
|
+
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
1313
|
+
if (!propAssign) continue;
|
|
1314
|
+
const init = propAssign.getInitializer();
|
|
1315
|
+
if (!init) continue;
|
|
1316
|
+
out[readPropertyKey(propAssign)] = readScreenStatic(init);
|
|
1317
|
+
}
|
|
1318
|
+
return out;
|
|
1319
|
+
}
|
|
1320
|
+
const arr = node.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
1321
|
+
if (arr) {
|
|
1322
|
+
return arr.getElements().map(readScreenStatic);
|
|
1323
|
+
}
|
|
1324
|
+
const value = readDataLiteralNode(node);
|
|
1325
|
+
if (value === undefined) return SCREEN_OPAQUE_MARKER;
|
|
1326
|
+
return value;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
export function extractScreen(
|
|
1330
|
+
call: CallExpression,
|
|
1331
|
+
sourceFile: SourceFile,
|
|
1332
|
+
): ExtractOutput<ScreenPattern> {
|
|
1333
|
+
const arg = call.getArguments()[0];
|
|
1334
|
+
if (!arg) {
|
|
1335
|
+
return fail(
|
|
1336
|
+
"screen",
|
|
1337
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1338
|
+
"expected a ScreenDefinition object as first argument",
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
const obj = arg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1342
|
+
if (!obj) {
|
|
1343
|
+
return fail(
|
|
1344
|
+
"screen",
|
|
1345
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1346
|
+
"argument must be an inline object literal",
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
const opaqueProps: Record<string, SourceLocation> = {};
|
|
1350
|
+
collectScreenOpaqueProps(obj, "", sourceFile, opaqueProps);
|
|
1351
|
+
const definition = readScreenStatic(obj);
|
|
1352
|
+
if (!isPlainObject(definition)) {
|
|
1353
|
+
return fail(
|
|
1354
|
+
"screen",
|
|
1355
|
+
sourceLocationFromNode(call, sourceFile),
|
|
1356
|
+
"definition could not be read structurally",
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return ok({
|
|
1360
|
+
kind: "screen",
|
|
1361
|
+
source: sourceLocationFromNode(call, sourceFile),
|
|
1362
|
+
definition: definition as ScreenDefinition,
|
|
1363
|
+
opaqueProps: opaqueProps as OpaquePropMap,
|
|
1364
|
+
});
|
|
1365
|
+
}
|