@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.
Files changed (166) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/package.json +124 -39
  3. package/src/__tests__/full-stack.integration.ts +2 -2
  4. package/src/api/auth-routes.ts +5 -5
  5. package/src/api/jwt.ts +2 -2
  6. package/src/api/route-registrars.ts +1 -1
  7. package/src/api/routes.ts +3 -3
  8. package/src/api/server.ts +6 -7
  9. package/src/compliance/profiles.ts +8 -8
  10. package/src/db/assert-exists-in.ts +2 -2
  11. package/src/db/cursor.ts +3 -3
  12. package/src/db/event-store-executor.ts +19 -13
  13. package/src/db/located-timestamp.ts +1 -1
  14. package/src/db/money.ts +12 -2
  15. package/src/db/pg-error.ts +1 -1
  16. package/src/db/row-helpers.ts +1 -1
  17. package/src/db/table-builder.ts +3 -5
  18. package/src/db/tenant-db.ts +9 -9
  19. package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
  20. package/src/engine/__tests__/build-target.test.ts +135 -0
  21. package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
  22. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  23. package/src/engine/__tests__/event-helpers.test.ts +4 -4
  24. package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
  25. package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
  26. package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
  27. package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
  28. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
  29. package/src/engine/__tests__/raw-table.test.ts +2 -2
  30. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
  31. package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
  32. package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
  33. package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
  34. package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
  35. package/src/engine/__tests__/steps-read.test.ts +142 -0
  36. package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
  37. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
  38. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
  39. package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
  40. package/src/engine/__tests__/steps-workflow.test.ts +198 -0
  41. package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
  42. package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
  43. package/src/engine/boot-validator/api-ext.ts +77 -0
  44. package/src/engine/boot-validator/config-deps.ts +163 -0
  45. package/src/engine/boot-validator/entity-handler.ts +466 -0
  46. package/src/engine/boot-validator/index.ts +159 -0
  47. package/src/engine/boot-validator/ownership.ts +198 -0
  48. package/src/engine/boot-validator/pii-retention.ts +155 -0
  49. package/src/engine/boot-validator/screens-nav.ts +624 -0
  50. package/src/engine/boot-validator.ts +1 -1804
  51. package/src/engine/build-app-schema.ts +1 -1
  52. package/src/engine/build-target.ts +99 -0
  53. package/src/engine/codemod/index.ts +15 -0
  54. package/src/engine/codemod/pipeline-codemod.ts +641 -0
  55. package/src/engine/config-helpers.ts +9 -19
  56. package/src/engine/constants.ts +1 -1
  57. package/src/engine/define-feature.ts +88 -9
  58. package/src/engine/define-handler.ts +89 -3
  59. package/src/engine/define-roles.ts +2 -2
  60. package/src/engine/define-step.ts +28 -0
  61. package/src/engine/define-workflow.ts +110 -0
  62. package/src/engine/entity-handlers.ts +10 -9
  63. package/src/engine/event-helpers.ts +4 -4
  64. package/src/engine/factories.ts +12 -12
  65. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
  66. package/src/engine/feature-ast/extractors/index.ts +74 -0
  67. package/src/engine/feature-ast/extractors/round1.ts +110 -0
  68. package/src/engine/feature-ast/extractors/round2.ts +253 -0
  69. package/src/engine/feature-ast/extractors/round3.ts +471 -0
  70. package/src/engine/feature-ast/extractors/round4.ts +1365 -0
  71. package/src/engine/feature-ast/extractors/round5.ts +72 -0
  72. package/src/engine/feature-ast/extractors/round6.ts +66 -0
  73. package/src/engine/feature-ast/extractors/shared.ts +177 -0
  74. package/src/engine/feature-ast/parse.ts +7 -0
  75. package/src/engine/feature-ast/patch.ts +9 -1
  76. package/src/engine/feature-ast/patcher.ts +10 -3
  77. package/src/engine/feature-ast/patterns.ts +49 -1
  78. package/src/engine/feature-ast/render.ts +17 -1
  79. package/src/engine/index.ts +44 -2
  80. package/src/engine/pattern-library/__tests__/library.test.ts +6 -0
  81. package/src/engine/pattern-library/library.ts +42 -2
  82. package/src/engine/pipeline.ts +88 -0
  83. package/src/engine/projection-helpers.ts +1 -1
  84. package/src/engine/read-claim.ts +1 -1
  85. package/src/engine/registry.ts +30 -2
  86. package/src/engine/resolve-config-or-param.ts +4 -0
  87. package/src/engine/run-pipeline.ts +162 -0
  88. package/src/engine/schema-builder.ts +2 -4
  89. package/src/engine/state-machine.ts +1 -1
  90. package/src/engine/steps/_drizzle-boundary.ts +19 -0
  91. package/src/engine/steps/_duration-utils.ts +33 -0
  92. package/src/engine/steps/_no-return-guard.ts +21 -0
  93. package/src/engine/steps/_resolver-utils.ts +42 -0
  94. package/src/engine/steps/_step-dispatch-constants.ts +38 -0
  95. package/src/engine/steps/aggregate-append-event.ts +56 -0
  96. package/src/engine/steps/aggregate-create.ts +56 -0
  97. package/src/engine/steps/aggregate-update.ts +68 -0
  98. package/src/engine/steps/branch.ts +84 -0
  99. package/src/engine/steps/call-feature.ts +49 -0
  100. package/src/engine/steps/compute.ts +41 -0
  101. package/src/engine/steps/for-each.ts +111 -0
  102. package/src/engine/steps/mail-send.ts +44 -0
  103. package/src/engine/steps/read-find-many.ts +51 -0
  104. package/src/engine/steps/read-find-one.ts +58 -0
  105. package/src/engine/steps/retry.ts +87 -0
  106. package/src/engine/steps/return.ts +34 -0
  107. package/src/engine/steps/unsafe-projection-delete.ts +46 -0
  108. package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
  109. package/src/engine/steps/wait-for-event.ts +71 -0
  110. package/src/engine/steps/wait.ts +69 -0
  111. package/src/engine/steps/webhook-send.ts +71 -0
  112. package/src/engine/system-user.ts +1 -1
  113. package/src/engine/types/feature.ts +93 -1
  114. package/src/engine/types/handlers.ts +18 -10
  115. package/src/engine/types/index.ts +11 -1
  116. package/src/engine/types/step.ts +334 -0
  117. package/src/engine/types/target-ref.ts +21 -0
  118. package/src/engine/types/tree-node.ts +132 -0
  119. package/src/engine/types/workspace.ts +7 -0
  120. package/src/engine/validate-projection-allowlist.ts +161 -0
  121. package/src/event-store/snapshot.ts +1 -1
  122. package/src/event-store/upcaster-dead-letter.ts +1 -1
  123. package/src/event-store/upcaster.ts +1 -1
  124. package/src/files/file-routes.ts +1 -1
  125. package/src/files/types.ts +2 -2
  126. package/src/jobs/job-runner.ts +10 -10
  127. package/src/lifecycle/lifecycle.ts +0 -3
  128. package/src/logging/index.ts +1 -0
  129. package/src/logging/pino-logger.ts +11 -7
  130. package/src/logging/utils.ts +24 -0
  131. package/src/observability/prometheus-meter.ts +7 -5
  132. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  133. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  134. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  135. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  136. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  137. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  138. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  139. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  140. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  141. package/src/pipeline/append-event-core.ts +22 -6
  142. package/src/pipeline/dispatcher-utils.ts +188 -0
  143. package/src/pipeline/dispatcher.ts +63 -283
  144. package/src/pipeline/distributed-lock.ts +1 -1
  145. package/src/pipeline/entity-cache.ts +2 -2
  146. package/src/pipeline/event-consumer-state.ts +0 -13
  147. package/src/pipeline/event-dispatcher.ts +4 -4
  148. package/src/pipeline/index.ts +0 -2
  149. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  150. package/src/pipeline/msp-rebuild.ts +5 -5
  151. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  152. package/src/pipeline/projection-rebuild.ts +2 -2
  153. package/src/pipeline/projection-state.ts +0 -12
  154. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  155. package/src/rate-limit/resolver.ts +1 -1
  156. package/src/search/in-memory-adapter.ts +1 -1
  157. package/src/search/meilisearch-adapter.ts +3 -3
  158. package/src/search/types.ts +1 -1
  159. package/src/secrets/leak-guard.ts +2 -2
  160. package/src/stack/request-helper.ts +9 -5
  161. package/src/stack/test-stack.ts +1 -1
  162. package/src/testing/handler-context.ts +4 -4
  163. package/src/testing/http-cookies.ts +1 -1
  164. package/src/time/tz-context.ts +1 -2
  165. package/src/ui-types/index.ts +4 -0
  166. package/src/engine/feature-ast/extractors.ts +0 -2602
