@cosmicdrift/kumiko-framework 0.155.0 → 0.156.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +9 -38
  2. package/package.json +2 -2
  3. package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
  4. package/src/__tests__/raw-table.integration.test.ts +17 -40
  5. package/src/crypto/__tests__/pii-field-encryption.test.ts +77 -0
  6. package/src/crypto/index.ts +1 -0
  7. package/src/crypto/pii-field-encryption.ts +19 -0
  8. package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +27 -1
  9. package/src/db/__tests__/blind-index.integration.test.ts +16 -0
  10. package/src/db/__tests__/collect-table-metas.test.ts +6 -6
  11. package/src/db/__tests__/feature-table-sources.test.ts +7 -7
  12. package/src/db/__tests__/migrate-generator.test.ts +21 -0
  13. package/src/db/__tests__/number-field-fractional.integration.test.ts +1 -1
  14. package/src/db/__tests__/tenant-db-where-merge.test.ts +39 -0
  15. package/src/db/collect-table-metas.ts +6 -7
  16. package/src/db/entity-table-meta.ts +1 -1
  17. package/src/db/feature-table-sources.ts +5 -11
  18. package/src/db/migrate-generator.ts +39 -6
  19. package/src/db/queries/shadow-swap.ts +85 -3
  20. package/src/db/tenant-db.ts +8 -0
  21. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +102 -0
  22. package/src/engine/__tests__/build-config-feature-schema.test.ts +16 -0
  23. package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
  24. package/src/engine/__tests__/field-access.test.ts +23 -1
  25. package/src/engine/__tests__/raw-table.test.ts +134 -84
  26. package/src/engine/__tests__/registry.test.ts +40 -0
  27. package/src/engine/boot-validator/__tests__/config-deps.test.ts +52 -2
  28. package/src/engine/boot-validator/action-wiring.ts +2 -2
  29. package/src/engine/boot-validator/config-deps.ts +35 -0
  30. package/src/engine/boot-validator/index.ts +2 -0
  31. package/src/engine/boot-validator/pii-retention.ts +35 -3
  32. package/src/engine/build-config-feature-schema.ts +3 -3
  33. package/src/engine/config-helpers.ts +2 -0
  34. package/src/engine/define-feature.ts +2 -6
  35. package/src/engine/entity-handlers.ts +2 -4
  36. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
  37. package/src/engine/feature-ast/__tests__/parse.test.ts +170 -0
  38. package/src/engine/feature-ast/__tests__/patch.test.ts +31 -0
  39. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +6 -1
  40. package/src/engine/feature-ast/extractors/events.ts +322 -0
  41. package/src/engine/feature-ast/extractors/handlers.ts +222 -0
  42. package/src/engine/feature-ast/extractors/hooks.ts +243 -0
  43. package/src/engine/feature-ast/extractors/index.ts +32 -24
  44. package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
  45. package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
  46. package/src/engine/feature-ast/extractors/round5.ts +3 -3
  47. package/src/engine/feature-ast/parse.ts +3 -3
  48. package/src/engine/feature-ast/render.ts +16 -11
  49. package/src/engine/feature-builder-state.ts +0 -3
  50. package/src/engine/feature-config-events-jobs.ts +3 -6
  51. package/src/engine/feature-entity-handlers.ts +9 -1
  52. package/src/engine/feature-ui-extensions.ts +10 -40
  53. package/src/engine/field-access.ts +13 -2
  54. package/src/engine/object-form.ts +15 -0
  55. package/src/engine/registry-facade.ts +4 -0
  56. package/src/engine/registry-ingest.ts +19 -29
  57. package/src/engine/registry-state.ts +1 -7
  58. package/src/engine/registry-validate.ts +5 -28
  59. package/src/engine/registry.ts +0 -2
  60. package/src/engine/types/config.ts +7 -0
  61. package/src/engine/types/feature.ts +40 -68
  62. package/src/engine/types/fields.ts +6 -0
  63. package/src/engine/types/index.ts +0 -3
  64. package/src/es-ops/README.md +1 -1
  65. package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
  66. package/src/es-ops/context.ts +4 -2
  67. package/src/es-ops/types.ts +8 -1
  68. package/src/pipeline/msp-rebuild.ts +4 -0
  69. package/src/pipeline/projection-rebuild.ts +35 -0
  70. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +8 -1
  71. package/src/stack/test-stack.ts +17 -2
  72. package/src/ui-types/index.ts +1 -0
  73. package/src/engine/__tests__/unmanaged-table.test.ts +0 -172
  74. package/src/engine/feature-ast/extractors/round4.ts +0 -1227
