@cosmicdrift/kumiko-framework 0.2.2 → 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.
- package/CHANGELOG.md +54 -0
- package/package.json +124 -38
- package/src/__tests__/full-stack.integration.ts +2 -2
- package/src/api/auth-routes.ts +5 -5
- package/src/api/jwt.ts +2 -2
- package/src/api/route-registrars.ts +1 -1
- package/src/api/routes.ts +3 -3
- package/src/api/server.ts +6 -7
- package/src/auth/__tests__/roles.test.ts +24 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/roles.ts +42 -0
- package/src/compliance/__tests__/duration-spec.test.ts +72 -0
- package/src/compliance/__tests__/profiles.test.ts +308 -0
- package/src/compliance/__tests__/sub-processors.test.ts +139 -0
- package/src/compliance/duration-spec.ts +44 -0
- package/src/compliance/index.ts +31 -0
- package/src/compliance/override-schema.ts +136 -0
- package/src/compliance/profiles.ts +427 -0
- package/src/compliance/sub-processors.ts +152 -0
- package/src/db/__tests__/big-int-field.test.ts +131 -0
- package/src/db/assert-exists-in.ts +2 -2
- package/src/db/cursor.ts +3 -3
- package/src/db/event-store-executor.ts +19 -13
- package/src/db/located-timestamp.ts +1 -1
- package/src/db/money.ts +12 -2
- package/src/db/pg-error.ts +1 -1
- package/src/db/row-helpers.ts +1 -1
- package/src/db/table-builder.ts +20 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -0
- package/src/engine/__tests__/build-target.test.ts +135 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +4 -4
- package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
- package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
- package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
- package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
- package/src/engine/__tests__/raw-table.test.ts +2 -2
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
- package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
- package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
- package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
- package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
- package/src/engine/__tests__/steps-read.test.ts +142 -0
- package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
- package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
- package/src/engine/__tests__/steps-workflow.test.ts +198 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
- package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
- package/src/engine/boot-validator/api-ext.ts +77 -0
- package/src/engine/boot-validator/config-deps.ts +163 -0
- package/src/engine/boot-validator/entity-handler.ts +466 -0
- package/src/engine/boot-validator/index.ts +159 -0
- package/src/engine/boot-validator/ownership.ts +198 -0
- package/src/engine/boot-validator/pii-retention.ts +155 -0
- package/src/engine/boot-validator/screens-nav.ts +624 -0
- package/src/engine/boot-validator.ts +1 -1528
- package/src/engine/build-app-schema.ts +1 -1
- package/src/engine/build-target.ts +99 -0
- package/src/engine/codemod/index.ts +15 -0
- package/src/engine/codemod/pipeline-codemod.ts +641 -0
- package/src/engine/config-helpers.ts +9 -19
- package/src/engine/constants.ts +1 -1
- package/src/engine/define-feature.ts +127 -9
- package/src/engine/define-handler.ts +89 -3
- package/src/engine/define-roles.ts +2 -2
- package/src/engine/define-step.ts +28 -0
- package/src/engine/define-workflow.ts +110 -0
- package/src/engine/entity-handlers.ts +10 -9
- package/src/engine/event-helpers.ts +4 -4
- package/src/engine/extension-names.ts +105 -0
- package/src/engine/extensions/user-data.ts +106 -0
- package/src/engine/factories.ts +26 -16
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
- package/src/engine/feature-ast/extractors/index.ts +74 -0
- package/src/engine/feature-ast/extractors/round1.ts +110 -0
- package/src/engine/feature-ast/extractors/round2.ts +253 -0
- package/src/engine/feature-ast/extractors/round3.ts +471 -0
- package/src/engine/feature-ast/extractors/round4.ts +1365 -0
- package/src/engine/feature-ast/extractors/round5.ts +72 -0
- package/src/engine/feature-ast/extractors/round6.ts +66 -0
- package/src/engine/feature-ast/extractors/shared.ts +177 -0
- package/src/engine/feature-ast/parse.ts +13 -0
- package/src/engine/feature-ast/patch.ts +9 -1
- package/src/engine/feature-ast/patcher.ts +10 -3
- package/src/engine/feature-ast/patterns.ts +71 -1
- package/src/engine/feature-ast/render.ts +31 -1
- package/src/engine/index.ts +66 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
- package/src/engine/pattern-library/library.ts +78 -2
- package/src/engine/pipeline.ts +88 -0
- package/src/engine/projection-helpers.ts +1 -1
- package/src/engine/read-claim.ts +1 -1
- package/src/engine/registry.ts +30 -2
- package/src/engine/resolve-config-or-param.ts +4 -0
- package/src/engine/run-pipeline.ts +162 -0
- package/src/engine/schema-builder.ts +10 -4
- package/src/engine/state-machine.ts +1 -1
- package/src/engine/steps/_drizzle-boundary.ts +19 -0
- package/src/engine/steps/_duration-utils.ts +33 -0
- package/src/engine/steps/_no-return-guard.ts +21 -0
- package/src/engine/steps/_resolver-utils.ts +42 -0
- package/src/engine/steps/_step-dispatch-constants.ts +38 -0
- package/src/engine/steps/aggregate-append-event.ts +56 -0
- package/src/engine/steps/aggregate-create.ts +56 -0
- package/src/engine/steps/aggregate-update.ts +68 -0
- package/src/engine/steps/branch.ts +84 -0
- package/src/engine/steps/call-feature.ts +49 -0
- package/src/engine/steps/compute.ts +41 -0
- package/src/engine/steps/for-each.ts +111 -0
- package/src/engine/steps/mail-send.ts +44 -0
- package/src/engine/steps/read-find-many.ts +51 -0
- package/src/engine/steps/read-find-one.ts +58 -0
- package/src/engine/steps/retry.ts +87 -0
- package/src/engine/steps/return.ts +34 -0
- package/src/engine/steps/unsafe-projection-delete.ts +46 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
- package/src/engine/steps/wait-for-event.ts +71 -0
- package/src/engine/steps/wait.ts +69 -0
- package/src/engine/steps/webhook-send.ts +71 -0
- package/src/engine/system-user.ts +1 -1
- package/src/engine/types/feature.ts +143 -1
- package/src/engine/types/fields.ts +134 -10
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/identifiers.ts +1 -0
- package/src/engine/types/index.ts +15 -1
- package/src/engine/types/step.ts +334 -0
- package/src/engine/types/target-ref.ts +21 -0
- package/src/engine/types/tree-node.ts +130 -0
- package/src/engine/types/workspace.ts +7 -0
- package/src/engine/validate-projection-allowlist.ts +161 -0
- package/src/event-store/snapshot.ts +1 -1
- package/src/event-store/upcaster-dead-letter.ts +1 -1
- package/src/event-store/upcaster.ts +1 -1
- package/src/files/__tests__/read-stream.test.ts +105 -0
- package/src/files/__tests__/write-stream.test.ts +233 -0
- package/src/files/__tests__/zip-stream.test.ts +357 -0
- package/src/files/file-routes.ts +1 -1
- package/src/files/in-memory-provider.ts +38 -0
- package/src/files/index.ts +3 -0
- package/src/files/local-provider.ts +58 -1
- package/src/files/types.ts +36 -8
- package/src/files/zip-stream.ts +251 -0
- package/src/jobs/job-runner.ts +10 -10
- package/src/lifecycle/lifecycle.ts +0 -3
- package/src/logging/index.ts +1 -0
- package/src/logging/pino-logger.ts +11 -7
- package/src/logging/utils.ts +24 -0
- package/src/observability/prometheus-meter.ts +7 -5
- package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
- package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
- package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
- package/src/pipeline/append-event-core.ts +22 -6
- package/src/pipeline/dispatcher-utils.ts +188 -0
- package/src/pipeline/dispatcher.ts +63 -283
- package/src/pipeline/distributed-lock.ts +1 -1
- package/src/pipeline/entity-cache.ts +2 -2
- package/src/pipeline/event-consumer-state.ts +0 -13
- package/src/pipeline/event-dispatcher.ts +4 -4
- package/src/pipeline/index.ts +0 -2
- package/src/pipeline/lifecycle-pipeline.ts +6 -12
- package/src/pipeline/msp-rebuild.ts +5 -5
- package/src/pipeline/multi-stream-apply-context.ts +6 -7
- package/src/pipeline/projection-rebuild.ts +2 -2
- package/src/pipeline/projection-state.ts +0 -12
- package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
- package/src/rate-limit/resolver.ts +1 -1
- package/src/search/in-memory-adapter.ts +1 -1
- package/src/search/meilisearch-adapter.ts +3 -3
- package/src/search/types.ts +1 -1
- package/src/secrets/leak-guard.ts +2 -2
- package/src/stack/request-helper.ts +9 -5
- package/src/stack/test-stack.ts +1 -1
- package/src/testing/handler-context.ts +4 -4
- package/src/testing/http-cookies.ts +1 -1
- package/src/time/tz-context.ts +1 -2
- package/src/ui-types/index.ts +4 -0
- package/src/engine/feature-ast/extractors.ts +0 -2562
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noConsole: CLI tool — console.log is the output
|
|
2
|
+
// biome-ignore-all lint/style/noNonNullAssertion: regex capture-group indexing — match-success implies non-null capture
|
|
3
|
+
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { Glob } from "bun";
|
|
7
|
+
import { type CallExpression, Project, type SourceFile, SyntaxKind } from "ts-morph";
|
|
8
|
+
|
|
9
|
+
export type CodemodResult = {
|
|
10
|
+
readonly filePath: string;
|
|
11
|
+
readonly status: "converted" | "skipped" | "error";
|
|
12
|
+
readonly reason?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type CodemodReport = {
|
|
16
|
+
readonly results: readonly CodemodResult[];
|
|
17
|
+
readonly converted: number;
|
|
18
|
+
readonly skipped: number;
|
|
19
|
+
readonly errors: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type CodemodOptions = {
|
|
23
|
+
readonly dryRun?: boolean;
|
|
24
|
+
readonly verbose?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type GuardConfig = {
|
|
28
|
+
readonly condition: string;
|
|
29
|
+
readonly failureReturn: string;
|
|
30
|
+
readonly successReturn: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type ParsedHandlerInfo = {
|
|
34
|
+
readonly handlerBodyText: string;
|
|
35
|
+
readonly isStaticReturn: boolean;
|
|
36
|
+
readonly isSimpleExecutorCreate: boolean;
|
|
37
|
+
readonly isSimpleExecutorUpdate: boolean;
|
|
38
|
+
readonly hasConditionalLogic: boolean;
|
|
39
|
+
readonly executorName?: string;
|
|
40
|
+
readonly executorCreateVar?: string;
|
|
41
|
+
readonly executorCreateArgs?: string[];
|
|
42
|
+
readonly executorUpdateVar?: string;
|
|
43
|
+
readonly isExpressionBodyCreate: boolean;
|
|
44
|
+
readonly isExpressionBodyUpdate: boolean;
|
|
45
|
+
readonly expressionBodyArgs: string[] | undefined;
|
|
46
|
+
readonly isGuardedCreate: boolean;
|
|
47
|
+
readonly guardConfig: GuardConfig | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export function analyzeHandlerArrow(arrowText: string): ParsedHandlerInfo {
|
|
51
|
+
const text = arrowText.trim();
|
|
52
|
+
|
|
53
|
+
let handlerBodyText: string;
|
|
54
|
+
let isStaticReturn = false;
|
|
55
|
+
let isSimpleExecutorCreate = false;
|
|
56
|
+
let isSimpleExecutorUpdate = false;
|
|
57
|
+
let hasConditionalLogic = false;
|
|
58
|
+
let executorName: string | undefined;
|
|
59
|
+
let executorCreateVar: string | undefined;
|
|
60
|
+
let executorCreateArgs: string[] | undefined;
|
|
61
|
+
let executorUpdateVar: string | undefined;
|
|
62
|
+
let isExpressionBodyCreate = false;
|
|
63
|
+
let isExpressionBodyUpdate = false;
|
|
64
|
+
let expressionBodyArgs: string[] | undefined;
|
|
65
|
+
let isGuardedCreate = false;
|
|
66
|
+
let guardConfig: GuardConfig | undefined;
|
|
67
|
+
|
|
68
|
+
if (text.startsWith("async (")) {
|
|
69
|
+
const arrowIdx = text.indexOf("=>");
|
|
70
|
+
if (arrowIdx === -1) {
|
|
71
|
+
return {
|
|
72
|
+
handlerBodyText: text,
|
|
73
|
+
isStaticReturn: false,
|
|
74
|
+
isSimpleExecutorCreate: false,
|
|
75
|
+
isSimpleExecutorUpdate: false,
|
|
76
|
+
hasConditionalLogic: false,
|
|
77
|
+
isExpressionBodyCreate: false,
|
|
78
|
+
isExpressionBodyUpdate: false,
|
|
79
|
+
expressionBodyArgs: undefined,
|
|
80
|
+
isGuardedCreate: false,
|
|
81
|
+
guardConfig: undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const afterArrow = text.slice(arrowIdx + 2).trim();
|
|
85
|
+
|
|
86
|
+
if (afterArrow.startsWith("(")) {
|
|
87
|
+
isStaticReturn = true;
|
|
88
|
+
handlerBodyText = afterArrow;
|
|
89
|
+
} else if (afterArrow.startsWith("{")) {
|
|
90
|
+
handlerBodyText = afterArrow;
|
|
91
|
+
|
|
92
|
+
const lines = handlerBodyText
|
|
93
|
+
.split("\n")
|
|
94
|
+
.map((l) => l.trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
|
|
97
|
+
// Detect conditional logic (if/try/for/while/catch)
|
|
98
|
+
hasConditionalLogic = lines.some(
|
|
99
|
+
(l) =>
|
|
100
|
+
l.startsWith("if ") ||
|
|
101
|
+
l.startsWith("try ") ||
|
|
102
|
+
l.startsWith("for ") ||
|
|
103
|
+
l.startsWith("while ") ||
|
|
104
|
+
l.startsWith("} else"),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const createMatch = handlerBodyText.match(/const\s+(\w+)\s*=\s*await\s+(\w+)\.create\s*\(/);
|
|
108
|
+
const updateMatch = handlerBodyText.match(/const\s+(\w+)\s*=\s*await\s+(\w+)\.update\s*\(/);
|
|
109
|
+
const hasSimpleReturn = /return\s*\{[^}]*isSuccess:\s*true/.test(handlerBodyText);
|
|
110
|
+
|
|
111
|
+
if (createMatch && hasSimpleReturn && !hasConditionalLogic) {
|
|
112
|
+
isSimpleExecutorCreate = true;
|
|
113
|
+
executorName = createMatch[2];
|
|
114
|
+
executorCreateVar = createMatch[1];
|
|
115
|
+
executorCreateArgs = extractArgs(handlerBodyText, createMatch[2] as string, "create");
|
|
116
|
+
} else if (createMatch && hasSimpleReturn && hasConditionalLogic) {
|
|
117
|
+
// Try to match the guard pattern: if (!<var>.isSuccess) { return fail } return success
|
|
118
|
+
const guard = extractGuardedPattern(handlerBodyText, createMatch[1] as string);
|
|
119
|
+
if (guard) {
|
|
120
|
+
isGuardedCreate = true;
|
|
121
|
+
guardConfig = guard;
|
|
122
|
+
executorName = createMatch[2];
|
|
123
|
+
executorCreateVar = createMatch[1];
|
|
124
|
+
executorCreateArgs = extractArgs(handlerBodyText, createMatch[2] as string, "create");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
updateMatch &&
|
|
130
|
+
hasSimpleReturn &&
|
|
131
|
+
!hasConditionalLogic &&
|
|
132
|
+
!isSimpleExecutorCreate &&
|
|
133
|
+
!isGuardedCreate
|
|
134
|
+
) {
|
|
135
|
+
isSimpleExecutorUpdate = true;
|
|
136
|
+
executorName = updateMatch[2];
|
|
137
|
+
executorUpdateVar = updateMatch[1];
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
handlerBodyText = afterArrow;
|
|
141
|
+
// Expression body — check for member-call patterns like
|
|
142
|
+
// `crud.create(event.payload)` or `crud.update({ id, changes })`.
|
|
143
|
+
const exprCallMatch = afterArrow.match(/^(\w+)\.(create|update)\s*\(/);
|
|
144
|
+
if (exprCallMatch) {
|
|
145
|
+
executorName = exprCallMatch[1]!;
|
|
146
|
+
const method = exprCallMatch[2]!;
|
|
147
|
+
expressionBodyArgs = extractExprArgs(afterArrow, executorName, method);
|
|
148
|
+
if (method === "create") {
|
|
149
|
+
isExpressionBodyCreate = true;
|
|
150
|
+
} else {
|
|
151
|
+
isExpressionBodyUpdate = true;
|
|
152
|
+
}
|
|
153
|
+
} else if (afterArrow.startsWith("(") && afterArrow.endsWith(")")) {
|
|
154
|
+
isStaticReturn = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
handlerBodyText = text;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
handlerBodyText,
|
|
163
|
+
isStaticReturn,
|
|
164
|
+
isSimpleExecutorCreate,
|
|
165
|
+
isSimpleExecutorUpdate,
|
|
166
|
+
hasConditionalLogic,
|
|
167
|
+
executorName,
|
|
168
|
+
executorCreateVar,
|
|
169
|
+
executorCreateArgs,
|
|
170
|
+
executorUpdateVar,
|
|
171
|
+
isExpressionBodyCreate,
|
|
172
|
+
isExpressionBodyUpdate,
|
|
173
|
+
expressionBodyArgs,
|
|
174
|
+
isGuardedCreate,
|
|
175
|
+
guardConfig,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function extractArgs(body: string, executorVar: string, method: string): string[] | undefined {
|
|
180
|
+
const pattern = new RegExp(
|
|
181
|
+
`const\\s+\\w+\\s*=\\s*await\\s+${escapeRegex(executorVar)}\\.${method}\\s*\\(`,
|
|
182
|
+
);
|
|
183
|
+
const match = body.match(pattern);
|
|
184
|
+
if (!match) return undefined;
|
|
185
|
+
|
|
186
|
+
const startIdx = (match.index ?? 0) + match[0].length;
|
|
187
|
+
let depth = 1;
|
|
188
|
+
let endIdx = startIdx;
|
|
189
|
+
while (endIdx < body.length && depth > 0) {
|
|
190
|
+
if (body[endIdx] === "(") depth++;
|
|
191
|
+
else if (body[endIdx] === ")") depth--;
|
|
192
|
+
if (depth > 0) endIdx++;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const argsStr = body.slice(startIdx, endIdx);
|
|
196
|
+
return splitTopLevel(argsStr);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function escapeRegex(str: string): string {
|
|
200
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function splitTopLevel(args: string): string[] {
|
|
204
|
+
const result: string[] = [];
|
|
205
|
+
let depth = 0;
|
|
206
|
+
let current = "";
|
|
207
|
+
for (const ch of args) {
|
|
208
|
+
if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
209
|
+
else if (ch === ")" || ch === "}" || ch === "]") depth--;
|
|
210
|
+
if (ch === "," && depth === 0) {
|
|
211
|
+
result.push(current.trim());
|
|
212
|
+
current = "";
|
|
213
|
+
} else {
|
|
214
|
+
current += ch;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (current.trim()) result.push(current.trim());
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractExprArgs(body: string, executorVar: string, method: string): string[] | undefined {
|
|
222
|
+
const escaped = executorVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
223
|
+
const pattern = new RegExp(`${escaped}\\.${method}\\s*\\(`);
|
|
224
|
+
const match = body.match(pattern);
|
|
225
|
+
if (!match) return undefined;
|
|
226
|
+
|
|
227
|
+
const startIdx = (match.index ?? 0) + match[0].length;
|
|
228
|
+
let depth = 1;
|
|
229
|
+
let endIdx = startIdx;
|
|
230
|
+
while (endIdx < body.length && depth > 0) {
|
|
231
|
+
if (body[endIdx] === "(") depth++;
|
|
232
|
+
else if (body[endIdx] === ")") depth--;
|
|
233
|
+
if (depth > 0) endIdx++;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const argsStr = body.slice(startIdx, endIdx);
|
|
237
|
+
return splitTopLevel(argsStr);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function transpileEventRefs(argsStr: string): string {
|
|
241
|
+
// Transform `event.<prop>` → `ctx.event.<prop>` inside a resolver fn.
|
|
242
|
+
// The original handler has `event` as a parameter; inside the pipeline
|
|
243
|
+
// resolver, `event` lives on `ctx`, not as a standalone binding.
|
|
244
|
+
// `ctx` references stay as-is (the resolver also receives ctx).
|
|
245
|
+
return argsStr.replace(/\bevent\./g, "ctx.event.");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function transpileGuardRefs(body: string, varName: string): string {
|
|
249
|
+
// Transform `<var>` → `ctx.steps.result` in guard body text.
|
|
250
|
+
// The original handler references the executor result via
|
|
251
|
+
// `result.isSuccess`, `result.error`, `result.data`, or standalone
|
|
252
|
+
// `return result`. Inside the compute step, executor result lives at
|
|
253
|
+
// `ctx.steps.result`. `\b` word boundary ensures we only match the
|
|
254
|
+
// exact variable name.
|
|
255
|
+
return body.replace(new RegExp(`\\b${escapeRegex(varName)}\\b`, "g"), "ctx.steps.result");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function extractGuardedPattern(body: string, varName: string): GuardConfig | undefined {
|
|
259
|
+
const escaped = escapeRegex(varName);
|
|
260
|
+
// Match: if (!<varName>.isSuccess) { return ... }
|
|
261
|
+
const ifOpenPattern = new RegExp(`if\\s*\\(!${escaped}\\.isSuccess\\)\\s*\\{`);
|
|
262
|
+
const ifOpenMatch = body.match(ifOpenPattern);
|
|
263
|
+
if (!ifOpenMatch) return undefined;
|
|
264
|
+
|
|
265
|
+
// Extract condition
|
|
266
|
+
const condPattern = new RegExp(`if\\s*\\((!${escaped}\\.isSuccess)\\)`);
|
|
267
|
+
const condMatch = body.match(condPattern);
|
|
268
|
+
if (!condMatch) return undefined;
|
|
269
|
+
const condition = condMatch[1]!;
|
|
270
|
+
|
|
271
|
+
// Extract if body (between { and matching })
|
|
272
|
+
const ifBodyStart = (ifOpenMatch.index ?? 0) + ifOpenMatch[0].length;
|
|
273
|
+
let depth = 1;
|
|
274
|
+
let ifBodyEnd = ifBodyStart;
|
|
275
|
+
while (ifBodyEnd < body.length && depth > 0) {
|
|
276
|
+
if (body[ifBodyEnd] === "{") depth++;
|
|
277
|
+
else if (body[ifBodyEnd] === "}") depth--;
|
|
278
|
+
if (depth > 0) ifBodyEnd++;
|
|
279
|
+
}
|
|
280
|
+
const ifBodyContent = body.slice(ifBodyStart, ifBodyEnd).trim();
|
|
281
|
+
const failRetMatch = ifBodyContent.match(/return\s*([^;]+)\s*;/);
|
|
282
|
+
if (!failRetMatch) return undefined;
|
|
283
|
+
const failureReturn = failRetMatch[1]!.trim();
|
|
284
|
+
|
|
285
|
+
// Extract trailing return after the if block
|
|
286
|
+
const afterIf = body.slice(ifBodyEnd + 1).trim();
|
|
287
|
+
const succRetMatch = afterIf.match(/return\s*([^;]+)\s*;/);
|
|
288
|
+
if (!succRetMatch) return undefined;
|
|
289
|
+
const successReturn = succRetMatch[1]!.trim();
|
|
290
|
+
|
|
291
|
+
return { condition, failureReturn, successReturn };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function generatePerformBlock(
|
|
295
|
+
analysis: ParsedHandlerInfo,
|
|
296
|
+
schemaType: string,
|
|
297
|
+
indent: string,
|
|
298
|
+
): string | null {
|
|
299
|
+
const steps: string[] = [];
|
|
300
|
+
|
|
301
|
+
if (analysis.isStaticReturn) {
|
|
302
|
+
let body = analysis.handlerBodyText.trim();
|
|
303
|
+
if (body.startsWith("(") && body.endsWith(")")) {
|
|
304
|
+
body = body.slice(1, -1);
|
|
305
|
+
}
|
|
306
|
+
steps.push(`r.step.return((ctx) => (${body}))`);
|
|
307
|
+
} else if (
|
|
308
|
+
analysis.isSimpleExecutorCreate &&
|
|
309
|
+
analysis.executorName &&
|
|
310
|
+
analysis.executorCreateArgs
|
|
311
|
+
) {
|
|
312
|
+
const dataArg = analysis.executorCreateArgs[0] ?? "{}";
|
|
313
|
+
steps.push(
|
|
314
|
+
`r.step.aggregate.create("result", { executor: ${analysis.executorName}, data: (ctx) => ${dataArg} })`,
|
|
315
|
+
);
|
|
316
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
317
|
+
} else if (analysis.isSimpleExecutorUpdate && analysis.executorName) {
|
|
318
|
+
steps.push(
|
|
319
|
+
`r.step.aggregate.update("result", { executor: ${analysis.executorName}, id: event.payload.id, changes: event.payload.changes })`,
|
|
320
|
+
);
|
|
321
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
322
|
+
} else if (
|
|
323
|
+
analysis.isExpressionBodyCreate &&
|
|
324
|
+
analysis.executorName &&
|
|
325
|
+
analysis.expressionBodyArgs
|
|
326
|
+
) {
|
|
327
|
+
const dataArg = transpileEventRefs(analysis.expressionBodyArgs[0] ?? "{}");
|
|
328
|
+
steps.push(
|
|
329
|
+
`r.step.aggregate.create("result", { executor: ${analysis.executorName}, data: (ctx) => ${dataArg} })`,
|
|
330
|
+
);
|
|
331
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
332
|
+
} else if (
|
|
333
|
+
analysis.isExpressionBodyUpdate &&
|
|
334
|
+
analysis.executorName &&
|
|
335
|
+
analysis.expressionBodyArgs
|
|
336
|
+
) {
|
|
337
|
+
const argStr = transpileEventRefs(analysis.expressionBodyArgs[0] ?? "{}");
|
|
338
|
+
steps.push(`r.step.compute("result", (ctx) => ${analysis.executorName}.update(${argStr}))`);
|
|
339
|
+
steps.push(`r.step.return((ctx) => ({ isSuccess: true, data: ctx.steps.result }))`);
|
|
340
|
+
} else if (
|
|
341
|
+
analysis.isGuardedCreate &&
|
|
342
|
+
analysis.executorName &&
|
|
343
|
+
analysis.executorCreateArgs &&
|
|
344
|
+
analysis.guardConfig
|
|
345
|
+
) {
|
|
346
|
+
const varName = analysis.executorCreateVar ?? "result";
|
|
347
|
+
const dataArg = analysis.executorCreateArgs[0] ?? "{}";
|
|
348
|
+
const guard = analysis.guardConfig;
|
|
349
|
+
const condition = guard.condition; // already references varName
|
|
350
|
+
const failureReturn = transpileGuardRefs(guard.failureReturn, varName);
|
|
351
|
+
const successReturn = transpileGuardRefs(guard.successReturn, varName);
|
|
352
|
+
// The condition references the original var name — transpile it too
|
|
353
|
+
const condTranspiled = transpileGuardRefs(condition, varName);
|
|
354
|
+
steps.push(
|
|
355
|
+
`r.step.aggregate.create("result", { executor: ${analysis.executorName}, data: (ctx) => ${dataArg} })`,
|
|
356
|
+
);
|
|
357
|
+
steps.push(`r.step.compute("outcome", (ctx) => {
|
|
358
|
+
if (${condTranspiled}) {
|
|
359
|
+
return ${failureReturn};
|
|
360
|
+
}
|
|
361
|
+
return ${successReturn};
|
|
362
|
+
})`);
|
|
363
|
+
steps.push(`r.step.return((ctx) => ctx.steps.outcome)`);
|
|
364
|
+
} else {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const stepIndent = indent;
|
|
369
|
+
const stepsStr = steps.map((s) => `${stepIndent} ${s}`).join(",\n");
|
|
370
|
+
const pipelineType = schemaType ? `<${schemaType}, unknown>` : "";
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
`perform: pipeline${pipelineType}(({ event, r }) => [`,
|
|
374
|
+
stepsStr,
|
|
375
|
+
` ${stepIndent}]),`,
|
|
376
|
+
].join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function inferSchemaType(objLiteral: import("ts-morph").ObjectLiteralExpression): string {
|
|
380
|
+
const schemaProp = objLiteral.getProperty("schema");
|
|
381
|
+
if (!schemaProp) return "unknown";
|
|
382
|
+
const init = (schemaProp as import("ts-morph").PropertyAssignment).getInitializer();
|
|
383
|
+
if (!init) return "unknown";
|
|
384
|
+
const text = init.getText();
|
|
385
|
+
// Only return simple names (like `InvoiceSchema`), not inline definitions
|
|
386
|
+
if (/^[A-Za-z_$][\w$.]*$/.test(text)) {
|
|
387
|
+
return `typeof ${text}`;
|
|
388
|
+
}
|
|
389
|
+
return ""; // Empty = omit type params
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function findFiles(rootDir: string): string[] {
|
|
393
|
+
const glob = new Glob("**/*.write.ts");
|
|
394
|
+
const files: string[] = [];
|
|
395
|
+
for (const file of glob.scanSync(rootDir)) {
|
|
396
|
+
files.push(join(rootDir, file));
|
|
397
|
+
}
|
|
398
|
+
return files.sort();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function scanForCandidates(rootDir: string): FileAnalysis[] {
|
|
402
|
+
const files = findFiles(rootDir);
|
|
403
|
+
const results: FileAnalysis[] = [];
|
|
404
|
+
|
|
405
|
+
for (const file of files) {
|
|
406
|
+
const analysis = analyzeFile(file);
|
|
407
|
+
if (analysis) results.push(analysis);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return results;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export type FileAnalysis = {
|
|
414
|
+
readonly filePath: string;
|
|
415
|
+
readonly pattern: "free-form-write" | "pipeline-write" | "query-handler" | "other";
|
|
416
|
+
readonly convertible: boolean;
|
|
417
|
+
readonly reason: string;
|
|
418
|
+
readonly handlerLine?: number;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export function analyzeFile(filePath: string): FileAnalysis | null {
|
|
422
|
+
if (!existsSync(filePath)) return null;
|
|
423
|
+
try {
|
|
424
|
+
const content = readFileSync(filePath, "utf8");
|
|
425
|
+
if (!content.includes("defineWriteHandler") && !content.includes("defineQueryHandler")) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (content.includes("defineWriteHandler") && content.includes("perform:")) {
|
|
430
|
+
return {
|
|
431
|
+
filePath,
|
|
432
|
+
pattern: "pipeline-write",
|
|
433
|
+
convertible: false,
|
|
434
|
+
reason: "already_uses_pipeline_form",
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (content.includes("defineWriteHandler")) {
|
|
439
|
+
return {
|
|
440
|
+
filePath,
|
|
441
|
+
pattern: "free-form-write",
|
|
442
|
+
convertible: true,
|
|
443
|
+
reason: "free_form_write_handler",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (content.includes("defineQueryHandler")) {
|
|
448
|
+
return {
|
|
449
|
+
filePath,
|
|
450
|
+
pattern: "query-handler",
|
|
451
|
+
convertible: false,
|
|
452
|
+
reason: "query_handlers_not_convertible",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return null;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function runCodemod(
|
|
463
|
+
rootDir: string,
|
|
464
|
+
options: CodemodOptions = {},
|
|
465
|
+
): Promise<CodemodReport> {
|
|
466
|
+
const project = new Project({
|
|
467
|
+
skipAddingFilesFromTsConfig: true,
|
|
468
|
+
skipFileDependencyResolution: true,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const candidates = scanForCandidates(rootDir);
|
|
472
|
+
const writeHandlers = candidates.filter((c) => c.pattern === "free-form-write");
|
|
473
|
+
|
|
474
|
+
console.log(`\n Scanning ${rootDir}...`);
|
|
475
|
+
console.log(` Found ${writeHandlers.length} file(s) with free-form write handlers.\n`);
|
|
476
|
+
|
|
477
|
+
if (writeHandlers.length === 0) {
|
|
478
|
+
return { results: [], converted: 0, skipped: 0, errors: 0 };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const results: CodemodResult[] = [];
|
|
482
|
+
|
|
483
|
+
for (const candidate of writeHandlers) {
|
|
484
|
+
const result = await convertFile(candidate.filePath, project, options);
|
|
485
|
+
results.push(result);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const converted = results.filter((r) => r.status === "converted").length;
|
|
489
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
490
|
+
const errors = results.filter((r) => r.status === "error").length;
|
|
491
|
+
|
|
492
|
+
console.log(`\n Results: ${converted} converted, ${skipped} skipped, ${errors} errors\n`);
|
|
493
|
+
|
|
494
|
+
if (options.verbose) {
|
|
495
|
+
for (const r of results) {
|
|
496
|
+
if (r.status === "error") {
|
|
497
|
+
console.log(` ✗ ${relative(process.cwd(), r.filePath)}: ${r.reason}`);
|
|
498
|
+
} else if (r.status === "converted") {
|
|
499
|
+
console.log(` ✓ ${relative(process.cwd(), r.filePath)}`);
|
|
500
|
+
} else {
|
|
501
|
+
console.log(` - ${relative(process.cwd(), r.filePath)}: ${r.reason}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { results, converted, skipped, errors };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function contentHasPipelineImport(content: string): boolean {
|
|
510
|
+
// Check the actual import line, not later usage of the word `pipeline`
|
|
511
|
+
// in the perform block. Single-line import matching only (standard
|
|
512
|
+
// formatting in this codebase — never multi-line).
|
|
513
|
+
const importLine = content
|
|
514
|
+
.split("\n")
|
|
515
|
+
.find((l) => l.includes("import") && l.includes("@cosmicdrift/kumiko-framework/engine"));
|
|
516
|
+
return !!importLine && importLine.includes("pipeline");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function ensurePipelineImport(content: string): string | null {
|
|
520
|
+
if (contentHasPipelineImport(content)) return null;
|
|
521
|
+
// Single-line regex is safe — imports in this codebase are always
|
|
522
|
+
// `import { ... } from "@cosmicdrift/kumiko-framework/engine"`.
|
|
523
|
+
const importRegex =
|
|
524
|
+
/import\s*\{([^}]*)\}\s*from\s*["']@cosmicdrift\/kumiko-framework\/engine["']/;
|
|
525
|
+
const match = content.match(importRegex);
|
|
526
|
+
if (match) {
|
|
527
|
+
const existingImports = (match[1] as string).trim();
|
|
528
|
+
const newImports = existingImports ? `${existingImports}, pipeline` : "pipeline";
|
|
529
|
+
return content.replace(
|
|
530
|
+
importRegex,
|
|
531
|
+
`import { ${newImports} } from "@cosmicdrift/kumiko-framework/engine"`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export async function convertFile(
|
|
539
|
+
filePath: string,
|
|
540
|
+
project?: Project,
|
|
541
|
+
options: CodemodOptions = {},
|
|
542
|
+
): Promise<CodemodResult> {
|
|
543
|
+
try {
|
|
544
|
+
const contentBefore = readFileSync(filePath, "utf8");
|
|
545
|
+
const proj =
|
|
546
|
+
project ??
|
|
547
|
+
new Project({
|
|
548
|
+
skipAddingFilesFromTsConfig: true,
|
|
549
|
+
skipFileDependencyResolution: true,
|
|
550
|
+
});
|
|
551
|
+
const sourceFile = proj.addSourceFileAtPath(filePath);
|
|
552
|
+
|
|
553
|
+
const handlerCalls = findDefineWriteHandlerCalls(sourceFile);
|
|
554
|
+
if (handlerCalls.length === 0) {
|
|
555
|
+
return { filePath, status: "skipped", reason: "no_define_write_handler_calls" };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let content = contentBefore;
|
|
559
|
+
let hadChanges = false;
|
|
560
|
+
|
|
561
|
+
for (const call of handlerCalls) {
|
|
562
|
+
const arg = call.getArguments()[0];
|
|
563
|
+
if (!arg || arg.getKind() !== SyntaxKind.ObjectLiteralExpression) {
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const objLiteral = arg.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
568
|
+
if (!objLiteral) continue;
|
|
569
|
+
|
|
570
|
+
const handlerProp = objLiteral.getProperty("handler");
|
|
571
|
+
if (!handlerProp) continue;
|
|
572
|
+
|
|
573
|
+
const propAssign = handlerProp.asKind(SyntaxKind.PropertyAssignment);
|
|
574
|
+
if (!propAssign) continue;
|
|
575
|
+
|
|
576
|
+
const handlerInit = propAssign.getInitializer();
|
|
577
|
+
if (!handlerInit) continue;
|
|
578
|
+
|
|
579
|
+
const analysis = analyzeHandlerArrow(handlerInit.getText());
|
|
580
|
+
if (
|
|
581
|
+
!analysis.isStaticReturn &&
|
|
582
|
+
!analysis.isSimpleExecutorCreate &&
|
|
583
|
+
!analysis.isSimpleExecutorUpdate &&
|
|
584
|
+
!analysis.isExpressionBodyCreate &&
|
|
585
|
+
!analysis.isExpressionBodyUpdate &&
|
|
586
|
+
!analysis.isGuardedCreate
|
|
587
|
+
) {
|
|
588
|
+
if (options.verbose) {
|
|
589
|
+
console.log(` ~ ${relative(process.cwd(), filePath)}: non-trivial handler, skipping`);
|
|
590
|
+
}
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const schemaType = inferSchemaType(objLiteral);
|
|
595
|
+
const indent = propAssign.getIndentationText() ?? " ";
|
|
596
|
+
const performBlock = generatePerformBlock(analysis, schemaType, indent);
|
|
597
|
+
if (!performBlock) continue;
|
|
598
|
+
|
|
599
|
+
const start = propAssign.getStart();
|
|
600
|
+
let end = propAssign.getEnd();
|
|
601
|
+
// Consume trailing comma after the property assignment
|
|
602
|
+
if (end < content.length && content[end] === ",") {
|
|
603
|
+
end++;
|
|
604
|
+
}
|
|
605
|
+
content = content.slice(0, start) + performBlock + content.slice(end);
|
|
606
|
+
hadChanges = true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (hadChanges) {
|
|
610
|
+
const importResult = ensurePipelineImport(content);
|
|
611
|
+
if (importResult) {
|
|
612
|
+
content = importResult;
|
|
613
|
+
} else if (options.verbose) {
|
|
614
|
+
console.log(` ~ ${filePath}: pipeline import not needed or already present`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (hadChanges && !options.dryRun) {
|
|
619
|
+
writeFileSync(filePath, content, "utf8");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const status = hadChanges ? "converted" : "skipped";
|
|
623
|
+
const reason = hadChanges
|
|
624
|
+
? "handler replaced with perform: pipeline(...)"
|
|
625
|
+
: "no convertible handler";
|
|
626
|
+
return { filePath, status, reason };
|
|
627
|
+
} catch (err) {
|
|
628
|
+
return {
|
|
629
|
+
filePath,
|
|
630
|
+
status: "error",
|
|
631
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function findDefineWriteHandlerCalls(sourceFile: SourceFile): CallExpression[] {
|
|
637
|
+
return sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
638
|
+
const expr = call.getExpression();
|
|
639
|
+
return expr.getText() === "defineWriteHandler";
|
|
640
|
+
});
|
|
641
|
+
}
|
|
@@ -10,23 +10,13 @@ import type {
|
|
|
10
10
|
// --- Access Presets ---
|
|
11
11
|
|
|
12
12
|
export const access = {
|
|
13
|
-
all: ["all"] as readonly string[],
|
|
14
|
-
admin: ["Admin", "SystemAdmin"] as readonly string[],
|
|
15
|
-
systemAdmin: ["SystemAdmin"] as readonly string[],
|
|
16
|
-
system: ["system"] as readonly string[],
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
privileged: ["system", "SystemAdmin"] as readonly string[],
|
|
21
|
-
// Any signed-in user role. Use on authenticated-but-not-privileged handlers
|
|
22
|
-
// (change-password, logout, me-style queries). Does NOT include "system"
|
|
23
|
-
// since an unauthenticated system call shouldn't be able to hit these.
|
|
24
|
-
authenticated: ["User", "Admin", "SystemAdmin"] as readonly string[],
|
|
25
|
-
// Unauthenticated callers reaching public endpoints (server must opt in
|
|
26
|
-
// via `anonymousAccess`). Combine with authenticated roles when an
|
|
27
|
-
// endpoint should serve both — e.g. `roles: ["anonymous", "customer"]`
|
|
28
|
-
// for a product-listing that personalises when a session is present.
|
|
29
|
-
anonymous: ["anonymous"] as readonly string[],
|
|
13
|
+
all: ["all"] as readonly string[], // @cast-boundary schema-walk
|
|
14
|
+
admin: ["Admin", "SystemAdmin"] as readonly string[], // @cast-boundary schema-walk
|
|
15
|
+
systemAdmin: ["SystemAdmin"] as readonly string[], // @cast-boundary schema-walk
|
|
16
|
+
system: ["system"] as readonly string[], // @cast-boundary schema-walk
|
|
17
|
+
privileged: ["system", "SystemAdmin"] as readonly string[], // @cast-boundary schema-walk
|
|
18
|
+
authenticated: ["User", "Admin", "SystemAdmin"] as readonly string[], // @cast-boundary schema-walk
|
|
19
|
+
anonymous: ["anonymous"] as readonly string[], // @cast-boundary schema-walk
|
|
30
20
|
roles: (...roles: string[]): readonly string[] => roles,
|
|
31
21
|
} as const;
|
|
32
22
|
|
|
@@ -63,7 +53,7 @@ const SCOPE_DEFAULTS: Record<ConfigScope, { write: readonly string[]; read: read
|
|
|
63
53
|
tenant: { write: access.admin, read: access.all },
|
|
64
54
|
system: { write: access.system, read: access.admin },
|
|
65
55
|
user: { write: access.all, read: access.all },
|
|
66
|
-
};
|
|
56
|
+
} satisfies Record<ConfigScope, { write: readonly string[]; read: readonly string[] }>;
|
|
67
57
|
|
|
68
58
|
// --- Factory ---
|
|
69
59
|
|
|
@@ -83,7 +73,7 @@ function createConfigKey<T extends ConfigKeyType>(
|
|
|
83
73
|
default: opts.default,
|
|
84
74
|
...(opts.encrypted ? { encrypted: true } : {}),
|
|
85
75
|
...(opts.options ? { options: opts.options } : {}),
|
|
86
|
-
bounds: opts.bounds as ConfigBounds | undefined,
|
|
76
|
+
bounds: opts.bounds as ConfigBounds | undefined, // @cast-boundary schema-walk
|
|
87
77
|
computed: opts.computed,
|
|
88
78
|
...(opts.allowPerRequest === true ? { allowPerRequest: true } : {}),
|
|
89
79
|
};
|
package/src/engine/constants.ts
CHANGED