@cosmicdrift/kumiko-framework 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +52 -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 +45 -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 +92 -1
  114. package/src/engine/types/handlers.ts +18 -10
  115. package/src/engine/types/identifiers.ts +1 -0
  116. package/src/engine/types/index.ts +12 -1
  117. package/src/engine/types/step.ts +334 -0
  118. package/src/engine/types/target-ref.ts +21 -0
  119. package/src/engine/types/tree-node.ts +130 -0
  120. package/src/engine/types/workspace.ts +7 -0
  121. package/src/engine/validate-projection-allowlist.ts +161 -0
  122. package/src/event-store/snapshot.ts +1 -1
  123. package/src/event-store/upcaster-dead-letter.ts +1 -1
  124. package/src/event-store/upcaster.ts +1 -1
  125. package/src/files/file-routes.ts +1 -1
  126. package/src/files/types.ts +2 -2
  127. package/src/jobs/job-runner.ts +10 -10
  128. package/src/lifecycle/lifecycle.ts +0 -3
  129. package/src/logging/index.ts +1 -0
  130. package/src/logging/pino-logger.ts +11 -7
  131. package/src/logging/utils.ts +24 -0
  132. package/src/observability/prometheus-meter.ts +7 -5
  133. package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
  134. package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
  135. package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
  136. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
  137. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
  138. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
  139. package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
  140. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
  141. package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
  142. package/src/pipeline/append-event-core.ts +22 -6
  143. package/src/pipeline/dispatcher-utils.ts +188 -0
  144. package/src/pipeline/dispatcher.ts +63 -283
  145. package/src/pipeline/distributed-lock.ts +1 -1
  146. package/src/pipeline/entity-cache.ts +2 -2
  147. package/src/pipeline/event-consumer-state.ts +0 -13
  148. package/src/pipeline/event-dispatcher.ts +4 -4
  149. package/src/pipeline/index.ts +0 -2
  150. package/src/pipeline/lifecycle-pipeline.ts +6 -12
  151. package/src/pipeline/msp-rebuild.ts +5 -5
  152. package/src/pipeline/multi-stream-apply-context.ts +6 -7
  153. package/src/pipeline/projection-rebuild.ts +2 -2
  154. package/src/pipeline/projection-state.ts +0 -12
  155. package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
  156. package/src/rate-limit/resolver.ts +1 -1
  157. package/src/search/in-memory-adapter.ts +1 -1
  158. package/src/search/meilisearch-adapter.ts +3 -3
  159. package/src/search/types.ts +1 -1
  160. package/src/secrets/leak-guard.ts +2 -2
  161. package/src/stack/request-helper.ts +9 -5
  162. package/src/stack/test-stack.ts +1 -1
  163. package/src/testing/handler-context.ts +4 -4
  164. package/src/testing/http-cookies.ts +1 -1
  165. package/src/time/tz-context.ts +1 -2
  166. package/src/ui-types/index.ts +4 -0
  167. package/src/engine/feature-ast/extractors.ts +0 -2602