@@ -0,0 +1,471 @@
1
+ import type { CallExpression, SourceFile } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type { ConfigKeyDefinition, ConfigKeyType, TranslationKeys } from "../../types/config";
4
+ import type { MetricOptions, SecretOptions } from "../../types/feature";
5
+ import type { ClaimKeyType } from "../../types/handlers";
6
+ import type { ParseError } from "../parse";
7
+ import type {
8
+ ClaimKeyPattern,
9
+ ConfigPattern,
10
+ MetricPattern,
11
+ ReferenceDataPattern,
12
+ SecretPattern,
13
+ TranslationsPattern,
14
+ UseExtensionPattern,
15
+ } from "../patterns";
16
+ import { sourceLocationFromNode } from "../source-location";
17
+ import {
18
+ type ExtractOutput,
19
+ fail,
20
+ isPlainObject,
21
+ ok,
22
+ readDataLiteralNode,
23
+ readNameOrRef,
24
+ } from "./shared";
25
+
26
+ export type NamedOptionsResult =
27
+ | { readonly kind: "ok"; readonly name: string; readonly options: Record<string, unknown> }
28
+ | { readonly kind: "error"; readonly error: ParseError };
29
+
30
+ export function readNamedOptions(
31
+ call: CallExpression,
32
+ sourceFile: SourceFile,
33
+ methodName: string,
34
+ ): NamedOptionsResult {
35
+ const args = call.getArguments();
36
+ const first = args[0];
37
+ if (!first) {
38
+ return fail(
39
+ methodName,
40
+ sourceLocationFromNode(call, sourceFile),
41
+ "expected at least one argument",
42
+ );
43
+ }
44
+
45
+ const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
46
+ if (obj && args.length === 1) {
47
+ const nameInit = obj
48
+ .getProperty("name")
49
+ ?.asKind(SyntaxKind.PropertyAssignment)
50
+ ?.getInitializer()
51
+ ?.asKind(SyntaxKind.StringLiteral);
52
+ if (!nameInit) {
53
+ return fail(
54
+ methodName,
55
+ sourceLocationFromNode(call, sourceFile),
56
+ "object form requires a string-literal `name` property",
57
+ );
58
+ }
59
+ const data = readDataLiteralNode(obj);
60
+ if (!isPlainObject(data)) {
61
+ return fail(
62
+ methodName,
63
+ sourceLocationFromNode(call, sourceFile),
64
+ "argument could not be read as a plain object",
65
+ );
66
+ }
67
+ const { name: _name, ...optionsWithoutName } = data;
68
+ return { kind: "ok", name: nameInit.getLiteralValue(), options: optionsWithoutName };
69
+ }
70
+
71
+ const nameLiteral = first.asKind(SyntaxKind.StringLiteral);
72
+ if (!nameLiteral) {
73
+ return fail(
74
+ methodName,
75
+ sourceLocationFromNode(call, sourceFile),
76
+ "first argument must be a string literal name (or use the object form)",
77
+ );
78
+ }
79
+ const optionsArg = args[1];
80
+ if (!optionsArg) {
81
+ return fail(
82
+ methodName,
83
+ sourceLocationFromNode(call, sourceFile),
84
+ "expected an options object as second argument",
85
+ );
86
+ }
87
+ const options = readDataLiteralNode(optionsArg);
88
+ if (!isPlainObject(options)) {
89
+ return fail(
90
+ methodName,
91
+ sourceLocationFromNode(call, sourceFile),
92
+ "options could not be read as a plain object",
93
+ );
94
+ }
95
+ return { kind: "ok", name: nameLiteral.getLiteralValue(), options };
96
+ }
97
+
98
+ export function extractConfig(
99
+ call: CallExpression,
100
+ sourceFile: SourceFile,
101
+ ): ExtractOutput<ConfigPattern> {
102
+ const arg = call.getArguments()[0];
103
+ if (!arg) {
104
+ return fail(
105
+ "config",
106
+ sourceLocationFromNode(call, sourceFile),
107
+ "expected `{ keys: { ... } }` as first argument",
108
+ );
109
+ }
110
+ const obj = readDataLiteralNode(arg);
111
+ if (!isPlainObject(obj)) {
112
+ return fail(
113
+ "config",
114
+ sourceLocationFromNode(call, sourceFile),
115
+ "argument could not be read as a plain object",
116
+ );
117
+ }
118
+ const keys = obj["keys"];
119
+ if (!isPlainObject(keys)) {
120
+ return fail(
121
+ "config",
122
+ sourceLocationFromNode(call, sourceFile),
123
+ "missing or non-object `keys` property",
124
+ );
125
+ }
126
+ return ok({
127
+ kind: "config",
128
+ source: sourceLocationFromNode(call, sourceFile),
129
+ keys: keys as Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>,
130
+ });
131
+ }
132
+
133
+ export function extractTranslations(
134
+ call: CallExpression,
135
+ sourceFile: SourceFile,
136
+ ): ExtractOutput<TranslationsPattern> {
137
+ const arg = call.getArguments()[0];
138
+ if (!arg) {
139
+ return fail(
140
+ "translations",
141
+ sourceLocationFromNode(call, sourceFile),
142
+ "expected `{ keys: { ... } }` as first argument",
143
+ );
144
+ }
145
+ const obj = readDataLiteralNode(arg);
146
+ if (!isPlainObject(obj)) {
147
+ return fail(
148
+ "translations",
149
+ sourceLocationFromNode(call, sourceFile),
150
+ "argument could not be read as a plain object",
151
+ );
152
+ }
153
+ const keys = obj["keys"];
154
+ if (!isPlainObject(keys)) {
155
+ return fail(
156
+ "translations",
157
+ sourceLocationFromNode(call, sourceFile),
158
+ "missing or non-object `keys` property",
159
+ );
160
+ }
161
+ return ok({
162
+ kind: "translations",
163
+ source: sourceLocationFromNode(call, sourceFile),
164
+ keys: keys as TranslationKeys,
165
+ });
166
+ }
167
+
168
+ export function extractMetric(
169
+ call: CallExpression,
170
+ sourceFile: SourceFile,
171
+ ): ExtractOutput<MetricPattern> {
172
+ const parsed = readNamedOptions(call, sourceFile, "metric");
173
+ if (parsed.kind === "error") return parsed;
174
+ return ok({
175
+ kind: "metric",
176
+ source: sourceLocationFromNode(call, sourceFile),
177
+ shortName: parsed.name,
178
+ options: parsed.options as MetricOptions,
179
+ });
180
+ }
181
+
182
+ export function extractSecret(
183
+ call: CallExpression,
184
+ sourceFile: SourceFile,
185
+ ): ExtractOutput<SecretPattern> {
186
+ const parsed = readNamedOptions(call, sourceFile, "secret");
187
+ if (parsed.kind === "error") return parsed;
188
+ return ok({
189
+ kind: "secret",
190
+ source: sourceLocationFromNode(call, sourceFile),
191
+ shortName: parsed.name,
192
+ options: parsed.options as SecretOptions,
193
+ });
194
+ }
195
+
196
+ export function isClaimKeyType(value: unknown): value is ClaimKeyType {
197
+ return (
198
+ value === "string" ||
199
+ value === "number" ||
200
+ value === "boolean" ||
201
+ value === "string[]" ||
202
+ value === "object"
203
+ );
204
+ }
205
+
206
+ export function extractClaimKey(
207
+ call: CallExpression,
208
+ sourceFile: SourceFile,
209
+ ): ExtractOutput<ClaimKeyPattern> {
210
+ const parsed = readNamedOptions(call, sourceFile, "claimKey");
211
+ if (parsed.kind === "error") return parsed;
212
+ const claimType = parsed.options["type"];
213
+ if (!isClaimKeyType(claimType)) {
214
+ return fail(
215
+ "claimKey",
216
+ sourceLocationFromNode(call, sourceFile),
217
+ 'type must be one of "string" | "number" | "boolean" | "string[]" | "object"',
218
+ );
219
+ }
220
+ return ok({
221
+ kind: "claimKey",
222
+ source: sourceLocationFromNode(call, sourceFile),
223
+ shortName: parsed.name,
224
+ claimType,
225
+ });
226
+ }
227
+
228
+ export function extractReferenceData(
229
+ call: CallExpression,
230
+ sourceFile: SourceFile,
231
+ ): ExtractOutput<ReferenceDataPattern> {
232
+ const args = call.getArguments();
233
+ const first = args[0];
234
+ if (!first) {
235
+ return fail(
236
+ "referenceData",
237
+ sourceLocationFromNode(call, sourceFile),
238
+ "expected at least one argument",
239
+ );
240
+ }
241
+
242
+ const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
243
+ if (obj && args.length === 1) {
244
+ const entityInit = obj
245
+ .getProperty("entity")
246
+ ?.asKind(SyntaxKind.PropertyAssignment)
247
+ ?.getInitializer();
248
+ if (!entityInit) {
249
+ return fail(
250
+ "referenceData",
251
+ sourceLocationFromNode(call, sourceFile),
252
+ "object form requires an `entity` property",
253
+ );
254
+ }
255
+ const entityName = readNameOrRef(entityInit);
256
+ if (!entityName) {
257
+ return fail(
258
+ "referenceData",
259
+ sourceLocationFromNode(call, sourceFile),
260
+ '`entity` must be a string literal or `{ name: "..." }` ref',
261
+ );
262
+ }
263
+ const dataInit = obj
264
+ .getProperty("data")
265
+ ?.asKind(SyntaxKind.PropertyAssignment)
266
+ ?.getInitializer();
267
+ if (!dataInit) {
268
+ return fail(
269
+ "referenceData",
270
+ sourceLocationFromNode(call, sourceFile),
271
+ "object form requires a `data` property",
272
+ );
273
+ }
274
+ const data = readDataLiteralNode(dataInit);
275
+ if (!Array.isArray(data) || !data.every(isPlainObject)) {
276
+ return fail(
277
+ "referenceData",
278
+ sourceLocationFromNode(call, sourceFile),
279
+ "data must be an array of plain objects",
280
+ );
281
+ }
282
+ let upsertKey: string | undefined;
283
+ const upsertKeyInit = obj
284
+ .getProperty("upsertKey")
285
+ ?.asKind(SyntaxKind.PropertyAssignment)
286
+ ?.getInitializer()
287
+ ?.asKind(SyntaxKind.StringLiteral);
288
+ if (upsertKeyInit) {
289
+ upsertKey = upsertKeyInit.getLiteralValue();
290
+ }
291
+ return ok({
292
+ kind: "referenceData",
293
+ source: sourceLocationFromNode(call, sourceFile),
294
+ entityName,
295
+ data: data as readonly Record<string, unknown>[],
296
+ ...(upsertKey !== undefined && { upsertKey }),
297
+ });
298
+ }
299
+
300
+ const entityName = readNameOrRef(first);
301
+ if (!entityName) {
302
+ return fail(
303
+ "referenceData",
304
+ sourceLocationFromNode(call, sourceFile),
305
+ 'first argument must be a string literal or an inline { name: "..." } object (or use the object form)',
306
+ );
307
+ }
308
+ const dataArg = args[1];
309
+ if (!dataArg) {
310
+ return fail(
311
+ "referenceData",
312
+ sourceLocationFromNode(call, sourceFile),
313
+ "expected a data array as second argument",
314
+ );
315
+ }
316
+ const data = readDataLiteralNode(dataArg);
317
+ if (!Array.isArray(data) || !data.every(isPlainObject)) {
318
+ return fail(
319
+ "referenceData",
320
+ sourceLocationFromNode(call, sourceFile),
321
+ "data must be an array of plain objects",
322
+ );
323
+ }
324
+ let upsertKey: string | undefined;
325
+ const optionsArg = args[2];
326
+ if (optionsArg) {
327
+ const options = readDataLiteralNode(optionsArg);
328
+ if (!isPlainObject(options)) {
329
+ return fail(
330
+ "referenceData",
331
+ sourceLocationFromNode(call, sourceFile),
332
+ "options could not be read as a plain object",
333
+ );
334
+ }
335
+ if (options["upsertKey"] !== undefined) {
336
+ if (typeof options["upsertKey"] !== "string") {
337
+ return fail(
338
+ "referenceData",
339
+ sourceLocationFromNode(call, sourceFile),
340
+ "upsertKey must be a string when provided",
341
+ );
342
+ }
343
+ upsertKey = options["upsertKey"];
344
+ }
345
+ }
346
+ return ok({
347
+ kind: "referenceData",
348
+ source: sourceLocationFromNode(call, sourceFile),
349
+ entityName,
350
+ data: data as readonly Record<string, unknown>[],
351
+ ...(upsertKey !== undefined && { upsertKey }),
352
+ });
353
+ }
354
+
355
+ export function extractUseExtension(
356
+ call: CallExpression,
357
+ sourceFile: SourceFile,
358
+ ): ExtractOutput<UseExtensionPattern> {
359
+ const args = call.getArguments();
360
+ const first = args[0];
361
+ if (!first) {
362
+ return fail(
363
+ "useExtension",
364
+ sourceLocationFromNode(call, sourceFile),
365
+ "expected at least one argument",
366
+ );
367
+ }
368
+
369
+ const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
370
+ if (obj && args.length === 1) {
371
+ const nameInit = obj
372
+ .getProperty("name")
373
+ ?.asKind(SyntaxKind.PropertyAssignment)
374
+ ?.getInitializer()
375
+ ?.asKind(SyntaxKind.StringLiteral);
376
+ if (!nameInit) {
377
+ return fail(
378
+ "useExtension",
379
+ sourceLocationFromNode(call, sourceFile),
380
+ "object form requires a string-literal `name` property",
381
+ );
382
+ }
383
+ const entityInit = obj
384
+ .getProperty("entity")
385
+ ?.asKind(SyntaxKind.PropertyAssignment)
386
+ ?.getInitializer();
387
+ if (!entityInit) {
388
+ return fail(
389
+ "useExtension",
390
+ sourceLocationFromNode(call, sourceFile),
391
+ "object form requires an `entity` property",
392
+ );
393
+ }
394
+ const entityName = readNameOrRef(entityInit);
395
+ if (!entityName) {
396
+ return fail(
397
+ "useExtension",
398
+ sourceLocationFromNode(call, sourceFile),
399
+ '`entity` must be a string literal or `{ name: "..." }` ref',
400
+ );
401
+ }
402
+ let options: Readonly<Record<string, unknown>> | undefined;
403
+ const optionsInit = obj
404
+ .getProperty("options")
405
+ ?.asKind(SyntaxKind.PropertyAssignment)
406
+ ?.getInitializer();
407
+ if (optionsInit) {
408
+ const parsed = readDataLiteralNode(optionsInit);
409
+ if (!isPlainObject(parsed)) {
410
+ return fail(
411
+ "useExtension",
412
+ sourceLocationFromNode(call, sourceFile),
413
+ "options could not be read as a plain object",
414
+ );
415
+ }
416
+ options = parsed;
417
+ }
418
+ return ok({
419
+ kind: "useExtension",
420
+ source: sourceLocationFromNode(call, sourceFile),
421
+ extensionName: nameInit.getLiteralValue(),
422
+ entityName,
423
+ ...(options !== undefined && { options }),
424
+ });
425
+ }
426
+
427
+ const nameArg = first.asKind(SyntaxKind.StringLiteral);
428
+ if (!nameArg) {
429
+ return fail(
430
+ "useExtension",
431
+ sourceLocationFromNode(call, sourceFile),
432
+ "first argument must be a string literal extension name (or use the object form)",
433
+ );
434
+ }
435
+ const entityRefArg = args[1];
436
+ if (!entityRefArg) {
437
+ return fail(
438
+ "useExtension",
439
+ sourceLocationFromNode(call, sourceFile),
440
+ "expected an entity reference as second argument",
441
+ );
442
+ }
443
+ const entityName = readNameOrRef(entityRefArg);
444
+ if (!entityName) {
445
+ return fail(
446
+ "useExtension",
447
+ sourceLocationFromNode(call, sourceFile),
448
+ 'second argument must be a string literal or an inline { name: "..." } object',
449
+ );
450
+ }
451
+ const optionsArg = args[2];
452
+ let options: Readonly<Record<string, unknown>> | undefined;
453
+ if (optionsArg) {
454
+ const parsed = readDataLiteralNode(optionsArg);
455
+ if (!isPlainObject(parsed)) {
456
+ return fail(
457
+ "useExtension",
458
+ sourceLocationFromNode(call, sourceFile),
459
+ "options could not be read as a plain object",
460
+ );
461
+ }
462
+ options = parsed;
463
+ }
464
+ return ok({
465
+ kind: "useExtension",
466
+ source: sourceLocationFromNode(call, sourceFile),
467
+ extensionName: nameArg.getLiteralValue(),
468
+ entityName,
469
+ ...(options !== undefined && { options }),
470
+ });
471
+ }