@cosmicdrift/kumiko-framework 0.155.1 → 0.156.1
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/README.md +9 -38
- package/package.json +2 -2
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/raw-table.integration.test.ts +17 -40
- package/src/crypto/__tests__/pii-field-encryption.test.ts +105 -1
- package/src/crypto/index.ts +1 -0
- package/src/crypto/pii-field-encryption.ts +19 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +27 -1
- package/src/db/__tests__/blind-index.integration.test.ts +16 -0
- package/src/db/__tests__/collect-table-metas.test.ts +6 -6
- package/src/db/__tests__/event-store-executor.integration.test.ts +75 -0
- package/src/db/__tests__/feature-table-sources.test.ts +7 -7
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.test.ts +39 -0
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/entity-table-meta.ts +1 -1
- package/src/db/event-store-executor-write.ts +23 -2
- package/src/db/event-store-executor.ts +1 -1
- package/src/db/feature-table-sources.ts +5 -11
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/queries/shadow-swap.ts +85 -3
- package/src/db/tenant-db.ts +8 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +102 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +16 -0
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/raw-table.test.ts +134 -84
- package/src/engine/__tests__/registry.test.ts +40 -0
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +52 -2
- package/src/engine/boot-validator/action-wiring.ts +2 -2
- package/src/engine/boot-validator/config-deps.ts +35 -0
- package/src/engine/boot-validator/index.ts +2 -0
- package/src/engine/boot-validator/pii-retention.ts +35 -3
- package/src/engine/build-config-feature-schema.ts +3 -3
- package/src/engine/config-helpers.ts +2 -0
- package/src/engine/define-feature.ts +2 -6
- package/src/engine/entity-handlers.ts +11 -5
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +170 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +31 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +6 -1
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +222 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +32 -24
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/parse.ts +3 -3
- package/src/engine/feature-ast/render.ts +16 -11
- package/src/engine/feature-builder-state.ts +0 -3
- package/src/engine/feature-config-events-jobs.ts +3 -6
- package/src/engine/feature-entity-handlers.ts +9 -1
- package/src/engine/feature-ui-extensions.ts +10 -40
- package/src/engine/field-access.ts +13 -2
- package/src/engine/object-form.ts +15 -0
- package/src/engine/registry-facade.ts +4 -0
- package/src/engine/registry-ingest.ts +19 -29
- package/src/engine/registry-state.ts +1 -7
- package/src/engine/registry-validate.ts +5 -28
- package/src/engine/registry.ts +0 -2
- package/src/engine/types/config.ts +7 -0
- package/src/engine/types/feature.ts +40 -68
- package/src/engine/types/fields.ts +6 -0
- package/src/engine/types/index.ts +0 -3
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/pipeline/__tests__/dispatcher.test.ts +29 -0
- package/src/pipeline/msp-rebuild.ts +4 -0
- package/src/pipeline/projection-rebuild.ts +35 -0
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +8 -1
- package/src/stack/test-stack.ts +17 -2
- package/src/ui-types/index.ts +1 -0
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -172
- 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
|
|
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
|
-
"
|
|
107
|
+
"rawTable",
|
|
108
108
|
sourceLocationFromNode(call, sourceFile),
|
|
109
|
-
"
|
|
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 "
|
|
472
|
-
return
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
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 (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|