@@ -0,0 +1,221 @@
1
+ import type { CallExpression, SourceFile } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type { JobDefinition } from "../../types/config";
4
+ import type { HttpRouteMethod } from "../../types/http-route";
5
+ import type { HttpRoutePattern, JobPattern } from "../patterns";
6
+ import { sourceLocationFromNode } from "../source-location";
7
+ import {
8
+ type ExtractOutput,
9
+ fail,
10
+ findFunctionLiteral,
11
+ isPlainObject,
12
+ ok,
13
+ readBooleanProperty,
14
+ readDataLiteralNode,
15
+ readPropertyKey,
16
+ } from "./shared";
17
+
18
+ export function isHttpRouteMethod(value: string): value is HttpRouteMethod {
19
+ return (
20
+ value === "GET" ||
21
+ value === "POST" ||
22
+ value === "PUT" ||
23
+ value === "PATCH" ||
24
+ value === "DELETE" ||
25
+ value === "HEAD" ||
26
+ value === "OPTIONS"
27
+ );
28
+ }
29
+
30
+ export function extractJob(
31
+ call: CallExpression,
32
+ sourceFile: SourceFile,
33
+ ): ExtractOutput<JobPattern> {
34
+ const args = call.getArguments();
35
+ const first = args[0];
36
+ if (!first) {
37
+ return fail("job", sourceLocationFromNode(call, sourceFile), "expected at least one argument");
38
+ }
39
+
40
+ const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
41
+ if (obj && args.length === 1) {
42
+ const nameInit = obj
43
+ .getProperty("name")
44
+ ?.asKind(SyntaxKind.PropertyAssignment)
45
+ ?.getInitializer()
46
+ ?.asKind(SyntaxKind.StringLiteral);
47
+ if (!nameInit) {
48
+ return fail(
49
+ "job",
50
+ sourceLocationFromNode(call, sourceFile),
51
+ "object form requires a string-literal `name` property",
52
+ );
53
+ }
54
+ const handlerInit = obj
55
+ .getProperty("handler")
56
+ ?.asKind(SyntaxKind.PropertyAssignment)
57
+ ?.getInitializer();
58
+ if (!handlerInit) {
59
+ return fail(
60
+ "job",
61
+ sourceLocationFromNode(call, sourceFile),
62
+ "object form requires a `handler` property",
63
+ );
64
+ }
65
+ const fn = findFunctionLiteral(handlerInit);
66
+ if (!fn) {
67
+ return fail(
68
+ "job",
69
+ sourceLocationFromNode(call, sourceFile),
70
+ "handler must be an inline arrow function or function expression",
71
+ );
72
+ }
73
+ const optionsBag: Record<string, unknown> = {};
74
+ for (const prop of obj.getProperties()) {
75
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
76
+ if (!propAssign) continue;
77
+ const key = readPropertyKey(propAssign);
78
+ if (key === "name" || key === "handler") continue;
79
+ const init = propAssign.getInitializer();
80
+ if (!init) continue;
81
+ const value = readDataLiteralNode(init);
82
+ if (value === undefined) {
83
+ return fail(
84
+ "job",
85
+ sourceLocationFromNode(call, sourceFile),
86
+ `option "${key}" could not be read as a plain value`,
87
+ );
88
+ }
89
+ optionsBag[key] = value;
90
+ }
91
+ return ok({
92
+ kind: "job",
93
+ source: sourceLocationFromNode(call, sourceFile),
94
+ jobName: nameInit.getLiteralValue(),
95
+ options: optionsBag as Omit<JobDefinition, "name" | "handler">,
96
+ handlerBody: sourceLocationFromNode(fn, sourceFile),
97
+ });
98
+ }
99
+
100
+ const nameArg = first.asKind(SyntaxKind.StringLiteral);
101
+ if (!nameArg) {
102
+ return fail(
103
+ "job",
104
+ sourceLocationFromNode(call, sourceFile),
105
+ "first argument must be a string literal job name (or use the object form)",
106
+ );
107
+ }
108
+ const optionsArg = args[1];
109
+ if (!optionsArg) {
110
+ return fail(
111
+ "job",
112
+ sourceLocationFromNode(call, sourceFile),
113
+ "expected an options object as second argument",
114
+ );
115
+ }
116
+ const options = readDataLiteralNode(optionsArg);
117
+ if (!isPlainObject(options)) {
118
+ return fail(
119
+ "job",
120
+ sourceLocationFromNode(call, sourceFile),
121
+ "options could not be read as a plain object",
122
+ );
123
+ }
124
+ const handlerArg = args[2];
125
+ if (!handlerArg) {
126
+ return fail(
127
+ "job",
128
+ sourceLocationFromNode(call, sourceFile),
129
+ "expected a handler function as third argument",
130
+ );
131
+ }
132
+ const fn = findFunctionLiteral(handlerArg);
133
+ if (!fn) {
134
+ return fail(
135
+ "job",
136
+ sourceLocationFromNode(call, sourceFile),
137
+ "third argument must be an inline arrow function or function expression",
138
+ );
139
+ }
140
+ return ok({
141
+ kind: "job",
142
+ source: sourceLocationFromNode(call, sourceFile),
143
+ jobName: nameArg.getLiteralValue(),
144
+ options: options as Omit<JobDefinition, "name" | "handler">,
145
+ handlerBody: sourceLocationFromNode(fn, sourceFile),
146
+ });
147
+ }
148
+
149
+ export function extractHttpRoute(
150
+ call: CallExpression,
151
+ sourceFile: SourceFile,
152
+ ): ExtractOutput<HttpRoutePattern> {
153
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
154
+ if (!arg) {
155
+ return fail(
156
+ "httpRoute",
157
+ sourceLocationFromNode(call, sourceFile),
158
+ "argument must be an inline HttpRouteDefinition object",
159
+ );
160
+ }
161
+ const methodLiteral = arg
162
+ .getProperty("method")
163
+ ?.asKind(SyntaxKind.PropertyAssignment)
164
+ ?.getInitializer()
165
+ ?.asKind(SyntaxKind.StringLiteral);
166
+ if (!methodLiteral) {
167
+ return fail(
168
+ "httpRoute",
169
+ sourceLocationFromNode(call, sourceFile),
170
+ "method must be a string literal",
171
+ );
172
+ }
173
+ const methodValue = methodLiteral.getLiteralValue();
174
+ if (!isHttpRouteMethod(methodValue)) {
175
+ return fail(
176
+ "httpRoute",
177
+ sourceLocationFromNode(call, sourceFile),
178
+ `method must be one of GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS (got "${methodValue}")`,
179
+ );
180
+ }
181
+ const pathLiteral = arg
182
+ .getProperty("path")
183
+ ?.asKind(SyntaxKind.PropertyAssignment)
184
+ ?.getInitializer()
185
+ ?.asKind(SyntaxKind.StringLiteral);
186
+ if (!pathLiteral) {
187
+ return fail(
188
+ "httpRoute",
189
+ sourceLocationFromNode(call, sourceFile),
190
+ "path must be a string literal",
191
+ );
192
+ }
193
+ const handlerInit = arg
194
+ .getProperty("handler")
195
+ ?.asKind(SyntaxKind.PropertyAssignment)
196
+ ?.getInitializer();
197
+ if (!handlerInit) {
198
+ return fail(
199
+ "httpRoute",
200
+ sourceLocationFromNode(call, sourceFile),
201
+ "missing `handler` property",
202
+ );
203
+ }
204
+ const fn = findFunctionLiteral(handlerInit);
205
+ if (!fn) {
206
+ return fail(
207
+ "httpRoute",
208
+ sourceLocationFromNode(call, sourceFile),
209
+ "handler must be an inline arrow function or function expression",
210
+ );
211
+ }
212
+ const anonymous = readBooleanProperty(arg, "anonymous");
213
+ return ok({
214
+ kind: "httpRoute",
215
+ source: sourceLocationFromNode(call, sourceFile),
216
+ method: methodValue,
217
+ path: pathLiteral.getLiteralValue(),
218
+ handlerBody: sourceLocationFromNode(fn, sourceFile),
219
+ ...(anonymous === true && { anonymous: true }),
220
+ });
221
+ }
@@ -0,0 +1,269 @@
1
+ import type { CallExpression, Node, SourceFile } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type { RunIn } from "../../types/config";
4
+ import type { MspErrorMode } from "../../types/projection";
5
+ import type { ScreenDefinition } from "../../types/screen";
6
+ import type {
7
+ MultiStreamProjectionPattern,
8
+ OpaquePropMap,
9
+ ProjectionPattern,
10
+ ScreenPattern,
11
+ } from "../patterns";
12
+ import { SCREEN_OPAQUE_MARKER } from "../patterns";
13
+ import type { SourceLocation } from "../source-location";
14
+ import { sourceLocationFromNode } from "../source-location";
15
+ import {
16
+ type ExtractOutput,
17
+ fail,
18
+ findFunctionLiteral,
19
+ isPlainObject,
20
+ isRawRefSentinel,
21
+ ok,
22
+ readDataLiteralNode,
23
+ readNameOrRefOrList,
24
+ readPropertyKey,
25
+ } from "./shared";
26
+
27
+ export function readApplyBodies(
28
+ defObj: ReturnType<Node["asKind"]>,
29
+ sourceFile: SourceFile,
30
+ ): Record<string, SourceLocation> | undefined {
31
+ if (!defObj) return undefined;
32
+ const obj = defObj.asKind?.(SyntaxKind.ObjectLiteralExpression);
33
+ if (!obj) return undefined;
34
+ const applyObj = obj
35
+ .getProperty("apply")
36
+ ?.asKind(SyntaxKind.PropertyAssignment)
37
+ ?.getInitializer()
38
+ ?.asKind(SyntaxKind.ObjectLiteralExpression);
39
+ if (!applyObj) return undefined;
40
+ const out: Record<string, SourceLocation> = {};
41
+ for (const prop of applyObj.getProperties()) {
42
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
43
+ if (!propAssign) return undefined;
44
+ const init = propAssign.getInitializer();
45
+ if (!init) return undefined;
46
+ const fn = findFunctionLiteral(init);
47
+ if (!fn) return undefined;
48
+ out[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ export function extractProjection(
54
+ call: CallExpression,
55
+ sourceFile: SourceFile,
56
+ ): ExtractOutput<ProjectionPattern> {
57
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
58
+ if (!arg) {
59
+ return fail(
60
+ "projection",
61
+ sourceLocationFromNode(call, sourceFile),
62
+ "argument must be an inline ProjectionDefinition object",
63
+ );
64
+ }
65
+ const nameLit = arg
66
+ .getProperty("name")
67
+ ?.asKind(SyntaxKind.PropertyAssignment)
68
+ ?.getInitializer()
69
+ ?.asKind(SyntaxKind.StringLiteral);
70
+ if (!nameLit) {
71
+ return fail(
72
+ "projection",
73
+ sourceLocationFromNode(call, sourceFile),
74
+ "name must be a string literal",
75
+ );
76
+ }
77
+ const sourceInit = arg
78
+ .getProperty("source")
79
+ ?.asKind(SyntaxKind.PropertyAssignment)
80
+ ?.getInitializer();
81
+ if (!sourceInit) {
82
+ return fail(
83
+ "projection",
84
+ sourceLocationFromNode(call, sourceFile),
85
+ "missing `source` property",
86
+ );
87
+ }
88
+ const sourceEntity = readNameOrRefOrList(sourceInit);
89
+ if (!sourceEntity) {
90
+ return fail(
91
+ "projection",
92
+ sourceLocationFromNode(call, sourceFile),
93
+ "source must be a string literal or array of string literals",
94
+ );
95
+ }
96
+ const applyBodies = readApplyBodies(arg, sourceFile);
97
+ if (!applyBodies) {
98
+ return fail(
99
+ "projection",
100
+ sourceLocationFromNode(call, sourceFile),
101
+ "apply must be an inline object map of event-type → function",
102
+ );
103
+ }
104
+ return ok({
105
+ kind: "projection",
106
+ source: sourceLocationFromNode(call, sourceFile),
107
+ name: nameLit.getLiteralValue(),
108
+ sourceEntity,
109
+ applyBodies,
110
+ });
111
+ }
112
+
113
+ export function extractMultiStreamProjection(
114
+ call: CallExpression,
115
+ sourceFile: SourceFile,
116
+ ): ExtractOutput<MultiStreamProjectionPattern> {
117
+ const arg = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
118
+ if (!arg) {
119
+ return fail(
120
+ "multiStreamProjection",
121
+ sourceLocationFromNode(call, sourceFile),
122
+ "argument must be an inline MultiStreamProjectionDefinition object",
123
+ );
124
+ }
125
+ const nameLit = arg
126
+ .getProperty("name")
127
+ ?.asKind(SyntaxKind.PropertyAssignment)
128
+ ?.getInitializer()
129
+ ?.asKind(SyntaxKind.StringLiteral);
130
+ if (!nameLit) {
131
+ return fail(
132
+ "multiStreamProjection",
133
+ sourceLocationFromNode(call, sourceFile),
134
+ "name must be a string literal",
135
+ );
136
+ }
137
+ const applyBodies = readApplyBodies(arg, sourceFile);
138
+ if (!applyBodies) {
139
+ return fail(
140
+ "multiStreamProjection",
141
+ sourceLocationFromNode(call, sourceFile),
142
+ "apply must be an inline object map of event-type → function",
143
+ );
144
+ }
145
+ const errorModeInit = arg
146
+ .getProperty("errorMode")
147
+ ?.asKind(SyntaxKind.PropertyAssignment)
148
+ ?.getInitializer();
149
+ const errorMode = errorModeInit ? readDataLiteralNode(errorModeInit) : undefined;
150
+ const runInLit = arg
151
+ .getProperty("runIn")
152
+ ?.asKind(SyntaxKind.PropertyAssignment)
153
+ ?.getInitializer()
154
+ ?.asKind(SyntaxKind.StringLiteral);
155
+ const runIn = runInLit ? (runInLit.getLiteralValue() as RunIn) : undefined;
156
+ const deliveryLit = arg
157
+ .getProperty("delivery")
158
+ ?.asKind(SyntaxKind.PropertyAssignment)
159
+ ?.getInitializer()
160
+ ?.asKind(SyntaxKind.StringLiteral);
161
+ const delivery = deliveryLit
162
+ ? (deliveryLit.getLiteralValue() as "shared" | "per-instance")
163
+ : undefined;
164
+ return ok({
165
+ kind: "multiStreamProjection",
166
+ source: sourceLocationFromNode(call, sourceFile),
167
+ name: nameLit.getLiteralValue(),
168
+ applyBodies,
169
+ ...(isPlainObject(errorMode) && { errorMode: errorMode as MspErrorMode }),
170
+ ...(runIn !== undefined && { runIn }),
171
+ ...(delivery !== undefined && { delivery }),
172
+ });
173
+ }
174
+
175
+ export function collectScreenOpaqueProps(
176
+ node: Node,
177
+ path: string,
178
+ sourceFile: SourceFile,
179
+ out: Record<string, SourceLocation>,
180
+ ): void {
181
+ const fn = findFunctionLiteral(node);
182
+ if (fn) {
183
+ out[path] = sourceLocationFromNode(fn, sourceFile);
184
+ } else if (node.isKind(SyntaxKind.ObjectLiteralExpression)) {
185
+ for (const prop of node.getProperties()) {
186
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
187
+ if (!propAssign) continue;
188
+ const init = propAssign.getInitializer();
189
+ if (!init) continue;
190
+ const key = readPropertyKey(propAssign);
191
+ const childPath = path ? `${path}.${key}` : key;
192
+ collectScreenOpaqueProps(init, childPath, sourceFile, out);
193
+ }
194
+ } else if (node.isKind(SyntaxKind.ArrayLiteralExpression)) {
195
+ node.getElements().forEach((el, idx) => {
196
+ collectScreenOpaqueProps(el, `${path}.${idx}`, sourceFile, out);
197
+ });
198
+ }
199
+ }
200
+
201
+ export function readScreenStatic(node: Node): unknown {
202
+ if (findFunctionLiteral(node)) return SCREEN_OPAQUE_MARKER;
203
+ const obj = node.asKind(SyntaxKind.ObjectLiteralExpression);
204
+ if (obj) {
205
+ const out: Record<string, unknown> = {};
206
+ for (const prop of obj.getProperties()) {
207
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
208
+ if (!propAssign) continue;
209
+ const init = propAssign.getInitializer();
210
+ if (!init) continue;
211
+ out[readPropertyKey(propAssign)] = readScreenStatic(init);
212
+ }
213
+ return out;
214
+ }
215
+ const arr = node.asKind(SyntaxKind.ArrayLiteralExpression);
216
+ if (arr) {
217
+ return arr.getElements().map(readScreenStatic);
218
+ }
219
+ const value = readDataLiteralNode(node);
220
+ if (value === undefined) return SCREEN_OPAQUE_MARKER;
221
+ return value;
222
+ }
223
+
224
+ export function extractScreen(
225
+ call: CallExpression,
226
+ sourceFile: SourceFile,
227
+ ): ExtractOutput<ScreenPattern> {
228
+ const arg = call.getArguments()[0];
229
+ if (!arg) {
230
+ return fail(
231
+ "screen",
232
+ sourceLocationFromNode(call, sourceFile),
233
+ "expected a ScreenDefinition object as first argument",
234
+ );
235
+ }
236
+ const obj = arg.asKind(SyntaxKind.ObjectLiteralExpression);
237
+ if (!obj) {
238
+ const raw = readDataLiteralNode(arg);
239
+ if (!isRawRefSentinel(raw)) {
240
+ return fail(
241
+ "screen",
242
+ sourceLocationFromNode(call, sourceFile),
243
+ "argument must be an inline object literal",
244
+ );
245
+ }
246
+ return ok({
247
+ kind: "screen",
248
+ source: sourceLocationFromNode(call, sourceFile),
249
+ definition: raw as unknown as ScreenDefinition,
250
+ opaqueProps: {} as OpaquePropMap,
251
+ });
252
+ }
253
+ const opaqueProps: Record<string, SourceLocation> = {};
254
+ collectScreenOpaqueProps(obj, "", sourceFile, opaqueProps);
255
+ const definition = readScreenStatic(obj);
256
+ if (!isPlainObject(definition)) {
257
+ return fail(
258
+ "screen",
259
+ sourceLocationFromNode(call, sourceFile),
260
+ "definition could not be read structurally",
261
+ );
262
+ }
263
+ return ok({
264
+ kind: "screen",
265
+ source: sourceLocationFromNode(call, sourceFile),
266
+ definition: definition as ScreenDefinition,
267
+ opaqueProps: opaqueProps as OpaquePropMap,
268
+ });
269
+ }
@@ -95,7 +95,7 @@ export function extractExposesApi(
95
95
  });
96
96
  }
97
97
 
98
- export function extractUnmanagedTable(
98
+ export function extractRawTable(
99
99
  call: CallExpression,
100
100
  sourceFile: SourceFile,
101
101
  ): ExtractOutput<never> {
@@ -104,8 +104,8 @@ export function extractUnmanagedTable(
104
104
  // so there is nothing to extract statically. A clean ParseError (not
105
105
  // UnknownPattern) marks it design-time-unreadable, like entity-by-identifier.
106
106
  return fail(
107
- "unmanagedTable",
107
+ "rawTable",
108
108
  sourceLocationFromNode(call, sourceFile),
109
- "unmanagedTable meta is a factory/identifier argument, not a static literal",
109
+ "rawTable meta is a factory/identifier argument, not a static literal",
110
110
  );
111
111
  }
@@ -51,6 +51,7 @@ import {
51
51
  extractOptionalRequires,
52
52
  extractProjection,
53
53
  extractQueryHandler,
54
+ extractRawTable,
54
55
  extractReadsConfig,
55
56
  extractReferenceData,
56
57
  extractRelation,
@@ -62,7 +63,6 @@ import {
62
63
  extractTranslations,
63
64
  extractTreeActions,
64
65
  extractUiHints,
65
- extractUnmanagedTable,
66
66
  extractUseExtension,
67
67
  extractUsesApi,
68
68
  extractWorkspace,
@@ -468,8 +468,8 @@ function dispatchExtractor(
468
468
  return extractUsesApi(call, sourceFile);
469
469
  case "exposesApi":
470
470
  return extractExposesApi(call, sourceFile);
471
- case "unmanagedTable":
472
- return extractUnmanagedTable(call, sourceFile);
471
+ case "rawTable":
472
+ return extractRawTable(call, sourceFile);
473
473
  // Round 6 — Tree-Actions pattern
474
474
  case "treeActions":
475
475
  return extractTreeActions(call, sourceFile);
@@ -491,19 +491,24 @@ function renderMultiStreamProjection(p: MultiStreamProjectionPattern): string {
491
491
  }
492
492
 
493
493
  function renderDefineEvent(p: DefineEventPattern): string {
494
- const lines: string[] = ["r.defineEvent({"];
495
- lines.push(` name: ${JSON.stringify(p.eventName)},`);
496
- lines.push(` schema: ${p.schemaSource.raw},`);
494
+ const migrationEntries = p.migrations !== undefined ? Object.entries(p.migrations) : [];
495
+ const hasOptions = p.version !== undefined || migrationEntries.length > 0;
496
+ if (!hasOptions) {
497
+ return `r.defineEvent(${JSON.stringify(p.eventName)}, ${p.schemaSource.raw});`;
498
+ }
499
+ const lines: string[] = [
500
+ `r.defineEvent(${JSON.stringify(p.eventName)}, ${p.schemaSource.raw}, {`,
501
+ ];
497
502
  if (p.version !== undefined) lines.push(` version: ${p.version},`);
498
- if (p.migrations !== undefined) {
499
- const entries = Object.entries(p.migrations);
500
- if (entries.length > 0) {
501
- lines.push(" migrations: {");
502
- for (const [fromVersion, transformBody] of entries) {
503
- lines.push(` "${fromVersion}": ${transformBody.raw},`);
504
- }
505
- lines.push(" },");
503
+ if (migrationEntries.length > 0) {
504
+ lines.push(" migrations: [");
505
+ for (const [fromVersion, transformBody] of migrationEntries) {
506
+ const from = Number(fromVersion);
507
+ lines.push(
508
+ ` { fromVersion: ${from}, toVersion: ${from + 1}, transform: ${transformBody.raw} },`,
509
+ );
506
510
  }
511
+ lines.push(" ],");
507
512
  }
508
513
  lines.push("});");
509
514
  return lines.join("\n");
@@ -32,7 +32,6 @@ import type {
32
32
  TranslationKeys,
33
33
  TreeActionDef,
34
34
  UiHints,
35
- UnmanagedTableEntry,
36
35
  ValidationHookFn,
37
36
  WriteHandlerDef,
38
37
  } from "./types";
@@ -88,7 +87,6 @@ export type FeatureBuilderState = {
88
87
  multiStreamProjections: Record<string, MultiStreamProjectionDefinition>;
89
88
  entityProjectionExtensions: Record<string, EntityProjectionExtension[]>;
90
89
  rawTables: Record<string, RawTableEntry>;
91
- unmanagedTables: Record<string, UnmanagedTableEntry>;
92
90
  authClaimsHooks: AuthClaimsFn[];
93
91
  claimKeys: Record<string, ClaimKeyDefinition>;
94
92
  screens: Record<string, ScreenDefinition>;
@@ -147,7 +145,6 @@ export function createInitialFeatureBuilderState(): FeatureBuilderState {
147
145
  multiStreamProjections: {},
148
146
  entityProjectionExtensions: {},
149
147
  rawTables: {},
150
- unmanagedTables: {},
151
148
  authClaimsHooks: [],
152
149
  claimKeys: {},
153
150
  screens: {},
@@ -1,6 +1,6 @@
1
1
  import { ZodObject, type ZodType, type z } from "zod";
2
2
  import type { FeatureBuilderState } from "./feature-builder-state";
3
- import { splitNamedDefinition } from "./object-form";
3
+ import { splitNamedDefinition, unwrapArrayForm } from "./object-form";
4
4
  import { QnTypes, qn, toKebab } from "./qualified-name";
5
5
  import type {
6
6
  AuthClaimsFn,
@@ -91,7 +91,7 @@ export function buildConfigEventsJobsMethods<TName extends string>(
91
91
  }
92
92
  }
93
93
  // Single-key call unwraps its own handle; multi-key returns the record.
94
- return typeof arg1 === "string" ? handles[arg1] : handles; // @cast-boundary engine-bridge — overload impl signature widens to unknown, narrowed by the two public overloads above
94
+ return typeof arg1 === "string" ? handles[arg1] : handles;
95
95
  }
96
96
 
97
97
  // piiFields misconfiguration is a boot-time error, not a silent
@@ -272,10 +272,7 @@ export function buildConfigEventsJobsMethods<TName extends string>(
272
272
  readsConfig(
273
273
  ...args: readonly [{ readonly keys: readonly string[] }] | readonly string[]
274
274
  ): void {
275
- const [first] = args;
276
- const qualifiedKeys =
277
- typeof first === "object" && first !== null ? first.keys : (args as readonly string[]);
278
- state.configReads.push(...qualifiedKeys);
275
+ state.configReads.push(...unwrapArrayForm(args, "keys"));
279
276
  },
280
277
  metric(
281
278
  shortNameOrDefinition: string | ({ readonly name: string } & MetricOptions),
@@ -1,6 +1,8 @@
1
1
  import type { ZodType, z } from "zod";
2
2
  import { toTableName } from "../db/table-builder";
3
3
  import type { QueryHandlerDefinition, WriteHandlerDefinition } from "./define-handler";
4
+ import type { RegisterEntityCrudOptions } from "./entity-handlers";
5
+ import { registerEntityCrud } from "./entity-handlers";
4
6
  import type { FeatureBuilderState } from "./feature-builder-state";
5
7
  import { splitNamedDefinition } from "./object-form";
6
8
  import type {
@@ -51,7 +53,7 @@ export function buildEntityHandlerMethods<TName extends string>(
51
53
  state: FeatureBuilderState,
52
54
  name: TName,
53
55
  ) {
54
- return {
56
+ const methods = {
55
57
  entity(
56
58
  nameOrDefinition: string | ({ readonly name: string } & EntityDefinition),
57
59
  definition?: EntityDefinition,
@@ -163,4 +165,10 @@ export function buildEntityHandlerMethods<TName extends string>(
163
165
  return { name: nameOrDef };
164
166
  },
165
167
  };
168
+ return {
169
+ ...methods,
170
+ crud(entityName: string, entity: EntityDefinition, options?: RegisterEntityCrudOptions): void {
171
+ registerEntityCrud(methods, entityName, entity, options);
172
+ },
173
+ };
166
174
  }