@@ -0,0 +1,184 @@
1
+ // AST round-trip tests for the Visual-Tree patterns. Each test feeds
2
+ // a minimal inline-feature into parseSourceFile and asserts the
3
+ // extracted Pattern shape + the editability classification.
4
+ //
5
+ // Why split from visual-tree-patterns.test.ts (engine-level): that suite
6
+ // covers the registrar+registry runtime path. This suite covers the
7
+ // AST extractor + the Designer/AI consumers (renderPattern,
8
+ // getEditability, PATTERN_LIBRARY). Both halves must agree on shape.
9
+
10
+ import { Project } from "ts-morph";
11
+ import { describe, expect, test } from "vitest";
12
+ import { parseSourceFile } from "../parse";
13
+ import { getEditability } from "../patterns";
14
+ import { renderPattern } from "../render";
15
+
16
+ function parseInline(source: string) {
17
+ const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: true });
18
+ const sourceFile = project.createSourceFile("feature.ts", source);
19
+ return parseSourceFile(sourceFile);
20
+ }
21
+
22
+ describe("parseSourceFile — r.treeActions extraction", () => {
23
+ test("extracts a static treeActions pattern from an inline action-map", () => {
24
+ const result = parseInline(`
25
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
26
+ defineFeature("text-content", (r) => {
27
+ r.treeActions({
28
+ edit: { args: { slug: "" as string } },
29
+ list: {},
30
+ });
31
+ });
32
+ `);
33
+
34
+ expect(result.errors).toEqual([]);
35
+ const treeActions = result.patterns.find((p) => p.kind === "treeActions");
36
+ expect(treeActions).toBeDefined();
37
+ expect(treeActions?.kind).toBe("treeActions");
38
+ if (treeActions?.kind === "treeActions") {
39
+ expect(treeActions.definitions).toEqual({
40
+ edit: { args: { slug: "" } },
41
+ list: {},
42
+ });
43
+ }
44
+ });
45
+
46
+ test("treeActions pattern is classified as static (Designer renders form)", () => {
47
+ const result = parseInline(`
48
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
49
+ defineFeature("x", (r) => { r.treeActions({ list: {} }); });
50
+ `);
51
+ const treeActions = result.patterns.find((p) => p.kind === "treeActions");
52
+ expect(treeActions).toBeDefined();
53
+ if (treeActions !== undefined) {
54
+ expect(getEditability(treeActions)).toBe("static");
55
+ }
56
+ });
57
+
58
+ test("missing first argument produces a parse-error, not a pattern", () => {
59
+ const result = parseInline(`
60
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
61
+ defineFeature("x", (r) => { r.treeActions(); });
62
+ `);
63
+ expect(result.patterns.find((p) => p.kind === "treeActions")).toBeUndefined();
64
+ expect(result.errors.some((e) => e.methodName === "treeActions")).toBe(true);
65
+ });
66
+
67
+ test("non-object first argument (identifier ref) produces a parse-error", () => {
68
+ const result = parseInline(`
69
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
70
+ const actions = { list: {} };
71
+ defineFeature("x", (r) => { r.treeActions(actions); });
72
+ `);
73
+ expect(result.patterns.find((p) => p.kind === "treeActions")).toBeUndefined();
74
+ expect(result.errors.some((e) => e.methodName === "treeActions")).toBe(true);
75
+ });
76
+
77
+ test("renderPattern round-trips back to a valid r.treeActions(...) call", () => {
78
+ const result = parseInline(`
79
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
80
+ defineFeature("x", (r) => {
81
+ r.treeActions({ edit: { args: { slug: "" } }, list: {} });
82
+ });
83
+ `);
84
+ const treeActions = result.patterns.find((p) => p.kind === "treeActions");
85
+ expect(treeActions).toBeDefined();
86
+ if (treeActions !== undefined) {
87
+ const rendered = renderPattern(treeActions);
88
+ expect(rendered).toMatch(/^r\.treeActions\(/);
89
+ // renderValue darf identifier-safe Keys unquoted ausgeben — beide
90
+ // Schreibweisen sind valide TypeScript-Source.
91
+ expect(rendered).toMatch(/(["])edit\1|edit\s*:/);
92
+ expect(rendered).toMatch(/(["])slug\1|slug\s*:/);
93
+ }
94
+ });
95
+ });
96
+
97
+ describe("parseSourceFile — r.tree extraction", () => {
98
+ test("extracts an opaque tree pattern with the provider body as SourceLocation", () => {
99
+ const result = parseInline(`
100
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
101
+ defineFeature("text-content", (r) => {
102
+ r.tree((ctx) => (emit) => {
103
+ emit([{ label: "Marketing" }]);
104
+ return () => {};
105
+ });
106
+ });
107
+ `);
108
+
109
+ expect(result.errors).toEqual([]);
110
+ const tree = result.patterns.find((p) => p.kind === "tree");
111
+ expect(tree).toBeDefined();
112
+ if (tree?.kind === "tree") {
113
+ expect(tree.providerBody.raw).toContain("emit");
114
+ expect(tree.providerBody.raw).toContain("Marketing");
115
+ }
116
+ });
117
+
118
+ test("tree pattern is classified as opaque (Designer renders read-only code-block)", () => {
119
+ const result = parseInline(`
120
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
121
+ defineFeature("x", (r) => { r.tree((ctx) => (emit) => () => {}); });
122
+ `);
123
+ const tree = result.patterns.find((p) => p.kind === "tree");
124
+ expect(tree).toBeDefined();
125
+ if (tree !== undefined) {
126
+ expect(getEditability(tree)).toBe("opaque");
127
+ }
128
+ });
129
+
130
+ test("missing first argument produces a parse-error, not a pattern", () => {
131
+ const result = parseInline(`
132
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
133
+ defineFeature("x", (r) => { r.tree(); });
134
+ `);
135
+ expect(result.patterns.find((p) => p.kind === "tree")).toBeUndefined();
136
+ expect(result.errors.some((e) => e.methodName === "tree")).toBe(true);
137
+ });
138
+
139
+ test("non-function first argument (identifier ref) produces a parse-error", () => {
140
+ const result = parseInline(`
141
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
142
+ const provider = (ctx: unknown) => (emit: unknown) => () => {};
143
+ defineFeature("x", (r) => { r.tree(provider); });
144
+ `);
145
+ expect(result.patterns.find((p) => p.kind === "tree")).toBeUndefined();
146
+ expect(result.errors.some((e) => e.methodName === "tree")).toBe(true);
147
+ });
148
+
149
+ test("renderPattern round-trips back to a valid r.tree(...) call with the body verbatim", () => {
150
+ const result = parseInline(`
151
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
152
+ defineFeature("x", (r) => {
153
+ r.tree((ctx) => (emit) => { emit([]); return () => {}; });
154
+ });
155
+ `);
156
+ const tree = result.patterns.find((p) => p.kind === "tree");
157
+ expect(tree).toBeDefined();
158
+ if (tree !== undefined) {
159
+ const rendered = renderPattern(tree);
160
+ expect(rendered).toMatch(/^r\.tree\(/);
161
+ expect(rendered).toContain("emit");
162
+ }
163
+ });
164
+ });
165
+
166
+ describe("Combined — feature with both r.treeActions and r.tree", () => {
167
+ test("both patterns coexist in the parse output, source-order preserved", () => {
168
+ const result = parseInline(`
169
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
170
+ defineFeature("text-content", (r) => {
171
+ r.treeActions({ edit: { args: { slug: "" } } });
172
+ r.tree((ctx) => (emit) => { emit([{ label: "Marketing" }]); return () => {}; });
173
+ });
174
+ `);
175
+
176
+ expect(result.errors).toEqual([]);
177
+ const kinds = result.patterns.map((p) => p.kind);
178
+ const idxActions = kinds.indexOf("treeActions");
179
+ const idxTree = kinds.indexOf("tree");
180
+ expect(idxActions).toBeGreaterThanOrEqual(0);
181
+ expect(idxTree).toBeGreaterThanOrEqual(0);
182
+ expect(idxActions).toBeLessThan(idxTree);
183
+ });
184
+ });
@@ -0,0 +1,74 @@
1
+ export {
2
+ extractOptionalRequires,
3
+ extractReadsConfig,
4
+ extractRequires,
5
+ extractSystemScope,
6
+ extractToggleable,
7
+ } from "./round1";
8
+ export {
9
+ extractEntity,
10
+ extractNav,
11
+ extractRelation,
12
+ extractWorkspace,
13
+ } from "./round2";
14
+ export {
15
+ extractClaimKey,
16
+ extractConfig,
17
+ extractMetric,
18
+ extractReferenceData,
19
+ extractSecret,
20
+ extractTranslations,
21
+ extractUseExtension,
22
+ isClaimKeyType,
23
+ type NamedOptionsResult,
24
+ readNamedOptions,
25
+ } from "./round3";
26
+ export {
27
+ collectScreenOpaqueProps,
28
+ extractAuthClaims,
29
+ extractDefineEvent,
30
+ extractEntityHook,
31
+ extractEventMigration,
32
+ extractHook,
33
+ extractHttpRoute,
34
+ extractJob,
35
+ extractMultiStreamProjection,
36
+ extractNotification,
37
+ extractProjection,
38
+ extractQueryHandler,
39
+ extractScreen,
40
+ extractWriteHandler,
41
+ isEntityHookType,
42
+ isHookType,
43
+ isHttpRouteMethod,
44
+ type ParsedHandlerCall,
45
+ parseHandlerCall,
46
+ readApplyBodies,
47
+ readOptionalAccessRule,
48
+ readOptionalPhase,
49
+ readOptionalRateLimit,
50
+ readScreenStatic,
51
+ } from "./round4";
52
+ export {
53
+ extractExposesApi,
54
+ extractExtendsRegistrar,
55
+ extractUsesApi,
56
+ } from "./round5";
57
+ export {
58
+ extractTree,
59
+ extractTreeActions,
60
+ } from "./round6";
61
+ export type { ExtractOutput } from "./shared";
62
+ export {
63
+ fail,
64
+ findFunctionLiteral,
65
+ isPlainObject,
66
+ ok,
67
+ readBooleanProperty,
68
+ readDataLiteralNode,
69
+ readNameOrRef,
70
+ readNameOrRefOrList,
71
+ readPropertyKey,
72
+ readStringLiteralArgs,
73
+ readVarargsOrArrayProp,
74
+ } from "./shared";
@@ -0,0 +1,110 @@
1
+ import type { CallExpression, SourceFile } from "ts-morph";
2
+ import type {
3
+ OptionalRequiresPattern,
4
+ ReadsConfigPattern,
5
+ RequiresPattern,
6
+ SystemScopePattern,
7
+ ToggleablePattern,
8
+ } from "../patterns";
9
+ import { sourceLocationFromNode } from "../source-location";
10
+ import {
11
+ type ExtractOutput,
12
+ fail,
13
+ ok,
14
+ readBooleanProperty,
15
+ readVarargsOrArrayProp,
16
+ } from "./shared";
17
+
18
+ export function extractRequires(
19
+ call: CallExpression,
20
+ sourceFile: SourceFile,
21
+ ): ExtractOutput<RequiresPattern> {
22
+ const names = readVarargsOrArrayProp(call, "features");
23
+ if (!names) {
24
+ return fail(
25
+ "requires",
26
+ sourceLocationFromNode(call, sourceFile),
27
+ "expected positional string literals or { features: string[] }",
28
+ );
29
+ }
30
+ return ok({
31
+ kind: "requires",
32
+ source: sourceLocationFromNode(call, sourceFile),
33
+ featureNames: names,
34
+ });
35
+ }
36
+
37
+ export function extractOptionalRequires(
38
+ call: CallExpression,
39
+ sourceFile: SourceFile,
40
+ ): ExtractOutput<OptionalRequiresPattern> {
41
+ const names = readVarargsOrArrayProp(call, "features");
42
+ if (!names) {
43
+ return fail(
44
+ "optionalRequires",
45
+ sourceLocationFromNode(call, sourceFile),
46
+ "expected positional string literals or { features: string[] }",
47
+ );
48
+ }
49
+ return ok({
50
+ kind: "optionalRequires",
51
+ source: sourceLocationFromNode(call, sourceFile),
52
+ featureNames: names,
53
+ });
54
+ }
55
+
56
+ export function extractReadsConfig(
57
+ call: CallExpression,
58
+ sourceFile: SourceFile,
59
+ ): ExtractOutput<ReadsConfigPattern> {
60
+ const keys = readVarargsOrArrayProp(call, "keys");
61
+ if (!keys) {
62
+ return fail(
63
+ "readsConfig",
64
+ sourceLocationFromNode(call, sourceFile),
65
+ "expected positional string literals or { keys: string[] }",
66
+ );
67
+ }
68
+ return ok({
69
+ kind: "readsConfig",
70
+ source: sourceLocationFromNode(call, sourceFile),
71
+ qualifiedKeys: keys,
72
+ });
73
+ }
74
+
75
+ export function extractSystemScope(
76
+ call: CallExpression,
77
+ sourceFile: SourceFile,
78
+ ): ExtractOutput<SystemScopePattern> {
79
+ return ok({
80
+ kind: "systemScope",
81
+ source: sourceLocationFromNode(call, sourceFile),
82
+ });
83
+ }
84
+
85
+ export function extractToggleable(
86
+ call: CallExpression,
87
+ sourceFile: SourceFile,
88
+ ): ExtractOutput<ToggleablePattern> {
89
+ const arg = call.getArguments()[0];
90
+ if (!arg) {
91
+ return fail(
92
+ "toggleable",
93
+ sourceLocationFromNode(call, sourceFile),
94
+ "expected an object argument with a `default` boolean",
95
+ );
96
+ }
97
+ const defaultValue = readBooleanProperty(arg, "default");
98
+ if (defaultValue === undefined) {
99
+ return fail(
100
+ "toggleable",
101
+ sourceLocationFromNode(call, sourceFile),
102
+ "argument must be `{ default: true | false }`",
103
+ );
104
+ }
105
+ return ok({
106
+ kind: "toggleable",
107
+ source: sourceLocationFromNode(call, sourceFile),
108
+ default: defaultValue,
109
+ });
110
+ }
@@ -0,0 +1,253 @@
1
+ import type { CallExpression, SourceFile } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type { EntityDefinition } from "../../types/fields";
4
+ import type { NavDefinition } from "../../types/nav";
5
+ import type { RelationDefinition } from "../../types/relations";
6
+ import type { WorkspaceDefinition } from "../../types/workspace";
7
+ import type { EntityPattern, NavPattern, RelationPattern, WorkspacePattern } from "../patterns";
8
+ import { sourceLocationFromNode } from "../source-location";
9
+ import {
10
+ type ExtractOutput,
11
+ fail,
12
+ isPlainObject,
13
+ ok,
14
+ readDataLiteralNode,
15
+ readNameOrRef,
16
+ } from "./shared";
17
+
18
+ export function extractEntity(
19
+ call: CallExpression,
20
+ sourceFile: SourceFile,
21
+ ): ExtractOutput<EntityPattern> {
22
+ const args = call.getArguments();
23
+ const first = args[0];
24
+ if (!first) {
25
+ return fail(
26
+ "entity",
27
+ sourceLocationFromNode(call, sourceFile),
28
+ "expected at least one argument",
29
+ );
30
+ }
31
+
32
+ const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
33
+ if (obj && args.length === 1) {
34
+ const nameInit = obj
35
+ .getProperty("name")
36
+ ?.asKind(SyntaxKind.PropertyAssignment)
37
+ ?.getInitializer()
38
+ ?.asKind(SyntaxKind.StringLiteral);
39
+ if (!nameInit) {
40
+ return fail(
41
+ "entity",
42
+ sourceLocationFromNode(call, sourceFile),
43
+ "object form requires a string-literal `name` property",
44
+ );
45
+ }
46
+ const definition = readDataLiteralNode(obj);
47
+ if (!isPlainObject(definition)) {
48
+ return fail(
49
+ "entity",
50
+ sourceLocationFromNode(call, sourceFile),
51
+ "definition could not be read as a plain object (contains functions or identifiers)",
52
+ );
53
+ }
54
+ const { name: _name, ...defWithoutName } = definition;
55
+ return ok({
56
+ kind: "entity",
57
+ source: sourceLocationFromNode(call, sourceFile),
58
+ entityName: nameInit.getLiteralValue(),
59
+ definition: defWithoutName as EntityDefinition,
60
+ });
61
+ }
62
+
63
+ const nameArg = first.asKind(SyntaxKind.StringLiteral);
64
+ if (!nameArg) {
65
+ return fail(
66
+ "entity",
67
+ sourceLocationFromNode(call, sourceFile),
68
+ "first argument must be a string literal name (or use the object form)",
69
+ );
70
+ }
71
+ const defArg = args[1];
72
+ if (!defArg) {
73
+ return fail(
74
+ "entity",
75
+ sourceLocationFromNode(call, sourceFile),
76
+ "expected a definition object as second argument",
77
+ );
78
+ }
79
+ const definition = readDataLiteralNode(defArg);
80
+ if (!isPlainObject(definition)) {
81
+ return fail(
82
+ "entity",
83
+ sourceLocationFromNode(call, sourceFile),
84
+ "definition could not be read as a plain object (contains functions or identifiers)",
85
+ );
86
+ }
87
+ return ok({
88
+ kind: "entity",
89
+ source: sourceLocationFromNode(call, sourceFile),
90
+ entityName: nameArg.getLiteralValue(),
91
+ definition: definition as EntityDefinition,
92
+ });
93
+ }
94
+
95
+ export function extractRelation(
96
+ call: CallExpression,
97
+ sourceFile: SourceFile,
98
+ ): ExtractOutput<RelationPattern> {
99
+ const args = call.getArguments();
100
+ const first = args[0];
101
+ if (!first) {
102
+ return fail(
103
+ "relation",
104
+ sourceLocationFromNode(call, sourceFile),
105
+ "expected at least one argument",
106
+ );
107
+ }
108
+
109
+ const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
110
+ if (obj && args.length === 1) {
111
+ const entityInit = obj
112
+ .getProperty("entity")
113
+ ?.asKind(SyntaxKind.PropertyAssignment)
114
+ ?.getInitializer();
115
+ if (!entityInit) {
116
+ return fail(
117
+ "relation",
118
+ sourceLocationFromNode(call, sourceFile),
119
+ "object form requires an `entity` property",
120
+ );
121
+ }
122
+ const entityName = readNameOrRef(entityInit);
123
+ if (!entityName) {
124
+ return fail(
125
+ "relation",
126
+ sourceLocationFromNode(call, sourceFile),
127
+ '`entity` must be a string literal or `{ name: "..." }` ref',
128
+ );
129
+ }
130
+ const nameInit = obj
131
+ .getProperty("name")
132
+ ?.asKind(SyntaxKind.PropertyAssignment)
133
+ ?.getInitializer()
134
+ ?.asKind(SyntaxKind.StringLiteral);
135
+ if (!nameInit) {
136
+ return fail(
137
+ "relation",
138
+ sourceLocationFromNode(call, sourceFile),
139
+ "object form requires a string-literal `name` property",
140
+ );
141
+ }
142
+ const definition = readDataLiteralNode(obj);
143
+ if (!isPlainObject(definition)) {
144
+ return fail(
145
+ "relation",
146
+ sourceLocationFromNode(call, sourceFile),
147
+ "definition could not be read as a plain object",
148
+ );
149
+ }
150
+ const { entity: _e, name: _n, ...defWithoutCarriers } = definition;
151
+ return ok({
152
+ kind: "relation",
153
+ source: sourceLocationFromNode(call, sourceFile),
154
+ entityName,
155
+ relationName: nameInit.getLiteralValue(),
156
+ definition: defWithoutCarriers as RelationDefinition,
157
+ });
158
+ }
159
+
160
+ const entityName = readNameOrRef(first);
161
+ if (!entityName) {
162
+ return fail(
163
+ "relation",
164
+ sourceLocationFromNode(call, sourceFile),
165
+ 'first argument must be a string literal or an inline { name: "..." } object (or use the object form)',
166
+ );
167
+ }
168
+ const nameArg = args[1]?.asKind(SyntaxKind.StringLiteral);
169
+ if (!nameArg) {
170
+ return fail(
171
+ "relation",
172
+ sourceLocationFromNode(call, sourceFile),
173
+ "second argument must be a string literal relation name",
174
+ );
175
+ }
176
+ const defArg = args[2];
177
+ if (!defArg) {
178
+ return fail(
179
+ "relation",
180
+ sourceLocationFromNode(call, sourceFile),
181
+ "expected a definition object as third argument",
182
+ );
183
+ }
184
+ const definition = readDataLiteralNode(defArg);
185
+ if (!isPlainObject(definition)) {
186
+ return fail(
187
+ "relation",
188
+ sourceLocationFromNode(call, sourceFile),
189
+ "definition could not be read as a plain object",
190
+ );
191
+ }
192
+ return ok({
193
+ kind: "relation",
194
+ source: sourceLocationFromNode(call, sourceFile),
195
+ entityName,
196
+ relationName: nameArg.getLiteralValue(),
197
+ definition: definition as RelationDefinition,
198
+ });
199
+ }
200
+
201
+ export function extractNav(
202
+ call: CallExpression,
203
+ sourceFile: SourceFile,
204
+ ): ExtractOutput<NavPattern> {
205
+ const arg = call.getArguments()[0];
206
+ if (!arg) {
207
+ return fail(
208
+ "nav",
209
+ sourceLocationFromNode(call, sourceFile),
210
+ "expected a NavDefinition object as first argument",
211
+ );
212
+ }
213
+ const definition = readDataLiteralNode(arg);
214
+ if (!isPlainObject(definition)) {
215
+ return fail(
216
+ "nav",
217
+ sourceLocationFromNode(call, sourceFile),
218
+ "definition could not be read as a plain object",
219
+ );
220
+ }
221
+ return ok({
222
+ kind: "nav",
223
+ source: sourceLocationFromNode(call, sourceFile),
224
+ definition: definition as NavDefinition,
225
+ });
226
+ }
227
+
228
+ export function extractWorkspace(
229
+ call: CallExpression,
230
+ sourceFile: SourceFile,
231
+ ): ExtractOutput<WorkspacePattern> {
232
+ const arg = call.getArguments()[0];
233
+ if (!arg) {
234
+ return fail(
235
+ "workspace",
236
+ sourceLocationFromNode(call, sourceFile),
237
+ "expected a WorkspaceDefinition object as first argument",
238
+ );
239
+ }
240
+ const definition = readDataLiteralNode(arg);
241
+ if (!isPlainObject(definition)) {
242
+ return fail(
243
+ "workspace",
244
+ sourceLocationFromNode(call, sourceFile),
245
+ "definition could not be read as a plain object",
246
+ );
247
+ }
248
+ return ok({
249
+ kind: "workspace",
250
+ source: sourceLocationFromNode(call, sourceFile),
251
+ definition: definition as WorkspaceDefinition,
252
+ });
253
+ }