@cosmicdrift/kumiko-framework 0.2.3 → 0.4.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 +93 -0
- package/package.json +124 -39
- 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/compliance/profiles.ts +8 -8
- 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 +3 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -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 -1804
- 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 +88 -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/factories.ts +12 -12
- 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 +7 -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 +49 -1
- package/src/engine/feature-ast/render.ts +17 -1
- package/src/engine/index.ts +44 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +6 -0
- package/src/engine/pattern-library/library.ts +42 -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 +2 -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 +93 -1
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/index.ts +11 -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 +132 -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/file-routes.ts +1 -1
- package/src/files/types.ts +2 -2
- 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 -2602
|
@@ -1,2602 +0,0 @@
|
|
|
1
|
-
// Per-pattern extractors — read the arguments of an `r.<method>(...)`
|
|
2
|
-
// call and produce the matching FeaturePattern. Each extractor is a
|
|
3
|
-
// pure function on (CallExpression, SourceFile) and either returns a
|
|
4
|
-
// pattern or a ParseError describing why the arguments could not be
|
|
5
|
-
// read statically.
|
|
6
|
-
//
|
|
7
|
-
// **Implementation order (C1.5):**
|
|
8
|
-
// - Round 1 (this file's first slice): the simplest static patterns —
|
|
9
|
-
// requires, optionalRequires, readsConfig, systemScope, toggleable.
|
|
10
|
-
// - Round 2: object-literal-based statics — entity, relation, nav,
|
|
11
|
-
// workspace.
|
|
12
|
-
// - Round 3: complex statics — config, translations, metric, secret,
|
|
13
|
-
// claimKey, referenceData, useExtension.
|
|
14
|
-
// - Round 4: mixed (header + body) — screen, writeHandler,
|
|
15
|
-
// queryHandler, hook, entityHook, job, notification, httpRoute,
|
|
16
|
-
// defineEvent, eventMigration, projection, multiStreamProjection.
|
|
17
|
-
// - Round 5: opaque — authClaims, extendsRegistrar.
|
|
18
|
-
//
|
|
19
|
-
// Until a pattern's extractor lands, the dispatcher in parse.ts falls
|
|
20
|
-
// back to UnknownPattern with the right method name. That's why the
|
|
21
|
-
// dispatcher's switch lists all method names — the catch-all default
|
|
22
|
-
// is reserved for r.* calls we have no pattern type for at all.
|
|
23
|
-
|
|
24
|
-
import type { CallExpression, Node, SourceFile } from "ts-morph";
|
|
25
|
-
import { SyntaxKind } from "ts-morph";
|
|
26
|
-
import type { LifecycleHookType } from "../constants";
|
|
27
|
-
import type {
|
|
28
|
-
ConfigKeyDefinition,
|
|
29
|
-
ConfigKeyType,
|
|
30
|
-
JobDefinition,
|
|
31
|
-
RunIn,
|
|
32
|
-
TranslationKeys,
|
|
33
|
-
} from "../types/config";
|
|
34
|
-
import type { MetricOptions, SecretOptions } from "../types/feature";
|
|
35
|
-
import type { EntityDefinition } from "../types/fields";
|
|
36
|
-
import type { AccessRule, ClaimKeyType, RateLimitOption } from "../types/handlers";
|
|
37
|
-
import type { HookPhase } from "../types/hooks";
|
|
38
|
-
import type { HttpRouteMethod } from "../types/http-route";
|
|
39
|
-
import type { NavDefinition } from "../types/nav";
|
|
40
|
-
import type { MspErrorMode } from "../types/projection";
|
|
41
|
-
import type { RelationDefinition } from "../types/relations";
|
|
42
|
-
import type { ScreenDefinition } from "../types/screen";
|
|
43
|
-
import type { WorkspaceDefinition } from "../types/workspace";
|
|
44
|
-
import type { ParseError } from "./parse";
|
|
45
|
-
import type {
|
|
46
|
-
AuthClaimsPattern,
|
|
47
|
-
ClaimKeyPattern,
|
|
48
|
-
ConfigPattern,
|
|
49
|
-
DefineEventPattern,
|
|
50
|
-
EntityHookPattern,
|
|
51
|
-
EntityPattern,
|
|
52
|
-
EventMigrationPattern,
|
|
53
|
-
ExposesApiPattern,
|
|
54
|
-
ExtendsRegistrarPattern,
|
|
55
|
-
HookPattern,
|
|
56
|
-
HttpRoutePattern,
|
|
57
|
-
JobPattern,
|
|
58
|
-
MetricPattern,
|
|
59
|
-
MultiStreamProjectionPattern,
|
|
60
|
-
NavPattern,
|
|
61
|
-
NotificationPattern,
|
|
62
|
-
OpaquePropMap,
|
|
63
|
-
OptionalRequiresPattern,
|
|
64
|
-
ProjectionPattern,
|
|
65
|
-
QueryHandlerPattern,
|
|
66
|
-
ReadsConfigPattern,
|
|
67
|
-
ReferenceDataPattern,
|
|
68
|
-
RelationPattern,
|
|
69
|
-
RequiresPattern,
|
|
70
|
-
ScreenPattern,
|
|
71
|
-
SecretPattern,
|
|
72
|
-
SystemScopePattern,
|
|
73
|
-
ToggleablePattern,
|
|
74
|
-
TranslationsPattern,
|
|
75
|
-
UseExtensionPattern,
|
|
76
|
-
UsesApiPattern,
|
|
77
|
-
WorkspacePattern,
|
|
78
|
-
WriteHandlerPattern,
|
|
79
|
-
} from "./patterns";
|
|
80
|
-
import { SCREEN_OPAQUE_MARKER } from "./patterns";
|
|
81
|
-
import type { SourceLocation } from "./source-location";
|
|
82
|
-
import { sourceLocationFromNode } from "./source-location";
|
|
83
|
-
|
|
84
|
-
// =============================================================================
|
|
85
|
-
// Result helpers — every extractor returns ExtractOutput so the
|
|
86
|
-
// dispatcher can route patterns vs errors uniformly.
|
|
87
|
-
// =============================================================================
|
|
88
|
-
|
|
89
|
-
export type ExtractOutput<TPattern> =
|
|
90
|
-
| { readonly kind: "pattern"; readonly pattern: TPattern }
|
|
91
|
-
| { readonly kind: "error"; readonly error: ParseError };
|
|
92
|
-
|
|
93
|
-
function ok<TPattern>(pattern: TPattern): ExtractOutput<TPattern> {
|
|
94
|
-
return { kind: "pattern", pattern };
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Narrow return type lets fail() flow through both ExtractOutput<T> (where
|
|
98
|
-
// the error variant is always valid) and through helpers like
|
|
99
|
-
// readNamedOptions that expose the error-half directly to their callers.
|
|
100
|
-
function fail(
|
|
101
|
-
methodName: string,
|
|
102
|
-
source: ParseError["source"],
|
|
103
|
-
reason: string,
|
|
104
|
-
): { readonly kind: "error"; readonly error: ParseError } {
|
|
105
|
-
return { kind: "error", error: { methodName, source, reason } };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// =============================================================================
|
|
109
|
-
// Argument readers — small primitives reused across extractors.
|
|
110
|
-
// =============================================================================
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Read a list of arguments where every entry must be a string literal.
|
|
114
|
-
* Returns the list of literal values or undefined when any argument is
|
|
115
|
-
* not a literal (e.g. spread of a const, identifier).
|
|
116
|
-
*/
|
|
117
|
-
function readStringLiteralArgs(call: CallExpression): readonly string[] | undefined {
|
|
118
|
-
const out: string[] = [];
|
|
119
|
-
for (const arg of call.getArguments()) {
|
|
120
|
-
const literal = arg.asKind(SyntaxKind.StringLiteral);
|
|
121
|
-
if (!literal) return undefined;
|
|
122
|
-
out.push(literal.getLiteralValue());
|
|
123
|
-
}
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Read a property from an object-literal node by name and return the
|
|
129
|
-
* boolean literal it points at. Returns undefined when the property is
|
|
130
|
-
* missing or not a `true`/`false` literal.
|
|
131
|
-
*/
|
|
132
|
-
function readBooleanProperty(objectLiteral: Node, propertyName: string): boolean | undefined {
|
|
133
|
-
const obj = objectLiteral.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
134
|
-
if (!obj) return undefined;
|
|
135
|
-
const prop = obj.getProperty(propertyName);
|
|
136
|
-
if (!prop) return undefined;
|
|
137
|
-
const assignment = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
138
|
-
if (!assignment) return undefined;
|
|
139
|
-
const initializer = assignment.getInitializer();
|
|
140
|
-
if (!initializer) return undefined;
|
|
141
|
-
const kind = initializer.getKind();
|
|
142
|
-
if (kind === SyntaxKind.TrueKeyword) return true;
|
|
143
|
-
if (kind === SyntaxKind.FalseKeyword) return false;
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Best-effort reader that turns a TypeScript expression into a JSON-like
|
|
149
|
-
* value. Recurses through arrays, object literals, parenthesised
|
|
150
|
-
* expressions, and `as`/`satisfies` wrappers. Returns undefined as
|
|
151
|
-
* "could not read" — used as the failure signal because no legitimate
|
|
152
|
-
* JSON value is undefined (we forbid `{ x: undefined }` shapes by
|
|
153
|
-
* rejecting any unreadable property).
|
|
154
|
-
*
|
|
155
|
-
* Accepts: string / number (incl. negative literals) / boolean / null,
|
|
156
|
-
* array literals, object literals (with PropertyAssignment props only),
|
|
157
|
-
* `as const`, `as Type`, `satisfies Type`, parenthesised expressions.
|
|
158
|
-
*
|
|
159
|
-
* Rejects (returns undefined): identifiers, function calls, arrow
|
|
160
|
-
* functions, template literals with substitutions, spread props,
|
|
161
|
-
* shorthand props, methods, computed keys.
|
|
162
|
-
*/
|
|
163
|
-
function readDataLiteralNode(node: Node): unknown {
|
|
164
|
-
const kind = node.getKind();
|
|
165
|
-
switch (kind) {
|
|
166
|
-
case SyntaxKind.StringLiteral:
|
|
167
|
-
return node.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue();
|
|
168
|
-
case SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
169
|
-
return node.asKindOrThrow(SyntaxKind.NoSubstitutionTemplateLiteral).getLiteralValue();
|
|
170
|
-
case SyntaxKind.NumericLiteral:
|
|
171
|
-
return Number(node.asKindOrThrow(SyntaxKind.NumericLiteral).getText());
|
|
172
|
-
case SyntaxKind.TrueKeyword:
|
|
173
|
-
return true;
|
|
174
|
-
case SyntaxKind.FalseKeyword:
|
|
175
|
-
return false;
|
|
176
|
-
case SyntaxKind.NullKeyword:
|
|
177
|
-
return null;
|
|
178
|
-
case SyntaxKind.PrefixUnaryExpression: {
|
|
179
|
-
// Negative number literals: -1, -2.5
|
|
180
|
-
const expr = node.asKindOrThrow(SyntaxKind.PrefixUnaryExpression);
|
|
181
|
-
if (expr.getOperatorToken() !== SyntaxKind.MinusToken) return undefined;
|
|
182
|
-
const inner = readDataLiteralNode(expr.getOperand());
|
|
183
|
-
if (typeof inner !== "number") return undefined;
|
|
184
|
-
return -inner;
|
|
185
|
-
}
|
|
186
|
-
case SyntaxKind.ArrayLiteralExpression: {
|
|
187
|
-
const arr = node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
188
|
-
const out: unknown[] = [];
|
|
189
|
-
for (const el of arr.getElements()) {
|
|
190
|
-
const value = readDataLiteralNode(el);
|
|
191
|
-
if (value === undefined) return undefined;
|
|
192
|
-
out.push(value);
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
|
-
case SyntaxKind.ObjectLiteralExpression: {
|
|
197
|
-
const obj = node.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
198
|
-
const out: Record<string, unknown> = {};
|
|
199
|
-
for (const prop of obj.getProperties()) {
|
|
200
|
-
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
201
|
-
if (!propAssign) return undefined; // shorthand / spread / method
|
|
202
|
-
const initializer = propAssign.getInitializer();
|
|
203
|
-
if (!initializer) return undefined;
|
|
204
|
-
const value = readDataLiteralNode(initializer);
|
|
205
|
-
if (value === undefined) return undefined;
|
|
206
|
-
out[readPropertyKey(propAssign)] = value;
|
|
207
|
-
}
|
|
208
|
-
return out;
|
|
209
|
-
}
|
|
210
|
-
case SyntaxKind.AsExpression:
|
|
211
|
-
return readDataLiteralNode(node.asKindOrThrow(SyntaxKind.AsExpression).getExpression());
|
|
212
|
-
case SyntaxKind.SatisfiesExpression:
|
|
213
|
-
return readDataLiteralNode(
|
|
214
|
-
node.asKindOrThrow(SyntaxKind.SatisfiesExpression).getExpression(),
|
|
215
|
-
);
|
|
216
|
-
case SyntaxKind.ParenthesizedExpression:
|
|
217
|
-
return readDataLiteralNode(
|
|
218
|
-
node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression(),
|
|
219
|
-
);
|
|
220
|
-
default:
|
|
221
|
-
return undefined;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
226
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Read a PropertyAssignment's key as the unquoted string. ts-morph's
|
|
231
|
-
* getName() returns the source text including quote chars for keys like
|
|
232
|
-
* `"task.created"`; we strip them so consumers see the same literal
|
|
233
|
-
* value whether the author used identifier or string-key form.
|
|
234
|
-
*/
|
|
235
|
-
function readPropertyKey(propAssign: import("ts-morph").PropertyAssignment): string {
|
|
236
|
-
const nameNode = propAssign.getNameNode();
|
|
237
|
-
const literal = nameNode.asKind(SyntaxKind.StringLiteral);
|
|
238
|
-
if (literal) return literal.getLiteralValue();
|
|
239
|
-
return propAssign.getName();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Read a NameOrRef argument: either a string literal or an inline
|
|
244
|
-
* object literal `{ name: "..." }`. Identifier references (e.g. a
|
|
245
|
-
* captured const) cannot be resolved statically and return undefined.
|
|
246
|
-
*/
|
|
247
|
-
function readNameOrRef(node: Node): string | undefined {
|
|
248
|
-
const literal = node.asKind(SyntaxKind.StringLiteral);
|
|
249
|
-
if (literal) return literal.getLiteralValue();
|
|
250
|
-
const obj = readDataLiteralNode(node);
|
|
251
|
-
if (isPlainObject(obj) && typeof obj["name"] === "string") return obj["name"];
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Match a node that looks like a function literal — arrow function,
|
|
257
|
-
* function expression, or one of those wrapped in parentheses. Returns
|
|
258
|
-
* undefined for identifiers / call expressions / other shapes (a hook
|
|
259
|
-
* registered by passing a const reference, for example, won't be
|
|
260
|
-
* resolved statically).
|
|
261
|
-
*/
|
|
262
|
-
function findFunctionLiteral(node: Node): Node | undefined {
|
|
263
|
-
if (node.getKind() === SyntaxKind.ArrowFunction) return node;
|
|
264
|
-
if (node.getKind() === SyntaxKind.FunctionExpression) return node;
|
|
265
|
-
const paren = node.asKind(SyntaxKind.ParenthesizedExpression);
|
|
266
|
-
if (paren) return findFunctionLiteral(paren.getExpression());
|
|
267
|
-
return undefined;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Read a NameOrRef argument or an array of them. Returns either the
|
|
272
|
-
* single string or the list. undefined when neither shape matches.
|
|
273
|
-
*/
|
|
274
|
-
function readNameOrRefOrList(node: Node): string | readonly string[] | undefined {
|
|
275
|
-
const single = readNameOrRef(node);
|
|
276
|
-
if (single) return single;
|
|
277
|
-
const arr = node.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
278
|
-
if (!arr) return undefined;
|
|
279
|
-
const out: string[] = [];
|
|
280
|
-
for (const el of arr.getElements()) {
|
|
281
|
-
const name = readNameOrRef(el);
|
|
282
|
-
if (!name) return undefined;
|
|
283
|
-
out.push(name);
|
|
284
|
-
}
|
|
285
|
-
return out;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// =============================================================================
|
|
289
|
-
// Round 1 — simplest static patterns
|
|
290
|
-
// =============================================================================
|
|
291
|
-
|
|
292
|
-
// Reads either varargs string literals, or a single { features: string[] } /
|
|
293
|
-
// { keys: string[] } object — covers both the legacy positional form and
|
|
294
|
-
// the canonical Object-Form. `arrayPropName` controls which property name
|
|
295
|
-
// the object form uses (`features` for requires, `keys` for readsConfig).
|
|
296
|
-
function readVarargsOrArrayProp(
|
|
297
|
-
call: CallExpression,
|
|
298
|
-
arrayPropName: "features" | "keys",
|
|
299
|
-
): readonly string[] | undefined {
|
|
300
|
-
const args = call.getArguments();
|
|
301
|
-
// Object-Form: single object-literal arg with the named array property.
|
|
302
|
-
if (args.length === 1) {
|
|
303
|
-
const obj = args[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
304
|
-
if (obj) {
|
|
305
|
-
const propInit = obj
|
|
306
|
-
.getProperty(arrayPropName)
|
|
307
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
308
|
-
?.getInitializer();
|
|
309
|
-
if (propInit) {
|
|
310
|
-
const arr = propInit.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
311
|
-
if (!arr) return undefined;
|
|
312
|
-
const out: string[] = [];
|
|
313
|
-
for (const el of arr.getElements()) {
|
|
314
|
-
const lit = el.asKind(SyntaxKind.StringLiteral);
|
|
315
|
-
if (!lit) return undefined;
|
|
316
|
-
out.push(lit.getLiteralValue());
|
|
317
|
-
}
|
|
318
|
-
return out;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// Legacy positional form: every arg must be a string literal.
|
|
323
|
-
return readStringLiteralArgs(call);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
export function extractRequires(
|
|
327
|
-
call: CallExpression,
|
|
328
|
-
sourceFile: SourceFile,
|
|
329
|
-
): ExtractOutput<RequiresPattern> {
|
|
330
|
-
const names = readVarargsOrArrayProp(call, "features");
|
|
331
|
-
if (!names) {
|
|
332
|
-
return fail(
|
|
333
|
-
"requires",
|
|
334
|
-
sourceLocationFromNode(call, sourceFile),
|
|
335
|
-
"expected positional string literals or { features: string[] }",
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
return ok({
|
|
339
|
-
kind: "requires",
|
|
340
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
341
|
-
featureNames: names,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function extractOptionalRequires(
|
|
346
|
-
call: CallExpression,
|
|
347
|
-
sourceFile: SourceFile,
|
|
348
|
-
): ExtractOutput<OptionalRequiresPattern> {
|
|
349
|
-
const names = readVarargsOrArrayProp(call, "features");
|
|
350
|
-
if (!names) {
|
|
351
|
-
return fail(
|
|
352
|
-
"optionalRequires",
|
|
353
|
-
sourceLocationFromNode(call, sourceFile),
|
|
354
|
-
"expected positional string literals or { features: string[] }",
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
return ok({
|
|
358
|
-
kind: "optionalRequires",
|
|
359
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
360
|
-
featureNames: names,
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
export function extractReadsConfig(
|
|
365
|
-
call: CallExpression,
|
|
366
|
-
sourceFile: SourceFile,
|
|
367
|
-
): ExtractOutput<ReadsConfigPattern> {
|
|
368
|
-
const keys = readVarargsOrArrayProp(call, "keys");
|
|
369
|
-
if (!keys) {
|
|
370
|
-
return fail(
|
|
371
|
-
"readsConfig",
|
|
372
|
-
sourceLocationFromNode(call, sourceFile),
|
|
373
|
-
"expected positional string literals or { keys: string[] }",
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
return ok({
|
|
377
|
-
kind: "readsConfig",
|
|
378
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
379
|
-
qualifiedKeys: keys,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export function extractSystemScope(
|
|
384
|
-
call: CallExpression,
|
|
385
|
-
sourceFile: SourceFile,
|
|
386
|
-
): ExtractOutput<SystemScopePattern> {
|
|
387
|
-
// r.systemScope() takes no arguments. We don't fail when extras are
|
|
388
|
-
// present — the runtime ignores them, and the Designer doesn't lose
|
|
389
|
-
// anything by dropping them.
|
|
390
|
-
return ok({
|
|
391
|
-
kind: "systemScope",
|
|
392
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
export function extractToggleable(
|
|
397
|
-
call: CallExpression,
|
|
398
|
-
sourceFile: SourceFile,
|
|
399
|
-
): ExtractOutput<ToggleablePattern> {
|
|
400
|
-
const arg = call.getArguments()[0];
|
|
401
|
-
if (!arg) {
|
|
402
|
-
return fail(
|
|
403
|
-
"toggleable",
|
|
404
|
-
sourceLocationFromNode(call, sourceFile),
|
|
405
|
-
"expected an object argument with a `default` boolean",
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
const defaultValue = readBooleanProperty(arg, "default");
|
|
409
|
-
if (defaultValue === undefined) {
|
|
410
|
-
return fail(
|
|
411
|
-
"toggleable",
|
|
412
|
-
sourceLocationFromNode(call, sourceFile),
|
|
413
|
-
"argument must be `{ default: true | false }`",
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
return ok({
|
|
417
|
-
kind: "toggleable",
|
|
418
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
419
|
-
default: defaultValue,
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// =============================================================================
|
|
424
|
-
// Round 2 — object-literal-based static patterns
|
|
425
|
-
//
|
|
426
|
-
// These read a definition object via readDataLiteralNode. The reader is
|
|
427
|
-
// best-effort: function-typed properties (e.g. EntityDefinition with a
|
|
428
|
-
// computed `default`) make the extractor fail with a ParseError that
|
|
429
|
-
// the Designer/AI surface as "this entity has custom code, can't edit".
|
|
430
|
-
// Plain-data shapes round-trip cleanly.
|
|
431
|
-
// =============================================================================
|
|
432
|
-
|
|
433
|
-
export function extractEntity(
|
|
434
|
-
call: CallExpression,
|
|
435
|
-
sourceFile: SourceFile,
|
|
436
|
-
): ExtractOutput<EntityPattern> {
|
|
437
|
-
const args = call.getArguments();
|
|
438
|
-
const first = args[0];
|
|
439
|
-
if (!first) {
|
|
440
|
-
return fail(
|
|
441
|
-
"entity",
|
|
442
|
-
sourceLocationFromNode(call, sourceFile),
|
|
443
|
-
"expected at least one argument",
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Object-Form: r.entity({ name, fields, ...rest })
|
|
448
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
449
|
-
if (obj && args.length === 1) {
|
|
450
|
-
const nameInit = obj
|
|
451
|
-
.getProperty("name")
|
|
452
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
453
|
-
?.getInitializer()
|
|
454
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
455
|
-
if (!nameInit) {
|
|
456
|
-
return fail(
|
|
457
|
-
"entity",
|
|
458
|
-
sourceLocationFromNode(call, sourceFile),
|
|
459
|
-
"object form requires a string-literal `name` property",
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
const definition = readDataLiteralNode(obj);
|
|
463
|
-
if (!isPlainObject(definition)) {
|
|
464
|
-
return fail(
|
|
465
|
-
"entity",
|
|
466
|
-
sourceLocationFromNode(call, sourceFile),
|
|
467
|
-
"definition could not be read as a plain object (contains functions or identifiers)",
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
// Strip the `name` property — it lives on EntityPattern.entityName.
|
|
471
|
-
const { name: _name, ...defWithoutName } = definition;
|
|
472
|
-
return ok({
|
|
473
|
-
kind: "entity",
|
|
474
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
475
|
-
entityName: nameInit.getLiteralValue(),
|
|
476
|
-
definition: defWithoutName as EntityDefinition,
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Legacy positional form: r.entity("name", { fields, ... })
|
|
481
|
-
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
482
|
-
if (!nameArg) {
|
|
483
|
-
return fail(
|
|
484
|
-
"entity",
|
|
485
|
-
sourceLocationFromNode(call, sourceFile),
|
|
486
|
-
"first argument must be a string literal name (or use the object form)",
|
|
487
|
-
);
|
|
488
|
-
}
|
|
489
|
-
const defArg = args[1];
|
|
490
|
-
if (!defArg) {
|
|
491
|
-
return fail(
|
|
492
|
-
"entity",
|
|
493
|
-
sourceLocationFromNode(call, sourceFile),
|
|
494
|
-
"expected a definition object as second argument",
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
const definition = readDataLiteralNode(defArg);
|
|
498
|
-
if (!isPlainObject(definition)) {
|
|
499
|
-
return fail(
|
|
500
|
-
"entity",
|
|
501
|
-
sourceLocationFromNode(call, sourceFile),
|
|
502
|
-
"definition could not be read as a plain object (contains functions or identifiers)",
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
return ok({
|
|
506
|
-
kind: "entity",
|
|
507
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
508
|
-
entityName: nameArg.getLiteralValue(),
|
|
509
|
-
// The reader produced a JSON-like object whose runtime shape comes
|
|
510
|
-
// from source code that already type-checks against EntityDefinition.
|
|
511
|
-
// Downstream consumers (Designer, validator) may re-validate before use.
|
|
512
|
-
definition: definition as EntityDefinition,
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
export function extractRelation(
|
|
517
|
-
call: CallExpression,
|
|
518
|
-
sourceFile: SourceFile,
|
|
519
|
-
): ExtractOutput<RelationPattern> {
|
|
520
|
-
const args = call.getArguments();
|
|
521
|
-
const first = args[0];
|
|
522
|
-
if (!first) {
|
|
523
|
-
return fail(
|
|
524
|
-
"relation",
|
|
525
|
-
sourceLocationFromNode(call, sourceFile),
|
|
526
|
-
"expected at least one argument",
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Object-Form: r.relation({ entity, name, kind, to, ...rest })
|
|
531
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
532
|
-
if (obj && args.length === 1) {
|
|
533
|
-
const entityInit = obj
|
|
534
|
-
.getProperty("entity")
|
|
535
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
536
|
-
?.getInitializer();
|
|
537
|
-
if (!entityInit) {
|
|
538
|
-
return fail(
|
|
539
|
-
"relation",
|
|
540
|
-
sourceLocationFromNode(call, sourceFile),
|
|
541
|
-
"object form requires an `entity` property",
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
const entityName = readNameOrRef(entityInit);
|
|
545
|
-
if (!entityName) {
|
|
546
|
-
return fail(
|
|
547
|
-
"relation",
|
|
548
|
-
sourceLocationFromNode(call, sourceFile),
|
|
549
|
-
'`entity` must be a string literal or `{ name: "..." }` ref',
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
const nameInit = obj
|
|
553
|
-
.getProperty("name")
|
|
554
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
555
|
-
?.getInitializer()
|
|
556
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
557
|
-
if (!nameInit) {
|
|
558
|
-
return fail(
|
|
559
|
-
"relation",
|
|
560
|
-
sourceLocationFromNode(call, sourceFile),
|
|
561
|
-
"object form requires a string-literal `name` property",
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
const definition = readDataLiteralNode(obj);
|
|
565
|
-
if (!isPlainObject(definition)) {
|
|
566
|
-
return fail(
|
|
567
|
-
"relation",
|
|
568
|
-
sourceLocationFromNode(call, sourceFile),
|
|
569
|
-
"definition could not be read as a plain object",
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
// Strip the carrier-properties — `entity` and `name` live separately
|
|
573
|
-
// on the pattern, the rest stays in `definition`.
|
|
574
|
-
const { entity: _e, name: _n, ...defWithoutCarriers } = definition;
|
|
575
|
-
return ok({
|
|
576
|
-
kind: "relation",
|
|
577
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
578
|
-
entityName,
|
|
579
|
-
relationName: nameInit.getLiteralValue(),
|
|
580
|
-
definition: defWithoutCarriers as RelationDefinition,
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Legacy positional: r.relation(entity, name, def)
|
|
585
|
-
const entityName = readNameOrRef(first);
|
|
586
|
-
if (!entityName) {
|
|
587
|
-
return fail(
|
|
588
|
-
"relation",
|
|
589
|
-
sourceLocationFromNode(call, sourceFile),
|
|
590
|
-
'first argument must be a string literal or an inline { name: "..." } object (or use the object form)',
|
|
591
|
-
);
|
|
592
|
-
}
|
|
593
|
-
const nameArg = args[1]?.asKind(SyntaxKind.StringLiteral);
|
|
594
|
-
if (!nameArg) {
|
|
595
|
-
return fail(
|
|
596
|
-
"relation",
|
|
597
|
-
sourceLocationFromNode(call, sourceFile),
|
|
598
|
-
"second argument must be a string literal relation name",
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
const defArg = args[2];
|
|
602
|
-
if (!defArg) {
|
|
603
|
-
return fail(
|
|
604
|
-
"relation",
|
|
605
|
-
sourceLocationFromNode(call, sourceFile),
|
|
606
|
-
"expected a definition object as third argument",
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
const definition = readDataLiteralNode(defArg);
|
|
610
|
-
if (!isPlainObject(definition)) {
|
|
611
|
-
return fail(
|
|
612
|
-
"relation",
|
|
613
|
-
sourceLocationFromNode(call, sourceFile),
|
|
614
|
-
"definition could not be read as a plain object",
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
return ok({
|
|
618
|
-
kind: "relation",
|
|
619
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
620
|
-
entityName,
|
|
621
|
-
relationName: nameArg.getLiteralValue(),
|
|
622
|
-
definition: definition as RelationDefinition,
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
export function extractNav(
|
|
627
|
-
call: CallExpression,
|
|
628
|
-
sourceFile: SourceFile,
|
|
629
|
-
): ExtractOutput<NavPattern> {
|
|
630
|
-
const arg = call.getArguments()[0];
|
|
631
|
-
if (!arg) {
|
|
632
|
-
return fail(
|
|
633
|
-
"nav",
|
|
634
|
-
sourceLocationFromNode(call, sourceFile),
|
|
635
|
-
"expected a NavDefinition object as first argument",
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
const definition = readDataLiteralNode(arg);
|
|
639
|
-
if (!isPlainObject(definition)) {
|
|
640
|
-
return fail(
|
|
641
|
-
"nav",
|
|
642
|
-
sourceLocationFromNode(call, sourceFile),
|
|
643
|
-
"definition could not be read as a plain object",
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
return ok({
|
|
647
|
-
kind: "nav",
|
|
648
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
649
|
-
definition: definition as NavDefinition,
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
export function extractWorkspace(
|
|
654
|
-
call: CallExpression,
|
|
655
|
-
sourceFile: SourceFile,
|
|
656
|
-
): ExtractOutput<WorkspacePattern> {
|
|
657
|
-
const arg = call.getArguments()[0];
|
|
658
|
-
if (!arg) {
|
|
659
|
-
return fail(
|
|
660
|
-
"workspace",
|
|
661
|
-
sourceLocationFromNode(call, sourceFile),
|
|
662
|
-
"expected a WorkspaceDefinition object as first argument",
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
const definition = readDataLiteralNode(arg);
|
|
666
|
-
if (!isPlainObject(definition)) {
|
|
667
|
-
return fail(
|
|
668
|
-
"workspace",
|
|
669
|
-
sourceLocationFromNode(call, sourceFile),
|
|
670
|
-
"definition could not be read as a plain object",
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
return ok({
|
|
674
|
-
kind: "workspace",
|
|
675
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
676
|
-
definition: definition as WorkspaceDefinition,
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// =============================================================================
|
|
681
|
-
// Round 3 — complex static patterns
|
|
682
|
-
//
|
|
683
|
-
// Two-argument extractors (metric, secret, claimKey) take a string-literal
|
|
684
|
-
// short name plus an options object. The options-object extractors
|
|
685
|
-
// (config, translations) wrap a `keys` map. referenceData/useExtension
|
|
686
|
-
// take an entity reference plus payload.
|
|
687
|
-
// =============================================================================
|
|
688
|
-
|
|
689
|
-
export function extractConfig(
|
|
690
|
-
call: CallExpression,
|
|
691
|
-
sourceFile: SourceFile,
|
|
692
|
-
): ExtractOutput<ConfigPattern> {
|
|
693
|
-
const arg = call.getArguments()[0];
|
|
694
|
-
if (!arg) {
|
|
695
|
-
return fail(
|
|
696
|
-
"config",
|
|
697
|
-
sourceLocationFromNode(call, sourceFile),
|
|
698
|
-
"expected `{ keys: { ... } }` as first argument",
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
const obj = readDataLiteralNode(arg);
|
|
702
|
-
if (!isPlainObject(obj)) {
|
|
703
|
-
return fail(
|
|
704
|
-
"config",
|
|
705
|
-
sourceLocationFromNode(call, sourceFile),
|
|
706
|
-
"argument could not be read as a plain object",
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
const keys = obj["keys"];
|
|
710
|
-
if (!isPlainObject(keys)) {
|
|
711
|
-
return fail(
|
|
712
|
-
"config",
|
|
713
|
-
sourceLocationFromNode(call, sourceFile),
|
|
714
|
-
"missing or non-object `keys` property",
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
return ok({
|
|
718
|
-
kind: "config",
|
|
719
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
720
|
-
keys: keys as Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>,
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
export function extractTranslations(
|
|
725
|
-
call: CallExpression,
|
|
726
|
-
sourceFile: SourceFile,
|
|
727
|
-
): ExtractOutput<TranslationsPattern> {
|
|
728
|
-
const arg = call.getArguments()[0];
|
|
729
|
-
if (!arg) {
|
|
730
|
-
return fail(
|
|
731
|
-
"translations",
|
|
732
|
-
sourceLocationFromNode(call, sourceFile),
|
|
733
|
-
"expected `{ keys: { ... } }` as first argument",
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
const obj = readDataLiteralNode(arg);
|
|
737
|
-
if (!isPlainObject(obj)) {
|
|
738
|
-
return fail(
|
|
739
|
-
"translations",
|
|
740
|
-
sourceLocationFromNode(call, sourceFile),
|
|
741
|
-
"argument could not be read as a plain object",
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
const keys = obj["keys"];
|
|
745
|
-
if (!isPlainObject(keys)) {
|
|
746
|
-
return fail(
|
|
747
|
-
"translations",
|
|
748
|
-
sourceLocationFromNode(call, sourceFile),
|
|
749
|
-
"missing or non-object `keys` property",
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
return ok({
|
|
753
|
-
kind: "translations",
|
|
754
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
755
|
-
keys: keys as TranslationKeys,
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Shared shape for extractors that take a name + options bag — accepts
|
|
760
|
-
// both positional `(name, { ...options })` and single object-form
|
|
761
|
-
// `({ name, ...options })`. Returns the parsed name + the options bag
|
|
762
|
-
// (options bag minus the `name` property in object-form), or routes
|
|
763
|
-
// the failure through `fail()` so error-reason strings don't show up
|
|
764
|
-
// as object-literal `reason:` properties (the error-reasons-guard
|
|
765
|
-
// expects snake_case for those, and our parser-error reasons are
|
|
766
|
-
// human-prose).
|
|
767
|
-
type NamedOptionsResult =
|
|
768
|
-
| { readonly kind: "ok"; readonly name: string; readonly options: Record<string, unknown> }
|
|
769
|
-
| { readonly kind: "error"; readonly error: ParseError };
|
|
770
|
-
|
|
771
|
-
function readNamedOptions(
|
|
772
|
-
call: CallExpression,
|
|
773
|
-
sourceFile: SourceFile,
|
|
774
|
-
methodName: string,
|
|
775
|
-
): NamedOptionsResult {
|
|
776
|
-
const args = call.getArguments();
|
|
777
|
-
const first = args[0];
|
|
778
|
-
if (!first) {
|
|
779
|
-
return fail(
|
|
780
|
-
methodName,
|
|
781
|
-
sourceLocationFromNode(call, sourceFile),
|
|
782
|
-
"expected at least one argument",
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Object-Form: r.method({ name: "...", ...options })
|
|
787
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
788
|
-
if (obj && args.length === 1) {
|
|
789
|
-
const nameInit = obj
|
|
790
|
-
.getProperty("name")
|
|
791
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
792
|
-
?.getInitializer()
|
|
793
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
794
|
-
if (!nameInit) {
|
|
795
|
-
return fail(
|
|
796
|
-
methodName,
|
|
797
|
-
sourceLocationFromNode(call, sourceFile),
|
|
798
|
-
"object form requires a string-literal `name` property",
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
const data = readDataLiteralNode(obj);
|
|
802
|
-
if (!isPlainObject(data)) {
|
|
803
|
-
return fail(
|
|
804
|
-
methodName,
|
|
805
|
-
sourceLocationFromNode(call, sourceFile),
|
|
806
|
-
"argument could not be read as a plain object",
|
|
807
|
-
);
|
|
808
|
-
}
|
|
809
|
-
const { name: _name, ...optionsWithoutName } = data;
|
|
810
|
-
return { kind: "ok", name: nameInit.getLiteralValue(), options: optionsWithoutName };
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// Legacy positional: r.method("name", { ...options })
|
|
814
|
-
const nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
815
|
-
if (!nameLiteral) {
|
|
816
|
-
return fail(
|
|
817
|
-
methodName,
|
|
818
|
-
sourceLocationFromNode(call, sourceFile),
|
|
819
|
-
"first argument must be a string literal name (or use the object form)",
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
const optionsArg = args[1];
|
|
823
|
-
if (!optionsArg) {
|
|
824
|
-
return fail(
|
|
825
|
-
methodName,
|
|
826
|
-
sourceLocationFromNode(call, sourceFile),
|
|
827
|
-
"expected an options object as second argument",
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
const options = readDataLiteralNode(optionsArg);
|
|
831
|
-
if (!isPlainObject(options)) {
|
|
832
|
-
return fail(
|
|
833
|
-
methodName,
|
|
834
|
-
sourceLocationFromNode(call, sourceFile),
|
|
835
|
-
"options could not be read as a plain object",
|
|
836
|
-
);
|
|
837
|
-
}
|
|
838
|
-
return { kind: "ok", name: nameLiteral.getLiteralValue(), options };
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
export function extractMetric(
|
|
842
|
-
call: CallExpression,
|
|
843
|
-
sourceFile: SourceFile,
|
|
844
|
-
): ExtractOutput<MetricPattern> {
|
|
845
|
-
const parsed = readNamedOptions(call, sourceFile, "metric");
|
|
846
|
-
if (parsed.kind === "error") return parsed;
|
|
847
|
-
return ok({
|
|
848
|
-
kind: "metric",
|
|
849
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
850
|
-
shortName: parsed.name,
|
|
851
|
-
options: parsed.options as MetricOptions,
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
export function extractSecret(
|
|
856
|
-
call: CallExpression,
|
|
857
|
-
sourceFile: SourceFile,
|
|
858
|
-
): ExtractOutput<SecretPattern> {
|
|
859
|
-
const parsed = readNamedOptions(call, sourceFile, "secret");
|
|
860
|
-
if (parsed.kind === "error") return parsed;
|
|
861
|
-
return ok({
|
|
862
|
-
kind: "secret",
|
|
863
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
864
|
-
shortName: parsed.name,
|
|
865
|
-
options: parsed.options as SecretOptions,
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
export function extractClaimKey(
|
|
870
|
-
call: CallExpression,
|
|
871
|
-
sourceFile: SourceFile,
|
|
872
|
-
): ExtractOutput<ClaimKeyPattern> {
|
|
873
|
-
const parsed = readNamedOptions(call, sourceFile, "claimKey");
|
|
874
|
-
if (parsed.kind === "error") return parsed;
|
|
875
|
-
const claimType = parsed.options["type"];
|
|
876
|
-
if (!isClaimKeyType(claimType)) {
|
|
877
|
-
return fail(
|
|
878
|
-
"claimKey",
|
|
879
|
-
sourceLocationFromNode(call, sourceFile),
|
|
880
|
-
'type must be one of "string" | "number" | "boolean" | "string[]" | "object"',
|
|
881
|
-
);
|
|
882
|
-
}
|
|
883
|
-
return ok({
|
|
884
|
-
kind: "claimKey",
|
|
885
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
886
|
-
shortName: parsed.name,
|
|
887
|
-
claimType,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function isClaimKeyType(value: unknown): value is ClaimKeyType {
|
|
892
|
-
return (
|
|
893
|
-
value === "string" ||
|
|
894
|
-
value === "number" ||
|
|
895
|
-
value === "boolean" ||
|
|
896
|
-
value === "string[]" ||
|
|
897
|
-
value === "object"
|
|
898
|
-
);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
export function extractReferenceData(
|
|
902
|
-
call: CallExpression,
|
|
903
|
-
sourceFile: SourceFile,
|
|
904
|
-
): ExtractOutput<ReferenceDataPattern> {
|
|
905
|
-
const args = call.getArguments();
|
|
906
|
-
const first = args[0];
|
|
907
|
-
if (!first) {
|
|
908
|
-
return fail(
|
|
909
|
-
"referenceData",
|
|
910
|
-
sourceLocationFromNode(call, sourceFile),
|
|
911
|
-
"expected at least one argument",
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// Object-Form: r.referenceData({ entity, data, upsertKey? })
|
|
916
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
917
|
-
if (obj && args.length === 1) {
|
|
918
|
-
const entityInit = obj
|
|
919
|
-
.getProperty("entity")
|
|
920
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
921
|
-
?.getInitializer();
|
|
922
|
-
if (!entityInit) {
|
|
923
|
-
return fail(
|
|
924
|
-
"referenceData",
|
|
925
|
-
sourceLocationFromNode(call, sourceFile),
|
|
926
|
-
"object form requires an `entity` property",
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
const entityName = readNameOrRef(entityInit);
|
|
930
|
-
if (!entityName) {
|
|
931
|
-
return fail(
|
|
932
|
-
"referenceData",
|
|
933
|
-
sourceLocationFromNode(call, sourceFile),
|
|
934
|
-
'`entity` must be a string literal or `{ name: "..." }` ref',
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
const dataInit = obj
|
|
938
|
-
.getProperty("data")
|
|
939
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
940
|
-
?.getInitializer();
|
|
941
|
-
if (!dataInit) {
|
|
942
|
-
return fail(
|
|
943
|
-
"referenceData",
|
|
944
|
-
sourceLocationFromNode(call, sourceFile),
|
|
945
|
-
"object form requires a `data` property",
|
|
946
|
-
);
|
|
947
|
-
}
|
|
948
|
-
const data = readDataLiteralNode(dataInit);
|
|
949
|
-
if (!Array.isArray(data) || !data.every(isPlainObject)) {
|
|
950
|
-
return fail(
|
|
951
|
-
"referenceData",
|
|
952
|
-
sourceLocationFromNode(call, sourceFile),
|
|
953
|
-
"data must be an array of plain objects",
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
let upsertKey: string | undefined;
|
|
957
|
-
const upsertKeyInit = obj
|
|
958
|
-
.getProperty("upsertKey")
|
|
959
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
960
|
-
?.getInitializer()
|
|
961
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
962
|
-
if (upsertKeyInit) {
|
|
963
|
-
upsertKey = upsertKeyInit.getLiteralValue();
|
|
964
|
-
}
|
|
965
|
-
return ok({
|
|
966
|
-
kind: "referenceData",
|
|
967
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
968
|
-
entityName,
|
|
969
|
-
data: data as readonly Record<string, unknown>[],
|
|
970
|
-
...(upsertKey !== undefined && { upsertKey }),
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Legacy positional: r.referenceData(entity, data, options?)
|
|
975
|
-
const entityName = readNameOrRef(first);
|
|
976
|
-
if (!entityName) {
|
|
977
|
-
return fail(
|
|
978
|
-
"referenceData",
|
|
979
|
-
sourceLocationFromNode(call, sourceFile),
|
|
980
|
-
'first argument must be a string literal or an inline { name: "..." } object (or use the object form)',
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
const dataArg = args[1];
|
|
984
|
-
if (!dataArg) {
|
|
985
|
-
return fail(
|
|
986
|
-
"referenceData",
|
|
987
|
-
sourceLocationFromNode(call, sourceFile),
|
|
988
|
-
"expected a data array as second argument",
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
const data = readDataLiteralNode(dataArg);
|
|
992
|
-
if (!Array.isArray(data) || !data.every(isPlainObject)) {
|
|
993
|
-
return fail(
|
|
994
|
-
"referenceData",
|
|
995
|
-
sourceLocationFromNode(call, sourceFile),
|
|
996
|
-
"data must be an array of plain objects",
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
let upsertKey: string | undefined;
|
|
1000
|
-
const optionsArg = args[2];
|
|
1001
|
-
if (optionsArg) {
|
|
1002
|
-
const options = readDataLiteralNode(optionsArg);
|
|
1003
|
-
if (!isPlainObject(options)) {
|
|
1004
|
-
return fail(
|
|
1005
|
-
"referenceData",
|
|
1006
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1007
|
-
"options could not be read as a plain object",
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
if (options["upsertKey"] !== undefined) {
|
|
1011
|
-
if (typeof options["upsertKey"] !== "string") {
|
|
1012
|
-
return fail(
|
|
1013
|
-
"referenceData",
|
|
1014
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1015
|
-
"upsertKey must be a string when provided",
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
upsertKey = options["upsertKey"];
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
return ok({
|
|
1022
|
-
kind: "referenceData",
|
|
1023
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1024
|
-
entityName,
|
|
1025
|
-
data: data as readonly Record<string, unknown>[],
|
|
1026
|
-
...(upsertKey !== undefined && { upsertKey }),
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
export function extractUseExtension(
|
|
1031
|
-
call: CallExpression,
|
|
1032
|
-
sourceFile: SourceFile,
|
|
1033
|
-
): ExtractOutput<UseExtensionPattern> {
|
|
1034
|
-
const args = call.getArguments();
|
|
1035
|
-
const first = args[0];
|
|
1036
|
-
if (!first) {
|
|
1037
|
-
return fail(
|
|
1038
|
-
"useExtension",
|
|
1039
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1040
|
-
"expected at least one argument",
|
|
1041
|
-
);
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Object-Form: r.useExtension({ name, entity, options? })
|
|
1045
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1046
|
-
if (obj && args.length === 1) {
|
|
1047
|
-
const nameInit = obj
|
|
1048
|
-
.getProperty("name")
|
|
1049
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1050
|
-
?.getInitializer()
|
|
1051
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1052
|
-
if (!nameInit) {
|
|
1053
|
-
return fail(
|
|
1054
|
-
"useExtension",
|
|
1055
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1056
|
-
"object form requires a string-literal `name` property",
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
const entityInit = obj
|
|
1060
|
-
.getProperty("entity")
|
|
1061
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1062
|
-
?.getInitializer();
|
|
1063
|
-
if (!entityInit) {
|
|
1064
|
-
return fail(
|
|
1065
|
-
"useExtension",
|
|
1066
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1067
|
-
"object form requires an `entity` property",
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
const entityName = readNameOrRef(entityInit);
|
|
1071
|
-
if (!entityName) {
|
|
1072
|
-
return fail(
|
|
1073
|
-
"useExtension",
|
|
1074
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1075
|
-
'`entity` must be a string literal or `{ name: "..." }` ref',
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
let options: Readonly<Record<string, unknown>> | undefined;
|
|
1079
|
-
const optionsInit = obj
|
|
1080
|
-
.getProperty("options")
|
|
1081
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1082
|
-
?.getInitializer();
|
|
1083
|
-
if (optionsInit) {
|
|
1084
|
-
const parsed = readDataLiteralNode(optionsInit);
|
|
1085
|
-
if (!isPlainObject(parsed)) {
|
|
1086
|
-
return fail(
|
|
1087
|
-
"useExtension",
|
|
1088
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1089
|
-
"options could not be read as a plain object",
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
options = parsed;
|
|
1093
|
-
}
|
|
1094
|
-
return ok({
|
|
1095
|
-
kind: "useExtension",
|
|
1096
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1097
|
-
extensionName: nameInit.getLiteralValue(),
|
|
1098
|
-
entityName,
|
|
1099
|
-
...(options !== undefined && { options }),
|
|
1100
|
-
});
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
// Legacy positional: r.useExtension(name, entity, options?)
|
|
1104
|
-
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
1105
|
-
if (!nameArg) {
|
|
1106
|
-
return fail(
|
|
1107
|
-
"useExtension",
|
|
1108
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1109
|
-
"first argument must be a string literal extension name (or use the object form)",
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
const entityRefArg = args[1];
|
|
1113
|
-
if (!entityRefArg) {
|
|
1114
|
-
return fail(
|
|
1115
|
-
"useExtension",
|
|
1116
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1117
|
-
"expected an entity reference as second argument",
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
const entityName = readNameOrRef(entityRefArg);
|
|
1121
|
-
if (!entityName) {
|
|
1122
|
-
return fail(
|
|
1123
|
-
"useExtension",
|
|
1124
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1125
|
-
'second argument must be a string literal or an inline { name: "..." } object',
|
|
1126
|
-
);
|
|
1127
|
-
}
|
|
1128
|
-
const optionsArg = args[2];
|
|
1129
|
-
let options: Readonly<Record<string, unknown>> | undefined;
|
|
1130
|
-
if (optionsArg) {
|
|
1131
|
-
const parsed = readDataLiteralNode(optionsArg);
|
|
1132
|
-
if (!isPlainObject(parsed)) {
|
|
1133
|
-
return fail(
|
|
1134
|
-
"useExtension",
|
|
1135
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1136
|
-
"options could not be read as a plain object",
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
options = parsed;
|
|
1140
|
-
}
|
|
1141
|
-
return ok({
|
|
1142
|
-
kind: "useExtension",
|
|
1143
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1144
|
-
extensionName: nameArg.getLiteralValue(),
|
|
1145
|
-
entityName,
|
|
1146
|
-
...(options !== undefined && { options }),
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
// =============================================================================
|
|
1151
|
-
// Round 4 — mixed patterns (header data + opaque body source)
|
|
1152
|
-
//
|
|
1153
|
-
// Each extractor reads the static parts (name, type, target) declaratively
|
|
1154
|
-
// and captures any closure / Zod-schema as a SourceLocation pointing at
|
|
1155
|
-
// the raw source span. Designer renders the body as a read-only block;
|
|
1156
|
-
// the AI patcher overwrites the span verbatim.
|
|
1157
|
-
//
|
|
1158
|
-
// Closure detection: findFunctionLiteral matches an inline arrow function
|
|
1159
|
-
// or function expression. A captured-const reference (e.g. r.hook(...,
|
|
1160
|
-
// myHandler)) is rejected with a ParseError — those need to be inlined.
|
|
1161
|
-
// =============================================================================
|
|
1162
|
-
|
|
1163
|
-
function isHookType(value: string): value is LifecycleHookType | "validation" {
|
|
1164
|
-
return (
|
|
1165
|
-
value === "preSave" ||
|
|
1166
|
-
value === "postSave" ||
|
|
1167
|
-
value === "preDelete" ||
|
|
1168
|
-
value === "postDelete" ||
|
|
1169
|
-
value === "preQuery" ||
|
|
1170
|
-
value === "validation"
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function isHttpRouteMethod(value: string): value is HttpRouteMethod {
|
|
1175
|
-
return (
|
|
1176
|
-
value === "GET" ||
|
|
1177
|
-
value === "POST" ||
|
|
1178
|
-
value === "PUT" ||
|
|
1179
|
-
value === "PATCH" ||
|
|
1180
|
-
value === "DELETE" ||
|
|
1181
|
-
value === "HEAD" ||
|
|
1182
|
-
value === "OPTIONS"
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function readOptionalPhase(node: Node | undefined): HookPhase | undefined {
|
|
1187
|
-
if (!node) return undefined;
|
|
1188
|
-
const obj = readDataLiteralNode(node);
|
|
1189
|
-
if (!isPlainObject(obj)) return undefined;
|
|
1190
|
-
const phase = obj["phase"];
|
|
1191
|
-
if (phase === "inTransaction" || phase === "afterCommit") return phase as HookPhase;
|
|
1192
|
-
return undefined;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function readOptionalAccessRule(value: unknown): AccessRule | undefined {
|
|
1196
|
-
if (!isPlainObject(value)) return undefined;
|
|
1197
|
-
if (Array.isArray(value["roles"]) && value["roles"].every((r) => typeof r === "string")) {
|
|
1198
|
-
return { roles: value["roles"] as readonly string[] };
|
|
1199
|
-
}
|
|
1200
|
-
if (value["openToAll"] === true) {
|
|
1201
|
-
return { openToAll: true };
|
|
1202
|
-
}
|
|
1203
|
-
return undefined;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
function readOptionalRateLimit(value: unknown): RateLimitOption | undefined {
|
|
1207
|
-
if (!isPlainObject(value)) return undefined;
|
|
1208
|
-
if (typeof value["per"] !== "string") return undefined;
|
|
1209
|
-
if (typeof value["limit"] !== "number") return undefined;
|
|
1210
|
-
if (typeof value["windowSeconds"] !== "number") return undefined;
|
|
1211
|
-
return value as unknown as RateLimitOption;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
export function extractHook(
|
|
1215
|
-
call: CallExpression,
|
|
1216
|
-
sourceFile: SourceFile,
|
|
1217
|
-
): ExtractOutput<HookPattern> {
|
|
1218
|
-
const args = call.getArguments();
|
|
1219
|
-
const first = args[0];
|
|
1220
|
-
if (!first) {
|
|
1221
|
-
return fail("hook", sourceLocationFromNode(call, sourceFile), "expected at least one argument");
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// Object-Form: r.hook({ type, target, handler, phase? })
|
|
1225
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1226
|
-
if (obj && args.length === 1) {
|
|
1227
|
-
const typeInit = obj
|
|
1228
|
-
.getProperty("type")
|
|
1229
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1230
|
-
?.getInitializer()
|
|
1231
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1232
|
-
if (!typeInit) {
|
|
1233
|
-
return fail(
|
|
1234
|
-
"hook",
|
|
1235
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1236
|
-
"object form requires a string-literal `type` property",
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
|
-
const hookType = typeInit.getLiteralValue();
|
|
1240
|
-
if (!isHookType(hookType)) {
|
|
1241
|
-
return fail(
|
|
1242
|
-
"hook",
|
|
1243
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1244
|
-
`hook type "${hookType}" is not one of the lifecycle types or "validation"`,
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
|
-
const targetInit = obj
|
|
1248
|
-
.getProperty("target")
|
|
1249
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1250
|
-
?.getInitializer();
|
|
1251
|
-
if (!targetInit) {
|
|
1252
|
-
return fail(
|
|
1253
|
-
"hook",
|
|
1254
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1255
|
-
"object form requires a `target` property",
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
const target = readNameOrRefOrList(targetInit);
|
|
1259
|
-
if (!target) {
|
|
1260
|
-
return fail(
|
|
1261
|
-
"hook",
|
|
1262
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1263
|
-
"target must be a string literal, an inline { name } object, or an array",
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
const handlerInit = obj
|
|
1267
|
-
.getProperty("handler")
|
|
1268
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1269
|
-
?.getInitializer();
|
|
1270
|
-
if (!handlerInit) {
|
|
1271
|
-
return fail(
|
|
1272
|
-
"hook",
|
|
1273
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1274
|
-
"object form requires a `handler` property",
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
const fn = findFunctionLiteral(handlerInit);
|
|
1278
|
-
if (!fn) {
|
|
1279
|
-
return fail(
|
|
1280
|
-
"hook",
|
|
1281
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1282
|
-
"handler must be an inline arrow function or function expression",
|
|
1283
|
-
);
|
|
1284
|
-
}
|
|
1285
|
-
const phase = readOptionalPhase(obj);
|
|
1286
|
-
return ok({
|
|
1287
|
-
kind: "hook",
|
|
1288
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1289
|
-
hookType,
|
|
1290
|
-
target,
|
|
1291
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
1292
|
-
...(phase !== undefined && { phase }),
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// Legacy positional: r.hook(type, target, fn, options?)
|
|
1297
|
-
const typeArg = first.asKind(SyntaxKind.StringLiteral);
|
|
1298
|
-
if (!typeArg) {
|
|
1299
|
-
return fail(
|
|
1300
|
-
"hook",
|
|
1301
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1302
|
-
"first argument must be a string literal hook type (or use the object form)",
|
|
1303
|
-
);
|
|
1304
|
-
}
|
|
1305
|
-
const hookType = typeArg.getLiteralValue();
|
|
1306
|
-
if (!isHookType(hookType)) {
|
|
1307
|
-
return fail(
|
|
1308
|
-
"hook",
|
|
1309
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1310
|
-
`hook type "${hookType}" is not one of the lifecycle types or "validation"`,
|
|
1311
|
-
);
|
|
1312
|
-
}
|
|
1313
|
-
const targetArg = args[1];
|
|
1314
|
-
if (!targetArg) {
|
|
1315
|
-
return fail(
|
|
1316
|
-
"hook",
|
|
1317
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1318
|
-
"expected a target (NameOrRef or array) as second argument",
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
const target = readNameOrRefOrList(targetArg);
|
|
1322
|
-
if (!target) {
|
|
1323
|
-
return fail(
|
|
1324
|
-
"hook",
|
|
1325
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1326
|
-
"target must be a string literal, an inline { name } object, or an array",
|
|
1327
|
-
);
|
|
1328
|
-
}
|
|
1329
|
-
const fnArg = args[2];
|
|
1330
|
-
if (!fnArg) {
|
|
1331
|
-
return fail(
|
|
1332
|
-
"hook",
|
|
1333
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1334
|
-
"expected a hook function as third argument",
|
|
1335
|
-
);
|
|
1336
|
-
}
|
|
1337
|
-
const fn = findFunctionLiteral(fnArg);
|
|
1338
|
-
if (!fn) {
|
|
1339
|
-
return fail(
|
|
1340
|
-
"hook",
|
|
1341
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1342
|
-
"third argument must be an inline arrow function or function expression",
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
const phase = readOptionalPhase(args[3]);
|
|
1346
|
-
return ok({
|
|
1347
|
-
kind: "hook",
|
|
1348
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1349
|
-
hookType,
|
|
1350
|
-
target,
|
|
1351
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
1352
|
-
...(phase !== undefined && { phase }),
|
|
1353
|
-
});
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
function isEntityHookType(value: string): value is "postSave" | "preDelete" | "postDelete" {
|
|
1357
|
-
return value === "postSave" || value === "preDelete" || value === "postDelete";
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
export function extractEntityHook(
|
|
1361
|
-
call: CallExpression,
|
|
1362
|
-
sourceFile: SourceFile,
|
|
1363
|
-
): ExtractOutput<EntityHookPattern> {
|
|
1364
|
-
const args = call.getArguments();
|
|
1365
|
-
const first = args[0];
|
|
1366
|
-
if (!first) {
|
|
1367
|
-
return fail(
|
|
1368
|
-
"entityHook",
|
|
1369
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1370
|
-
"expected at least one argument",
|
|
1371
|
-
);
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// Object-Form: r.entityHook({ type, entity, handler, phase? })
|
|
1375
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1376
|
-
if (obj && args.length === 1) {
|
|
1377
|
-
const typeInit = obj
|
|
1378
|
-
.getProperty("type")
|
|
1379
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1380
|
-
?.getInitializer()
|
|
1381
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1382
|
-
if (!typeInit) {
|
|
1383
|
-
return fail(
|
|
1384
|
-
"entityHook",
|
|
1385
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1386
|
-
"object form requires a string-literal `type` property",
|
|
1387
|
-
);
|
|
1388
|
-
}
|
|
1389
|
-
const hookType = typeInit.getLiteralValue();
|
|
1390
|
-
if (!isEntityHookType(hookType)) {
|
|
1391
|
-
return fail(
|
|
1392
|
-
"entityHook",
|
|
1393
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1394
|
-
`entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
|
|
1395
|
-
);
|
|
1396
|
-
}
|
|
1397
|
-
const entityInit = obj
|
|
1398
|
-
.getProperty("entity")
|
|
1399
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1400
|
-
?.getInitializer();
|
|
1401
|
-
if (!entityInit) {
|
|
1402
|
-
return fail(
|
|
1403
|
-
"entityHook",
|
|
1404
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1405
|
-
"object form requires an `entity` property",
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
const entityName = readNameOrRef(entityInit);
|
|
1409
|
-
if (!entityName) {
|
|
1410
|
-
return fail(
|
|
1411
|
-
"entityHook",
|
|
1412
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1413
|
-
"`entity` must be a string literal or inline { name } object",
|
|
1414
|
-
);
|
|
1415
|
-
}
|
|
1416
|
-
const handlerInit = obj
|
|
1417
|
-
.getProperty("handler")
|
|
1418
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1419
|
-
?.getInitializer();
|
|
1420
|
-
if (!handlerInit) {
|
|
1421
|
-
return fail(
|
|
1422
|
-
"entityHook",
|
|
1423
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1424
|
-
"object form requires a `handler` property",
|
|
1425
|
-
);
|
|
1426
|
-
}
|
|
1427
|
-
const fn = findFunctionLiteral(handlerInit);
|
|
1428
|
-
if (!fn) {
|
|
1429
|
-
return fail(
|
|
1430
|
-
"entityHook",
|
|
1431
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1432
|
-
"handler must be an inline arrow function or function expression",
|
|
1433
|
-
);
|
|
1434
|
-
}
|
|
1435
|
-
const phase = readOptionalPhase(obj);
|
|
1436
|
-
return ok({
|
|
1437
|
-
kind: "entityHook",
|
|
1438
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1439
|
-
hookType,
|
|
1440
|
-
entityName,
|
|
1441
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
1442
|
-
...(phase !== undefined && { phase }),
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
// Legacy positional: r.entityHook(type, entity, fn, options?)
|
|
1447
|
-
const typeArg = first.asKind(SyntaxKind.StringLiteral);
|
|
1448
|
-
if (!typeArg) {
|
|
1449
|
-
return fail(
|
|
1450
|
-
"entityHook",
|
|
1451
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1452
|
-
"first argument must be a string literal hook type (or use the object form)",
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
const hookType = typeArg.getLiteralValue();
|
|
1456
|
-
if (!isEntityHookType(hookType)) {
|
|
1457
|
-
return fail(
|
|
1458
|
-
"entityHook",
|
|
1459
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1460
|
-
`entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
|
|
1461
|
-
);
|
|
1462
|
-
}
|
|
1463
|
-
const entityArg = args[1];
|
|
1464
|
-
if (!entityArg) {
|
|
1465
|
-
return fail(
|
|
1466
|
-
"entityHook",
|
|
1467
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1468
|
-
"expected an entity reference as second argument",
|
|
1469
|
-
);
|
|
1470
|
-
}
|
|
1471
|
-
const entityName = readNameOrRef(entityArg);
|
|
1472
|
-
if (!entityName) {
|
|
1473
|
-
return fail(
|
|
1474
|
-
"entityHook",
|
|
1475
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1476
|
-
"second argument must be a string literal or inline { name } object",
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
const fnArg = args[2];
|
|
1480
|
-
if (!fnArg) {
|
|
1481
|
-
return fail(
|
|
1482
|
-
"entityHook",
|
|
1483
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1484
|
-
"expected a hook function as third argument",
|
|
1485
|
-
);
|
|
1486
|
-
}
|
|
1487
|
-
const fn = findFunctionLiteral(fnArg);
|
|
1488
|
-
if (!fn) {
|
|
1489
|
-
return fail(
|
|
1490
|
-
"entityHook",
|
|
1491
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1492
|
-
"third argument must be an inline arrow function or function expression",
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
const phase = readOptionalPhase(args[3]);
|
|
1496
|
-
return ok({
|
|
1497
|
-
kind: "entityHook",
|
|
1498
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1499
|
-
hookType,
|
|
1500
|
-
entityName,
|
|
1501
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
1502
|
-
...(phase !== undefined && { phase }),
|
|
1503
|
-
});
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
export function extractAuthClaims(
|
|
1507
|
-
call: CallExpression,
|
|
1508
|
-
sourceFile: SourceFile,
|
|
1509
|
-
): ExtractOutput<AuthClaimsPattern> {
|
|
1510
|
-
const arg = call.getArguments()[0];
|
|
1511
|
-
if (!arg) {
|
|
1512
|
-
return fail(
|
|
1513
|
-
"authClaims",
|
|
1514
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1515
|
-
"expected a function as first argument",
|
|
1516
|
-
);
|
|
1517
|
-
}
|
|
1518
|
-
const fn = findFunctionLiteral(arg);
|
|
1519
|
-
if (!fn) {
|
|
1520
|
-
return fail(
|
|
1521
|
-
"authClaims",
|
|
1522
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1523
|
-
"first argument must be an inline arrow function or function expression",
|
|
1524
|
-
);
|
|
1525
|
-
}
|
|
1526
|
-
return ok({
|
|
1527
|
-
kind: "authClaims",
|
|
1528
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1529
|
-
fnBody: sourceLocationFromNode(fn, sourceFile),
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
// Common fields produced by parseHandlerCall — both write- and query-
|
|
1534
|
-
// handler patterns share them. The wrapper functions below add the
|
|
1535
|
-
// kind-discriminator and the write-only skipTransitionGuard so the
|
|
1536
|
-
// shared helper stays unbiased.
|
|
1537
|
-
type ParsedHandlerCall = {
|
|
1538
|
-
readonly source: SourceLocation;
|
|
1539
|
-
readonly handlerName: string;
|
|
1540
|
-
readonly schemaSource: SourceLocation;
|
|
1541
|
-
readonly handlerBody: SourceLocation;
|
|
1542
|
-
readonly access?: AccessRule;
|
|
1543
|
-
readonly rateLimit?: RateLimitOption;
|
|
1544
|
-
readonly skipTransitionGuard?: boolean;
|
|
1545
|
-
};
|
|
1546
|
-
|
|
1547
|
-
// Shared parser for r.writeHandler / r.queryHandler. Accepts both
|
|
1548
|
-
// inline form r.<method>(name, schema, handler, options?) and the
|
|
1549
|
-
// single-arg object form r.<method>({ name, schema, handler, ... })
|
|
1550
|
-
// (the defineWriteHandler / defineQueryHandler shape).
|
|
1551
|
-
function parseHandlerCall(
|
|
1552
|
-
call: CallExpression,
|
|
1553
|
-
sourceFile: SourceFile,
|
|
1554
|
-
methodName: "writeHandler" | "queryHandler",
|
|
1555
|
-
): ExtractOutput<ParsedHandlerCall> {
|
|
1556
|
-
const args = call.getArguments();
|
|
1557
|
-
const first = args[0];
|
|
1558
|
-
if (!first) {
|
|
1559
|
-
return fail(
|
|
1560
|
-
methodName,
|
|
1561
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1562
|
-
"expected at least one argument",
|
|
1563
|
-
);
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// Object form: a single { name, schema, handler, ... } literal.
|
|
1567
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1568
|
-
if (obj && args.length === 1) {
|
|
1569
|
-
const nameLiteral = obj
|
|
1570
|
-
.getProperty("name")
|
|
1571
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1572
|
-
?.getInitializer()
|
|
1573
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1574
|
-
if (!nameLiteral) {
|
|
1575
|
-
return fail(
|
|
1576
|
-
methodName,
|
|
1577
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1578
|
-
"object form requires a string-literal `name` property",
|
|
1579
|
-
);
|
|
1580
|
-
}
|
|
1581
|
-
const schemaInit = obj
|
|
1582
|
-
.getProperty("schema")
|
|
1583
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1584
|
-
?.getInitializer();
|
|
1585
|
-
if (!schemaInit) {
|
|
1586
|
-
return fail(
|
|
1587
|
-
methodName,
|
|
1588
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1589
|
-
"object form requires a `schema` property",
|
|
1590
|
-
);
|
|
1591
|
-
}
|
|
1592
|
-
const handlerInit = obj
|
|
1593
|
-
.getProperty("handler")
|
|
1594
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1595
|
-
?.getInitializer();
|
|
1596
|
-
if (!handlerInit) {
|
|
1597
|
-
return fail(
|
|
1598
|
-
methodName,
|
|
1599
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1600
|
-
"object form requires a `handler` property",
|
|
1601
|
-
);
|
|
1602
|
-
}
|
|
1603
|
-
const fn = findFunctionLiteral(handlerInit);
|
|
1604
|
-
if (!fn) {
|
|
1605
|
-
return fail(
|
|
1606
|
-
methodName,
|
|
1607
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1608
|
-
"handler must be an inline arrow function or function expression",
|
|
1609
|
-
);
|
|
1610
|
-
}
|
|
1611
|
-
const accessInit = obj
|
|
1612
|
-
.getProperty("access")
|
|
1613
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1614
|
-
?.getInitializer();
|
|
1615
|
-
const access = accessInit ? readOptionalAccessRule(readDataLiteralNode(accessInit)) : undefined;
|
|
1616
|
-
const rateLimitInit = obj
|
|
1617
|
-
.getProperty("rateLimit")
|
|
1618
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1619
|
-
?.getInitializer();
|
|
1620
|
-
const rateLimit = rateLimitInit
|
|
1621
|
-
? readOptionalRateLimit(readDataLiteralNode(rateLimitInit))
|
|
1622
|
-
: undefined;
|
|
1623
|
-
const skip = readBooleanProperty(obj, "skipTransitionGuard");
|
|
1624
|
-
return ok({
|
|
1625
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1626
|
-
handlerName: nameLiteral.getLiteralValue(),
|
|
1627
|
-
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
1628
|
-
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
1629
|
-
...(access !== undefined && { access }),
|
|
1630
|
-
...(rateLimit !== undefined && { rateLimit }),
|
|
1631
|
-
...(skip === true && { skipTransitionGuard: true }),
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// Inline form: (name, schema, handler, options?).
|
|
1636
|
-
const nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
1637
|
-
if (!nameLiteral) {
|
|
1638
|
-
return fail(
|
|
1639
|
-
methodName,
|
|
1640
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1641
|
-
"first argument must be a string literal handler name (or use the object form)",
|
|
1642
|
-
);
|
|
1643
|
-
}
|
|
1644
|
-
const schemaArg = args[1];
|
|
1645
|
-
if (!schemaArg) {
|
|
1646
|
-
return fail(
|
|
1647
|
-
methodName,
|
|
1648
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1649
|
-
"expected a Zod schema as second argument",
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1652
|
-
const handlerArg = args[2];
|
|
1653
|
-
if (!handlerArg) {
|
|
1654
|
-
return fail(
|
|
1655
|
-
methodName,
|
|
1656
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1657
|
-
"expected a handler function as third argument",
|
|
1658
|
-
);
|
|
1659
|
-
}
|
|
1660
|
-
const fn = findFunctionLiteral(handlerArg);
|
|
1661
|
-
if (!fn) {
|
|
1662
|
-
return fail(
|
|
1663
|
-
methodName,
|
|
1664
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1665
|
-
"third argument must be an inline arrow function or function expression",
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
const optionsArg = args[3];
|
|
1669
|
-
let access: AccessRule | undefined;
|
|
1670
|
-
let rateLimit: RateLimitOption | undefined;
|
|
1671
|
-
if (optionsArg) {
|
|
1672
|
-
const options = readDataLiteralNode(optionsArg);
|
|
1673
|
-
if (isPlainObject(options)) {
|
|
1674
|
-
access = readOptionalAccessRule(options["access"]);
|
|
1675
|
-
rateLimit = readOptionalRateLimit(options["rateLimit"]);
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
return ok({
|
|
1679
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1680
|
-
handlerName: nameLiteral.getLiteralValue(),
|
|
1681
|
-
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
1682
|
-
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
1683
|
-
...(access !== undefined && { access }),
|
|
1684
|
-
...(rateLimit !== undefined && { rateLimit }),
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
export function extractWriteHandler(
|
|
1689
|
-
call: CallExpression,
|
|
1690
|
-
sourceFile: SourceFile,
|
|
1691
|
-
): ExtractOutput<WriteHandlerPattern> {
|
|
1692
|
-
const parsed = parseHandlerCall(call, sourceFile, "writeHandler");
|
|
1693
|
-
if (parsed.kind === "error") return parsed;
|
|
1694
|
-
return ok({
|
|
1695
|
-
kind: "writeHandler",
|
|
1696
|
-
source: parsed.pattern.source,
|
|
1697
|
-
handlerName: parsed.pattern.handlerName,
|
|
1698
|
-
schemaSource: parsed.pattern.schemaSource,
|
|
1699
|
-
handlerBody: parsed.pattern.handlerBody,
|
|
1700
|
-
...(parsed.pattern.access !== undefined && { access: parsed.pattern.access }),
|
|
1701
|
-
...(parsed.pattern.rateLimit !== undefined && { rateLimit: parsed.pattern.rateLimit }),
|
|
1702
|
-
...(parsed.pattern.skipTransitionGuard === true && { skipTransitionGuard: true }),
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
export function extractQueryHandler(
|
|
1707
|
-
call: CallExpression,
|
|
1708
|
-
sourceFile: SourceFile,
|
|
1709
|
-
): ExtractOutput<QueryHandlerPattern> {
|
|
1710
|
-
const parsed = parseHandlerCall(call, sourceFile, "queryHandler");
|
|
1711
|
-
if (parsed.kind === "error") return parsed;
|
|
1712
|
-
// QueryHandler has no skipTransitionGuard — the field is silently
|
|
1713
|
-
// ignored if the parser reads one (won't happen in practice because
|
|
1714
|
-
// queryHandlers don't carry that option).
|
|
1715
|
-
return ok({
|
|
1716
|
-
kind: "queryHandler",
|
|
1717
|
-
source: parsed.pattern.source,
|
|
1718
|
-
handlerName: parsed.pattern.handlerName,
|
|
1719
|
-
schemaSource: parsed.pattern.schemaSource,
|
|
1720
|
-
handlerBody: parsed.pattern.handlerBody,
|
|
1721
|
-
...(parsed.pattern.access !== undefined && { access: parsed.pattern.access }),
|
|
1722
|
-
...(parsed.pattern.rateLimit !== undefined && { rateLimit: parsed.pattern.rateLimit }),
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
export function extractJob(
|
|
1727
|
-
call: CallExpression,
|
|
1728
|
-
sourceFile: SourceFile,
|
|
1729
|
-
): ExtractOutput<JobPattern> {
|
|
1730
|
-
const args = call.getArguments();
|
|
1731
|
-
const first = args[0];
|
|
1732
|
-
if (!first) {
|
|
1733
|
-
return fail("job", sourceLocationFromNode(call, sourceFile), "expected at least one argument");
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
// Object-Form: r.job({ name, ...options, handler })
|
|
1737
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1738
|
-
if (obj && args.length === 1) {
|
|
1739
|
-
const nameInit = obj
|
|
1740
|
-
.getProperty("name")
|
|
1741
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1742
|
-
?.getInitializer()
|
|
1743
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1744
|
-
if (!nameInit) {
|
|
1745
|
-
return fail(
|
|
1746
|
-
"job",
|
|
1747
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1748
|
-
"object form requires a string-literal `name` property",
|
|
1749
|
-
);
|
|
1750
|
-
}
|
|
1751
|
-
const handlerInit = obj
|
|
1752
|
-
.getProperty("handler")
|
|
1753
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1754
|
-
?.getInitializer();
|
|
1755
|
-
if (!handlerInit) {
|
|
1756
|
-
return fail(
|
|
1757
|
-
"job",
|
|
1758
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1759
|
-
"object form requires a `handler` property",
|
|
1760
|
-
);
|
|
1761
|
-
}
|
|
1762
|
-
const fn = findFunctionLiteral(handlerInit);
|
|
1763
|
-
if (!fn) {
|
|
1764
|
-
return fail(
|
|
1765
|
-
"job",
|
|
1766
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1767
|
-
"handler must be an inline arrow function or function expression",
|
|
1768
|
-
);
|
|
1769
|
-
}
|
|
1770
|
-
// Read every property except `name` and `handler` as the options
|
|
1771
|
-
// bag — `handler` is a closure (not JSON-readable) and `name` lives
|
|
1772
|
-
// separately on the pattern. Walk properties one-by-one so handler
|
|
1773
|
-
// doesn't crash readDataLiteralNode.
|
|
1774
|
-
const optionsBag: Record<string, unknown> = {};
|
|
1775
|
-
for (const prop of obj.getProperties()) {
|
|
1776
|
-
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
1777
|
-
if (!propAssign) continue;
|
|
1778
|
-
const key = readPropertyKey(propAssign);
|
|
1779
|
-
if (key === "name" || key === "handler") continue;
|
|
1780
|
-
const init = propAssign.getInitializer();
|
|
1781
|
-
if (!init) continue;
|
|
1782
|
-
const value = readDataLiteralNode(init);
|
|
1783
|
-
if (value === undefined) {
|
|
1784
|
-
return fail(
|
|
1785
|
-
"job",
|
|
1786
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1787
|
-
`option "${key}" could not be read as a plain value`,
|
|
1788
|
-
);
|
|
1789
|
-
}
|
|
1790
|
-
optionsBag[key] = value;
|
|
1791
|
-
}
|
|
1792
|
-
return ok({
|
|
1793
|
-
kind: "job",
|
|
1794
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1795
|
-
jobName: nameInit.getLiteralValue(),
|
|
1796
|
-
options: optionsBag as Omit<JobDefinition, "name" | "handler">,
|
|
1797
|
-
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
1798
|
-
});
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
// Legacy positional: r.job(name, options, handler)
|
|
1802
|
-
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
1803
|
-
if (!nameArg) {
|
|
1804
|
-
return fail(
|
|
1805
|
-
"job",
|
|
1806
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1807
|
-
"first argument must be a string literal job name (or use the object form)",
|
|
1808
|
-
);
|
|
1809
|
-
}
|
|
1810
|
-
const optionsArg = args[1];
|
|
1811
|
-
if (!optionsArg) {
|
|
1812
|
-
return fail(
|
|
1813
|
-
"job",
|
|
1814
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1815
|
-
"expected an options object as second argument",
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
const options = readDataLiteralNode(optionsArg);
|
|
1819
|
-
if (!isPlainObject(options)) {
|
|
1820
|
-
return fail(
|
|
1821
|
-
"job",
|
|
1822
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1823
|
-
"options could not be read as a plain object",
|
|
1824
|
-
);
|
|
1825
|
-
}
|
|
1826
|
-
const handlerArg = args[2];
|
|
1827
|
-
if (!handlerArg) {
|
|
1828
|
-
return fail(
|
|
1829
|
-
"job",
|
|
1830
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1831
|
-
"expected a handler function as third argument",
|
|
1832
|
-
);
|
|
1833
|
-
}
|
|
1834
|
-
const fn = findFunctionLiteral(handlerArg);
|
|
1835
|
-
if (!fn) {
|
|
1836
|
-
return fail(
|
|
1837
|
-
"job",
|
|
1838
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1839
|
-
"third argument must be an inline arrow function or function expression",
|
|
1840
|
-
);
|
|
1841
|
-
}
|
|
1842
|
-
return ok({
|
|
1843
|
-
kind: "job",
|
|
1844
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1845
|
-
jobName: nameArg.getLiteralValue(),
|
|
1846
|
-
options: options as Omit<JobDefinition, "name" | "handler">,
|
|
1847
|
-
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
export function extractHttpRoute(
|
|
1852
|
-
call: CallExpression,
|
|
1853
|
-
sourceFile: SourceFile,
|
|
1854
|
-
): ExtractOutput<HttpRoutePattern> {
|
|
1855
|
-
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1856
|
-
if (!arg) {
|
|
1857
|
-
return fail(
|
|
1858
|
-
"httpRoute",
|
|
1859
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1860
|
-
"argument must be an inline HttpRouteDefinition object",
|
|
1861
|
-
);
|
|
1862
|
-
}
|
|
1863
|
-
const methodLiteral = arg
|
|
1864
|
-
.getProperty("method")
|
|
1865
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1866
|
-
?.getInitializer()
|
|
1867
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1868
|
-
if (!methodLiteral) {
|
|
1869
|
-
return fail(
|
|
1870
|
-
"httpRoute",
|
|
1871
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1872
|
-
"method must be a string literal",
|
|
1873
|
-
);
|
|
1874
|
-
}
|
|
1875
|
-
const methodValue = methodLiteral.getLiteralValue();
|
|
1876
|
-
if (!isHttpRouteMethod(methodValue)) {
|
|
1877
|
-
return fail(
|
|
1878
|
-
"httpRoute",
|
|
1879
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1880
|
-
`method must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS (got "${methodValue}")`,
|
|
1881
|
-
);
|
|
1882
|
-
}
|
|
1883
|
-
const pathLiteral = arg
|
|
1884
|
-
.getProperty("path")
|
|
1885
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1886
|
-
?.getInitializer()
|
|
1887
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1888
|
-
if (!pathLiteral) {
|
|
1889
|
-
return fail(
|
|
1890
|
-
"httpRoute",
|
|
1891
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1892
|
-
"path must be a string literal",
|
|
1893
|
-
);
|
|
1894
|
-
}
|
|
1895
|
-
const handlerInit = arg
|
|
1896
|
-
.getProperty("handler")
|
|
1897
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1898
|
-
?.getInitializer();
|
|
1899
|
-
if (!handlerInit) {
|
|
1900
|
-
return fail(
|
|
1901
|
-
"httpRoute",
|
|
1902
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1903
|
-
"missing `handler` property",
|
|
1904
|
-
);
|
|
1905
|
-
}
|
|
1906
|
-
const fn = findFunctionLiteral(handlerInit);
|
|
1907
|
-
if (!fn) {
|
|
1908
|
-
return fail(
|
|
1909
|
-
"httpRoute",
|
|
1910
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1911
|
-
"handler must be an inline arrow function or function expression",
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
const anonymous = readBooleanProperty(arg, "anonymous");
|
|
1915
|
-
return ok({
|
|
1916
|
-
kind: "httpRoute",
|
|
1917
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1918
|
-
method: methodValue,
|
|
1919
|
-
path: pathLiteral.getLiteralValue(),
|
|
1920
|
-
handlerBody: sourceLocationFromNode(fn, sourceFile),
|
|
1921
|
-
...(anonymous === true && { anonymous: true }),
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
export function extractDefineEvent(
|
|
1926
|
-
call: CallExpression,
|
|
1927
|
-
sourceFile: SourceFile,
|
|
1928
|
-
): ExtractOutput<DefineEventPattern> {
|
|
1929
|
-
const args = call.getArguments();
|
|
1930
|
-
const first = args[0];
|
|
1931
|
-
if (!first) {
|
|
1932
|
-
return fail(
|
|
1933
|
-
"defineEvent",
|
|
1934
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1935
|
-
"expected at least one argument",
|
|
1936
|
-
);
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
// Object-Form: r.defineEvent({ name, schema, version? })
|
|
1940
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
1941
|
-
if (obj && args.length === 1) {
|
|
1942
|
-
const nameInit = obj
|
|
1943
|
-
.getProperty("name")
|
|
1944
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1945
|
-
?.getInitializer()
|
|
1946
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
1947
|
-
if (!nameInit) {
|
|
1948
|
-
return fail(
|
|
1949
|
-
"defineEvent",
|
|
1950
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1951
|
-
"object form requires a string-literal `name` property",
|
|
1952
|
-
);
|
|
1953
|
-
}
|
|
1954
|
-
const schemaInit = obj
|
|
1955
|
-
.getProperty("schema")
|
|
1956
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1957
|
-
?.getInitializer();
|
|
1958
|
-
if (!schemaInit) {
|
|
1959
|
-
return fail(
|
|
1960
|
-
"defineEvent",
|
|
1961
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1962
|
-
"object form requires a `schema` property",
|
|
1963
|
-
);
|
|
1964
|
-
}
|
|
1965
|
-
let version: number | undefined;
|
|
1966
|
-
const versionInit = obj
|
|
1967
|
-
.getProperty("version")
|
|
1968
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
1969
|
-
?.getInitializer();
|
|
1970
|
-
if (versionInit) {
|
|
1971
|
-
const v = readDataLiteralNode(versionInit);
|
|
1972
|
-
if (typeof v === "number") version = v;
|
|
1973
|
-
}
|
|
1974
|
-
return ok({
|
|
1975
|
-
kind: "defineEvent",
|
|
1976
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
1977
|
-
eventName: nameInit.getLiteralValue(),
|
|
1978
|
-
schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
|
|
1979
|
-
...(version !== undefined && { version }),
|
|
1980
|
-
});
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
// Legacy positional: r.defineEvent(name, schema, options?)
|
|
1984
|
-
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
1985
|
-
if (!nameArg) {
|
|
1986
|
-
return fail(
|
|
1987
|
-
"defineEvent",
|
|
1988
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1989
|
-
"first argument must be a string literal event name (or use the object form)",
|
|
1990
|
-
);
|
|
1991
|
-
}
|
|
1992
|
-
const schemaArg = args[1];
|
|
1993
|
-
if (!schemaArg) {
|
|
1994
|
-
return fail(
|
|
1995
|
-
"defineEvent",
|
|
1996
|
-
sourceLocationFromNode(call, sourceFile),
|
|
1997
|
-
"expected a Zod schema as second argument",
|
|
1998
|
-
);
|
|
1999
|
-
}
|
|
2000
|
-
let version: number | undefined;
|
|
2001
|
-
const optionsArg = args[2];
|
|
2002
|
-
if (optionsArg) {
|
|
2003
|
-
const options = readDataLiteralNode(optionsArg);
|
|
2004
|
-
if (isPlainObject(options) && typeof options["version"] === "number") {
|
|
2005
|
-
version = options["version"];
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
return ok({
|
|
2009
|
-
kind: "defineEvent",
|
|
2010
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2011
|
-
eventName: nameArg.getLiteralValue(),
|
|
2012
|
-
schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
|
|
2013
|
-
...(version !== undefined && { version }),
|
|
2014
|
-
});
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
export function extractEventMigration(
|
|
2018
|
-
call: CallExpression,
|
|
2019
|
-
sourceFile: SourceFile,
|
|
2020
|
-
): ExtractOutput<EventMigrationPattern> {
|
|
2021
|
-
const args = call.getArguments();
|
|
2022
|
-
const first = args[0];
|
|
2023
|
-
if (!first) {
|
|
2024
|
-
return fail(
|
|
2025
|
-
"eventMigration",
|
|
2026
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2027
|
-
"expected at least one argument",
|
|
2028
|
-
);
|
|
2029
|
-
}
|
|
2030
|
-
|
|
2031
|
-
// Object-Form: r.eventMigration({ event, fromVersion, toVersion, transform })
|
|
2032
|
-
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2033
|
-
if (obj && args.length === 1) {
|
|
2034
|
-
const eventInit = obj
|
|
2035
|
-
.getProperty("event")
|
|
2036
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2037
|
-
?.getInitializer()
|
|
2038
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
2039
|
-
if (!eventInit) {
|
|
2040
|
-
return fail(
|
|
2041
|
-
"eventMigration",
|
|
2042
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2043
|
-
"object form requires a string-literal `event` property",
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
const fromInit = obj
|
|
2047
|
-
.getProperty("fromVersion")
|
|
2048
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2049
|
-
?.getInitializer();
|
|
2050
|
-
const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
|
|
2051
|
-
if (typeof fromVersion !== "number") {
|
|
2052
|
-
return fail(
|
|
2053
|
-
"eventMigration",
|
|
2054
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2055
|
-
"fromVersion must be a numeric literal",
|
|
2056
|
-
);
|
|
2057
|
-
}
|
|
2058
|
-
const toInit = obj
|
|
2059
|
-
.getProperty("toVersion")
|
|
2060
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2061
|
-
?.getInitializer();
|
|
2062
|
-
const toVersion = toInit ? readDataLiteralNode(toInit) : undefined;
|
|
2063
|
-
if (typeof toVersion !== "number") {
|
|
2064
|
-
return fail(
|
|
2065
|
-
"eventMigration",
|
|
2066
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2067
|
-
"toVersion must be a numeric literal",
|
|
2068
|
-
);
|
|
2069
|
-
}
|
|
2070
|
-
const transformInit = obj
|
|
2071
|
-
.getProperty("transform")
|
|
2072
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2073
|
-
?.getInitializer();
|
|
2074
|
-
if (!transformInit) {
|
|
2075
|
-
return fail(
|
|
2076
|
-
"eventMigration",
|
|
2077
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2078
|
-
"object form requires a `transform` property",
|
|
2079
|
-
);
|
|
2080
|
-
}
|
|
2081
|
-
const fn = findFunctionLiteral(transformInit);
|
|
2082
|
-
if (!fn) {
|
|
2083
|
-
return fail(
|
|
2084
|
-
"eventMigration",
|
|
2085
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2086
|
-
"transform must be an inline arrow function or function expression",
|
|
2087
|
-
);
|
|
2088
|
-
}
|
|
2089
|
-
return ok({
|
|
2090
|
-
kind: "eventMigration",
|
|
2091
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2092
|
-
eventName: eventInit.getLiteralValue(),
|
|
2093
|
-
fromVersion,
|
|
2094
|
-
toVersion,
|
|
2095
|
-
transformBody: sourceLocationFromNode(fn, sourceFile),
|
|
2096
|
-
});
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// Legacy positional: r.eventMigration(name, from, to, transform)
|
|
2100
|
-
const nameArg = first.asKind(SyntaxKind.StringLiteral);
|
|
2101
|
-
if (!nameArg) {
|
|
2102
|
-
return fail(
|
|
2103
|
-
"eventMigration",
|
|
2104
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2105
|
-
"first argument must be a string literal event name (or use the object form)",
|
|
2106
|
-
);
|
|
2107
|
-
}
|
|
2108
|
-
const fromArg = args[1];
|
|
2109
|
-
const fromVersion = fromArg ? readDataLiteralNode(fromArg) : undefined;
|
|
2110
|
-
if (typeof fromVersion !== "number") {
|
|
2111
|
-
return fail(
|
|
2112
|
-
"eventMigration",
|
|
2113
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2114
|
-
"fromVersion must be a numeric literal",
|
|
2115
|
-
);
|
|
2116
|
-
}
|
|
2117
|
-
const toArg = args[2];
|
|
2118
|
-
const toVersion = toArg ? readDataLiteralNode(toArg) : undefined;
|
|
2119
|
-
if (typeof toVersion !== "number") {
|
|
2120
|
-
return fail(
|
|
2121
|
-
"eventMigration",
|
|
2122
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2123
|
-
"toVersion must be a numeric literal",
|
|
2124
|
-
);
|
|
2125
|
-
}
|
|
2126
|
-
const transformArg = args[3];
|
|
2127
|
-
if (!transformArg) {
|
|
2128
|
-
return fail(
|
|
2129
|
-
"eventMigration",
|
|
2130
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2131
|
-
"expected a transform function as fourth argument",
|
|
2132
|
-
);
|
|
2133
|
-
}
|
|
2134
|
-
const fn = findFunctionLiteral(transformArg);
|
|
2135
|
-
if (!fn) {
|
|
2136
|
-
return fail(
|
|
2137
|
-
"eventMigration",
|
|
2138
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2139
|
-
"transform must be an inline arrow function or function expression",
|
|
2140
|
-
);
|
|
2141
|
-
}
|
|
2142
|
-
return ok({
|
|
2143
|
-
kind: "eventMigration",
|
|
2144
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2145
|
-
eventName: nameArg.getLiteralValue(),
|
|
2146
|
-
fromVersion,
|
|
2147
|
-
toVersion,
|
|
2148
|
-
transformBody: sourceLocationFromNode(fn, sourceFile),
|
|
2149
|
-
});
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
export function extractNotification(
|
|
2153
|
-
call: CallExpression,
|
|
2154
|
-
sourceFile: SourceFile,
|
|
2155
|
-
): ExtractOutput<NotificationPattern> {
|
|
2156
|
-
const args = call.getArguments();
|
|
2157
|
-
const first = args[0];
|
|
2158
|
-
if (!first) {
|
|
2159
|
-
return fail(
|
|
2160
|
-
"notification",
|
|
2161
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2162
|
-
"expected at least one argument",
|
|
2163
|
-
);
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
// Two argument shapes accepted:
|
|
2167
|
-
// (a) Legacy positional: r.notification("name", { trigger, recipient, data, templates? })
|
|
2168
|
-
// (b) Canonical Object-Form: r.notification({ name, trigger, recipient, data, templates? })
|
|
2169
|
-
// The body code below is shape-agnostic — `nameLiteral` carries the
|
|
2170
|
-
// notification's name, `defObj` is the object literal that holds the
|
|
2171
|
-
// trigger/recipient/data/templates.
|
|
2172
|
-
let nameLiteral: ReturnType<typeof first.asKind<SyntaxKind.StringLiteral>>;
|
|
2173
|
-
let defObj: ReturnType<typeof first.asKind<SyntaxKind.ObjectLiteralExpression>>;
|
|
2174
|
-
|
|
2175
|
-
const firstObj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2176
|
-
if (firstObj && args.length === 1) {
|
|
2177
|
-
// Object-Form
|
|
2178
|
-
nameLiteral = firstObj
|
|
2179
|
-
.getProperty("name")
|
|
2180
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2181
|
-
?.getInitializer()
|
|
2182
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
2183
|
-
if (!nameLiteral) {
|
|
2184
|
-
return fail(
|
|
2185
|
-
"notification",
|
|
2186
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2187
|
-
"object form requires a string-literal `name` property",
|
|
2188
|
-
);
|
|
2189
|
-
}
|
|
2190
|
-
defObj = firstObj;
|
|
2191
|
-
} else {
|
|
2192
|
-
// Legacy positional
|
|
2193
|
-
nameLiteral = first.asKind(SyntaxKind.StringLiteral);
|
|
2194
|
-
if (!nameLiteral) {
|
|
2195
|
-
return fail(
|
|
2196
|
-
"notification",
|
|
2197
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2198
|
-
"first argument must be a string literal notification name (or use the object form)",
|
|
2199
|
-
);
|
|
2200
|
-
}
|
|
2201
|
-
defObj = args[1]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2202
|
-
if (!defObj) {
|
|
2203
|
-
return fail(
|
|
2204
|
-
"notification",
|
|
2205
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2206
|
-
"second argument must be an inline definition object",
|
|
2207
|
-
);
|
|
2208
|
-
}
|
|
2209
|
-
}
|
|
2210
|
-
const nameArg = nameLiteral;
|
|
2211
|
-
const triggerObj = defObj
|
|
2212
|
-
.getProperty("trigger")
|
|
2213
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2214
|
-
?.getInitializer()
|
|
2215
|
-
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2216
|
-
if (!triggerObj) {
|
|
2217
|
-
return fail(
|
|
2218
|
-
"notification",
|
|
2219
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2220
|
-
"missing or non-object `trigger` property",
|
|
2221
|
-
);
|
|
2222
|
-
}
|
|
2223
|
-
const onInit = triggerObj
|
|
2224
|
-
.getProperty("on")
|
|
2225
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2226
|
-
?.getInitializer();
|
|
2227
|
-
const onName = onInit ? readNameOrRef(onInit) : undefined;
|
|
2228
|
-
if (!onName) {
|
|
2229
|
-
return fail(
|
|
2230
|
-
"notification",
|
|
2231
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2232
|
-
"trigger.on must be a string literal or inline { name } object",
|
|
2233
|
-
);
|
|
2234
|
-
}
|
|
2235
|
-
const recipientInit = defObj
|
|
2236
|
-
.getProperty("recipient")
|
|
2237
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2238
|
-
?.getInitializer();
|
|
2239
|
-
const recipientFn = recipientInit ? findFunctionLiteral(recipientInit) : undefined;
|
|
2240
|
-
if (!recipientFn) {
|
|
2241
|
-
return fail(
|
|
2242
|
-
"notification",
|
|
2243
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2244
|
-
"recipient must be an inline arrow function or function expression",
|
|
2245
|
-
);
|
|
2246
|
-
}
|
|
2247
|
-
const dataInit = defObj
|
|
2248
|
-
.getProperty("data")
|
|
2249
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2250
|
-
?.getInitializer();
|
|
2251
|
-
const dataFn = dataInit ? findFunctionLiteral(dataInit) : undefined;
|
|
2252
|
-
if (!dataFn) {
|
|
2253
|
-
return fail(
|
|
2254
|
-
"notification",
|
|
2255
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2256
|
-
"data must be an inline arrow function or function expression",
|
|
2257
|
-
);
|
|
2258
|
-
}
|
|
2259
|
-
let templates: Record<string, SourceLocation> | undefined;
|
|
2260
|
-
const templatesObj = defObj
|
|
2261
|
-
.getProperty("templates")
|
|
2262
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2263
|
-
?.getInitializer()
|
|
2264
|
-
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2265
|
-
if (templatesObj) {
|
|
2266
|
-
templates = {};
|
|
2267
|
-
for (const prop of templatesObj.getProperties()) {
|
|
2268
|
-
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
2269
|
-
if (!propAssign) continue;
|
|
2270
|
-
const init = propAssign.getInitializer();
|
|
2271
|
-
if (!init) continue;
|
|
2272
|
-
const tfn = findFunctionLiteral(init);
|
|
2273
|
-
if (!tfn) continue;
|
|
2274
|
-
templates[readPropertyKey(propAssign)] = sourceLocationFromNode(tfn, sourceFile);
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
return ok({
|
|
2278
|
-
kind: "notification",
|
|
2279
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2280
|
-
notificationName: nameArg.getLiteralValue(),
|
|
2281
|
-
trigger: { on: onName },
|
|
2282
|
-
recipientBody: sourceLocationFromNode(recipientFn, sourceFile),
|
|
2283
|
-
dataBody: sourceLocationFromNode(dataFn, sourceFile),
|
|
2284
|
-
...(templates !== undefined && { templates }),
|
|
2285
|
-
});
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
// Read an `apply: { eventType: fn }` map from a projection-definition object.
|
|
2289
|
-
function readApplyBodies(
|
|
2290
|
-
defObj: ReturnType<Node["asKind"]>,
|
|
2291
|
-
sourceFile: SourceFile,
|
|
2292
|
-
): Record<string, SourceLocation> | undefined {
|
|
2293
|
-
if (!defObj) return undefined;
|
|
2294
|
-
const obj = defObj.asKind?.(SyntaxKind.ObjectLiteralExpression);
|
|
2295
|
-
if (!obj) return undefined;
|
|
2296
|
-
const applyObj = obj
|
|
2297
|
-
.getProperty("apply")
|
|
2298
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2299
|
-
?.getInitializer()
|
|
2300
|
-
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2301
|
-
if (!applyObj) return undefined;
|
|
2302
|
-
const out: Record<string, SourceLocation> = {};
|
|
2303
|
-
for (const prop of applyObj.getProperties()) {
|
|
2304
|
-
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
2305
|
-
if (!propAssign) return undefined;
|
|
2306
|
-
const init = propAssign.getInitializer();
|
|
2307
|
-
if (!init) return undefined;
|
|
2308
|
-
const fn = findFunctionLiteral(init);
|
|
2309
|
-
if (!fn) return undefined;
|
|
2310
|
-
out[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
|
|
2311
|
-
}
|
|
2312
|
-
return out;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
export function extractProjection(
|
|
2316
|
-
call: CallExpression,
|
|
2317
|
-
sourceFile: SourceFile,
|
|
2318
|
-
): ExtractOutput<ProjectionPattern> {
|
|
2319
|
-
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2320
|
-
if (!arg) {
|
|
2321
|
-
return fail(
|
|
2322
|
-
"projection",
|
|
2323
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2324
|
-
"argument must be an inline ProjectionDefinition object",
|
|
2325
|
-
);
|
|
2326
|
-
}
|
|
2327
|
-
const nameLit = arg
|
|
2328
|
-
.getProperty("name")
|
|
2329
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2330
|
-
?.getInitializer()
|
|
2331
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
2332
|
-
if (!nameLit) {
|
|
2333
|
-
return fail(
|
|
2334
|
-
"projection",
|
|
2335
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2336
|
-
"name must be a string literal",
|
|
2337
|
-
);
|
|
2338
|
-
}
|
|
2339
|
-
const sourceInit = arg
|
|
2340
|
-
.getProperty("source")
|
|
2341
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2342
|
-
?.getInitializer();
|
|
2343
|
-
if (!sourceInit) {
|
|
2344
|
-
return fail(
|
|
2345
|
-
"projection",
|
|
2346
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2347
|
-
"missing `source` property",
|
|
2348
|
-
);
|
|
2349
|
-
}
|
|
2350
|
-
const sourceEntity = readNameOrRefOrList(sourceInit);
|
|
2351
|
-
if (!sourceEntity) {
|
|
2352
|
-
return fail(
|
|
2353
|
-
"projection",
|
|
2354
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2355
|
-
"source must be a string literal or array of string literals",
|
|
2356
|
-
);
|
|
2357
|
-
}
|
|
2358
|
-
const applyBodies = readApplyBodies(arg, sourceFile);
|
|
2359
|
-
if (!applyBodies) {
|
|
2360
|
-
return fail(
|
|
2361
|
-
"projection",
|
|
2362
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2363
|
-
"apply must be an inline object map of event-type → function",
|
|
2364
|
-
);
|
|
2365
|
-
}
|
|
2366
|
-
return ok({
|
|
2367
|
-
kind: "projection",
|
|
2368
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2369
|
-
name: nameLit.getLiteralValue(),
|
|
2370
|
-
sourceEntity,
|
|
2371
|
-
applyBodies,
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
export function extractMultiStreamProjection(
|
|
2376
|
-
call: CallExpression,
|
|
2377
|
-
sourceFile: SourceFile,
|
|
2378
|
-
): ExtractOutput<MultiStreamProjectionPattern> {
|
|
2379
|
-
const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2380
|
-
if (!arg) {
|
|
2381
|
-
return fail(
|
|
2382
|
-
"multiStreamProjection",
|
|
2383
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2384
|
-
"argument must be an inline MultiStreamProjectionDefinition object",
|
|
2385
|
-
);
|
|
2386
|
-
}
|
|
2387
|
-
const nameLit = arg
|
|
2388
|
-
.getProperty("name")
|
|
2389
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2390
|
-
?.getInitializer()
|
|
2391
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
2392
|
-
if (!nameLit) {
|
|
2393
|
-
return fail(
|
|
2394
|
-
"multiStreamProjection",
|
|
2395
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2396
|
-
"name must be a string literal",
|
|
2397
|
-
);
|
|
2398
|
-
}
|
|
2399
|
-
const applyBodies = readApplyBodies(arg, sourceFile);
|
|
2400
|
-
if (!applyBodies) {
|
|
2401
|
-
return fail(
|
|
2402
|
-
"multiStreamProjection",
|
|
2403
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2404
|
-
"apply must be an inline object map of event-type → function",
|
|
2405
|
-
);
|
|
2406
|
-
}
|
|
2407
|
-
const errorModeInit = arg
|
|
2408
|
-
.getProperty("errorMode")
|
|
2409
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2410
|
-
?.getInitializer();
|
|
2411
|
-
const errorMode = errorModeInit ? readDataLiteralNode(errorModeInit) : undefined;
|
|
2412
|
-
const runInLit = arg
|
|
2413
|
-
.getProperty("runIn")
|
|
2414
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2415
|
-
?.getInitializer()
|
|
2416
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
2417
|
-
const runIn = runInLit ? (runInLit.getLiteralValue() as RunIn) : undefined;
|
|
2418
|
-
const deliveryLit = arg
|
|
2419
|
-
.getProperty("delivery")
|
|
2420
|
-
?.asKind(SyntaxKind.PropertyAssignment)
|
|
2421
|
-
?.getInitializer()
|
|
2422
|
-
?.asKind(SyntaxKind.StringLiteral);
|
|
2423
|
-
const delivery = deliveryLit
|
|
2424
|
-
? (deliveryLit.getLiteralValue() as "shared" | "per-instance")
|
|
2425
|
-
: undefined;
|
|
2426
|
-
return ok({
|
|
2427
|
-
kind: "multiStreamProjection",
|
|
2428
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2429
|
-
name: nameLit.getLiteralValue(),
|
|
2430
|
-
applyBodies,
|
|
2431
|
-
...(isPlainObject(errorMode) && { errorMode: errorMode as MspErrorMode }),
|
|
2432
|
-
...(runIn !== undefined && { runIn }),
|
|
2433
|
-
...(delivery !== undefined && { delivery }),
|
|
2434
|
-
});
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
// Walk the screen definition and collect every closure-typed property
|
|
2438
|
-
// as a JSON-path → SourceLocation entry. The Designer renders forms for
|
|
2439
|
-
// the rest of the definition; the AI patcher knows it can replace the
|
|
2440
|
-
// span at the listed paths without touching surrounding fields.
|
|
2441
|
-
function collectScreenOpaqueProps(
|
|
2442
|
-
node: Node,
|
|
2443
|
-
path: string,
|
|
2444
|
-
sourceFile: SourceFile,
|
|
2445
|
-
out: Record<string, SourceLocation>,
|
|
2446
|
-
): void {
|
|
2447
|
-
const fn = findFunctionLiteral(node);
|
|
2448
|
-
if (fn) {
|
|
2449
|
-
out[path] = sourceLocationFromNode(fn, sourceFile);
|
|
2450
|
-
} else if (node.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
2451
|
-
for (const prop of node.getProperties()) {
|
|
2452
|
-
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
2453
|
-
if (!propAssign) continue;
|
|
2454
|
-
const init = propAssign.getInitializer();
|
|
2455
|
-
if (!init) continue;
|
|
2456
|
-
const key = readPropertyKey(propAssign);
|
|
2457
|
-
const childPath = path ? `${path}.${key}` : key;
|
|
2458
|
-
collectScreenOpaqueProps(init, childPath, sourceFile, out);
|
|
2459
|
-
}
|
|
2460
|
-
} else if (node.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
2461
|
-
node.getElements().forEach((el, idx) => {
|
|
2462
|
-
collectScreenOpaqueProps(el, `${path}.${idx}`, sourceFile, out);
|
|
2463
|
-
});
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
// Walk a screen-definition object and produce a JSON view, replacing any
|
|
2468
|
-
// closure-typed property with SCREEN_OPAQUE_MARKER. Identifiers and
|
|
2469
|
-
// other non-readable nodes also become the marker so the static tree
|
|
2470
|
-
// stays serialisable while pointing the Designer at opaqueProps for the
|
|
2471
|
-
// real source span.
|
|
2472
|
-
function readScreenStatic(node: Node): unknown {
|
|
2473
|
-
if (findFunctionLiteral(node)) return SCREEN_OPAQUE_MARKER;
|
|
2474
|
-
const obj = node.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2475
|
-
if (obj) {
|
|
2476
|
-
const out: Record<string, unknown> = {};
|
|
2477
|
-
for (const prop of obj.getProperties()) {
|
|
2478
|
-
const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
2479
|
-
if (!propAssign) continue;
|
|
2480
|
-
const init = propAssign.getInitializer();
|
|
2481
|
-
if (!init) continue;
|
|
2482
|
-
out[readPropertyKey(propAssign)] = readScreenStatic(init);
|
|
2483
|
-
}
|
|
2484
|
-
return out;
|
|
2485
|
-
}
|
|
2486
|
-
const arr = node.asKind(SyntaxKind.ArrayLiteralExpression);
|
|
2487
|
-
if (arr) {
|
|
2488
|
-
return arr.getElements().map(readScreenStatic);
|
|
2489
|
-
}
|
|
2490
|
-
const value = readDataLiteralNode(node);
|
|
2491
|
-
if (value === undefined) return SCREEN_OPAQUE_MARKER;
|
|
2492
|
-
return value;
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
export function extractScreen(
|
|
2496
|
-
call: CallExpression,
|
|
2497
|
-
sourceFile: SourceFile,
|
|
2498
|
-
): ExtractOutput<ScreenPattern> {
|
|
2499
|
-
const arg = call.getArguments()[0];
|
|
2500
|
-
if (!arg) {
|
|
2501
|
-
return fail(
|
|
2502
|
-
"screen",
|
|
2503
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2504
|
-
"expected a ScreenDefinition object as first argument",
|
|
2505
|
-
);
|
|
2506
|
-
}
|
|
2507
|
-
const obj = arg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
2508
|
-
if (!obj) {
|
|
2509
|
-
return fail(
|
|
2510
|
-
"screen",
|
|
2511
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2512
|
-
"argument must be an inline object literal",
|
|
2513
|
-
);
|
|
2514
|
-
}
|
|
2515
|
-
const opaqueProps: Record<string, SourceLocation> = {};
|
|
2516
|
-
collectScreenOpaqueProps(obj, "", sourceFile, opaqueProps);
|
|
2517
|
-
const definition = readScreenStatic(obj);
|
|
2518
|
-
if (!isPlainObject(definition)) {
|
|
2519
|
-
return fail(
|
|
2520
|
-
"screen",
|
|
2521
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2522
|
-
"definition could not be read structurally",
|
|
2523
|
-
);
|
|
2524
|
-
}
|
|
2525
|
-
return ok({
|
|
2526
|
-
kind: "screen",
|
|
2527
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2528
|
-
definition: definition as ScreenDefinition,
|
|
2529
|
-
opaqueProps: opaqueProps as OpaquePropMap,
|
|
2530
|
-
});
|
|
2531
|
-
}
|
|
2532
|
-
|
|
2533
|
-
// =============================================================================
|
|
2534
|
-
// Round 5 — opaque patterns (no static header beyond a name)
|
|
2535
|
-
// =============================================================================
|
|
2536
|
-
|
|
2537
|
-
export function extractExtendsRegistrar(
|
|
2538
|
-
call: CallExpression,
|
|
2539
|
-
sourceFile: SourceFile,
|
|
2540
|
-
): ExtractOutput<ExtendsRegistrarPattern> {
|
|
2541
|
-
const args = call.getArguments();
|
|
2542
|
-
const nameArg = args[0]?.asKind(SyntaxKind.StringLiteral);
|
|
2543
|
-
if (!nameArg) {
|
|
2544
|
-
return fail(
|
|
2545
|
-
"extendsRegistrar",
|
|
2546
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2547
|
-
"first argument must be a string literal extension name",
|
|
2548
|
-
);
|
|
2549
|
-
}
|
|
2550
|
-
const defArg = args[1];
|
|
2551
|
-
if (!defArg) {
|
|
2552
|
-
return fail(
|
|
2553
|
-
"extendsRegistrar",
|
|
2554
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2555
|
-
"expected a definition argument",
|
|
2556
|
-
);
|
|
2557
|
-
}
|
|
2558
|
-
return ok({
|
|
2559
|
-
kind: "extendsRegistrar",
|
|
2560
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2561
|
-
extensionName: nameArg.getLiteralValue(),
|
|
2562
|
-
defBody: sourceLocationFromNode(defArg, sourceFile),
|
|
2563
|
-
});
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
export function extractUsesApi(
|
|
2567
|
-
call: CallExpression,
|
|
2568
|
-
sourceFile: SourceFile,
|
|
2569
|
-
): ExtractOutput<UsesApiPattern> {
|
|
2570
|
-
const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
|
|
2571
|
-
if (!arg) {
|
|
2572
|
-
return fail(
|
|
2573
|
-
"usesApi",
|
|
2574
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2575
|
-
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
2576
|
-
);
|
|
2577
|
-
}
|
|
2578
|
-
return ok({
|
|
2579
|
-
kind: "usesApi",
|
|
2580
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2581
|
-
apiName: arg.getLiteralValue(),
|
|
2582
|
-
});
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
export function extractExposesApi(
|
|
2586
|
-
call: CallExpression,
|
|
2587
|
-
sourceFile: SourceFile,
|
|
2588
|
-
): ExtractOutput<ExposesApiPattern> {
|
|
2589
|
-
const arg = call.getArguments()[0]?.asKind(SyntaxKind.StringLiteral);
|
|
2590
|
-
if (!arg) {
|
|
2591
|
-
return fail(
|
|
2592
|
-
"exposesApi",
|
|
2593
|
-
sourceLocationFromNode(call, sourceFile),
|
|
2594
|
-
'expected a single string-literal API name (e.g. "sessions.revokeAllForUser")',
|
|
2595
|
-
);
|
|
2596
|
-
}
|
|
2597
|
-
return ok({
|
|
2598
|
-
kind: "exposesApi",
|
|
2599
|
-
source: sourceLocationFromNode(call, sourceFile),
|
|
2600
|
-
apiName: arg.getLiteralValue(),
|
|
2601
|
-
});
|
|
2602
|
-
}
|