@lunora/config 0.0.0 → 1.0.0-alpha.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/LICENSE.md +105 -0
- package/README.md +115 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +1075 -0
- package/dist/index.d.ts +1075 -0
- package/dist/index.mjs +20 -0
- package/dist/packem_shared/AGENT_RULES_DIR-lcgC08aE.mjs +40 -0
- package/dist/packem_shared/DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs +37 -0
- package/dist/packem_shared/LINKED_PROJECT_DIR-CXwXzV_C.mjs +52 -0
- package/dist/packem_shared/PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs +62 -0
- package/dist/packem_shared/REQUIRED_COMPATIBILITY_DATE-Dd1suoit.mjs +476 -0
- package/dist/packem_shared/applyAdditiveEdit-C-snTFEV.mjs +228 -0
- package/dist/packem_shared/buildPackageSecretsBlock-S74dgmwy.mjs +187 -0
- package/dist/packem_shared/classifyPolicyEdit-BHeAqF8P.mjs +99 -0
- package/dist/packem_shared/createConfirm-fvpdgJ9s.mjs +100 -0
- package/dist/packem_shared/detectFramework-Br-BcPBq.mjs +41 -0
- package/dist/packem_shared/discoverContainerInfo-BXFs6Wav.mjs +19 -0
- package/dist/packem_shared/discoverSchemaInfo-DWtypqpP.mjs +25 -0
- package/dist/packem_shared/discoverWorkflowInfo-CedvR0mn.mjs +19 -0
- package/dist/packem_shared/findWranglerFile-DwSuC-Kn.mjs +25 -0
- package/dist/packem_shared/formatLunoraEvent-D2fDeGB6.mjs +86 -0
- package/dist/packem_shared/handlePolicyScaffoldRequest-CiC2IGKx.mjs +103 -0
- package/dist/packem_shared/handleSchemaEditRequest-Df-Wrix-.mjs +99 -0
- package/dist/packem_shared/handleSeedRequest-DVCjaGO-.mjs +61 -0
- package/dist/packem_shared/inferLunoraBindings-0W3eRdIP.mjs +302 -0
- package/dist/packem_shared/injectRemoteFlags-C-WZAKLY.mjs +105 -0
- package/dist/packem_shared/interpretRemote-CtcIcB5-.mjs +34 -0
- package/dist/packem_shared/parseDevVariable-CJiq2IwE.mjs +30 -0
- package/dist/packem_shared/parseSchema-DSeyktvG.mjs +107 -0
- package/dist/packem_shared/policy-scaffold.d-DCmwn7zQ.d.mts +74 -0
- package/dist/packem_shared/policy-scaffold.d-DCmwn7zQ.d.ts +74 -0
- package/dist/packem_shared/reconcileWranglerBindings-ByJk3yLU.mjs +277 -0
- package/dist/packem_shared/renderStudioHtml-449Ysn75.mjs +37 -0
- package/dist/packem_shared/serveJsonHandler-B4OLTGLS.mjs +86 -0
- package/dist/packem_shared/studioAssetsStamp-Csk5RS4E.mjs +28 -0
- package/dist/studio-host/index.d.mts +227 -0
- package/dist/studio-host/index.d.ts +227 -0
- package/dist/studio-host/index.mjs +7 -0
- package/package.json +57 -17
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { discoverSchema } from '@lunora/codegen';
|
|
3
|
+
import { Project } from 'ts-morph';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const discoverSchemaInfo = (projectRoot, schemaDirectory) => {
|
|
7
|
+
const schemaPath = join(projectRoot, schemaDirectory, "schema.ts");
|
|
8
|
+
if (!existsSync(schemaPath)) {
|
|
9
|
+
return { info: void 0 };
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: false });
|
|
13
|
+
const schema = discoverSchema(project, schemaPath);
|
|
14
|
+
return {
|
|
15
|
+
info: {
|
|
16
|
+
hasGlobalTable: schema.tables.some((table) => table.shardMode === "global"),
|
|
17
|
+
vectorIndexNames: schema.vectorIndexes.map((index) => index.name)
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return { error: error instanceof Error ? error.message : String(error), info: void 0 };
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { discoverSchemaInfo };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { WORKFLOWS_FILENAME, discoverWorkflows } from '@lunora/codegen';
|
|
3
|
+
import { Project } from 'ts-morph';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const discoverWorkflowInfo = (projectRoot, schemaDirectory) => {
|
|
7
|
+
const workflowsPath = join(projectRoot, schemaDirectory, WORKFLOWS_FILENAME);
|
|
8
|
+
if (!existsSync(workflowsPath)) {
|
|
9
|
+
return { workflows: [] };
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: false });
|
|
13
|
+
return { workflows: discoverWorkflows(project, join(projectRoot, schemaDirectory)) };
|
|
14
|
+
} catch (error) {
|
|
15
|
+
return { error: error instanceof Error ? error.message : String(error), workflows: [] };
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { discoverWorkflowInfo };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { parse } from 'jsonc-parser';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const WRANGLER_FILES = ["wrangler.jsonc", "wrangler.json"];
|
|
6
|
+
const findWranglerFile = (projectRoot) => {
|
|
7
|
+
for (const candidate of WRANGLER_FILES) {
|
|
8
|
+
const fullPath = join(projectRoot, candidate);
|
|
9
|
+
if (existsSync(fullPath)) {
|
|
10
|
+
return fullPath;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
};
|
|
15
|
+
const readWranglerJsonc = (wranglerPath) => {
|
|
16
|
+
const text = readFileSync(wranglerPath, "utf8");
|
|
17
|
+
const parseErrors = [];
|
|
18
|
+
const value = parse(text, parseErrors, { allowTrailingComma: true });
|
|
19
|
+
if (parseErrors.length > 0 || value === null || typeof value !== "object") {
|
|
20
|
+
return { parsed: void 0, text };
|
|
21
|
+
}
|
|
22
|
+
return { parsed: value, text };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { WRANGLER_FILES, findWranglerFile, readWranglerJsonc };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const LUNORA_EVENT_SOURCE = "lunora";
|
|
2
|
+
const ROOT_SHARD_NAME = "__root__";
|
|
3
|
+
const asString = (value) => typeof value === "string" ? value : "";
|
|
4
|
+
const asStringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
5
|
+
const formatDuration = (value) => typeof value === "number" && Number.isFinite(value) ? `${String(Math.round(value))}ms` : "?ms";
|
|
6
|
+
const toLineLevel = (rawLevel) => {
|
|
7
|
+
if (rawLevel === "error") {
|
|
8
|
+
return "error";
|
|
9
|
+
}
|
|
10
|
+
if (rawLevel === "warn") {
|
|
11
|
+
return "warn";
|
|
12
|
+
}
|
|
13
|
+
return "info";
|
|
14
|
+
};
|
|
15
|
+
const parseLunoraEvent = (line) => {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(trimmed);
|
|
23
|
+
} catch {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
const event = parsed;
|
|
30
|
+
return event.source === LUNORA_EVENT_SOURCE ? event : void 0;
|
|
31
|
+
};
|
|
32
|
+
const labelFor = (functionPath, event) => {
|
|
33
|
+
const shard = asString(event.shard);
|
|
34
|
+
return shard === "" || shard === ROOT_SHARD_NAME ? functionPath : `${functionPath}@${shard}`;
|
|
35
|
+
};
|
|
36
|
+
const formatRequest = (event, functionPath) => {
|
|
37
|
+
const failed = event.outcome === "error";
|
|
38
|
+
const parts = [labelFor(functionPath, event), failed ? "error" : "ok", formatDuration(event.durationMs)];
|
|
39
|
+
const read = asStringList(event.tablesRead);
|
|
40
|
+
const written = asStringList(event.tablesWritten);
|
|
41
|
+
if (read.length > 0) {
|
|
42
|
+
parts.push(`read[${read.join(",")}]`);
|
|
43
|
+
}
|
|
44
|
+
if (written.length > 0) {
|
|
45
|
+
parts.push(`write[${written.join(",")}]`);
|
|
46
|
+
}
|
|
47
|
+
if (event.cacheHit === true) {
|
|
48
|
+
parts.push("cached");
|
|
49
|
+
}
|
|
50
|
+
const errorMessage = asString(event.error);
|
|
51
|
+
if (failed && errorMessage !== "") {
|
|
52
|
+
parts.push(errorMessage);
|
|
53
|
+
}
|
|
54
|
+
return { kind: "rpc", level: failed ? "error" : "info", text: parts.join(" ") };
|
|
55
|
+
};
|
|
56
|
+
const formatContainer = (event) => {
|
|
57
|
+
const name = asString(event.container) || "<unknown>";
|
|
58
|
+
const instance = asString(event.instance);
|
|
59
|
+
const transition = asString(event.event) || "event";
|
|
60
|
+
const shortId = instance === "" || instance === "unknown" ? "" : `#${instance.slice(0, 8)}`;
|
|
61
|
+
const message = asString(event.message);
|
|
62
|
+
const parts = [`container:${name}${shortId}`, transition];
|
|
63
|
+
if (message !== "") {
|
|
64
|
+
parts.push(message);
|
|
65
|
+
}
|
|
66
|
+
return { kind: "log", level: event.event === "error" ? "error" : "info", text: parts.join(" ") };
|
|
67
|
+
};
|
|
68
|
+
const formatLunoraEvent = (line) => {
|
|
69
|
+
const event = parseLunoraEvent(line);
|
|
70
|
+
if (!event) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
const functionPath = asString(event.function) || "<unknown>";
|
|
74
|
+
if (event.type === "log") {
|
|
75
|
+
return { kind: "log", level: toLineLevel(asString(event.level)), text: `${labelFor(functionPath, event)} ${asString(event.message)}`.trimEnd() };
|
|
76
|
+
}
|
|
77
|
+
if (event.type === "request") {
|
|
78
|
+
return formatRequest(event, functionPath);
|
|
79
|
+
}
|
|
80
|
+
if (event.type === "container") {
|
|
81
|
+
return formatContainer(event);
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export { LUNORA_EVENT_SOURCE, formatLunoraEvent };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { join, isAbsolute, relative } from 'node:path';
|
|
3
|
+
import { runCodegen, CodegenDiagnosticError } from '@lunora/codegen';
|
|
4
|
+
import { classifyPolicyEdit, scaffoldPolicyFile, wireRlsIntoProcedure } from './classifyPolicyEdit-BHeAqF8P.mjs';
|
|
5
|
+
|
|
6
|
+
const POLICY_SCAFFOLD_ENDPOINT = "/__lunora/policy-scaffold";
|
|
7
|
+
const statusForFailure = (reason) => {
|
|
8
|
+
if (reason === "unknown-procedure") {
|
|
9
|
+
return 404;
|
|
10
|
+
}
|
|
11
|
+
if (reason === "already-wired") {
|
|
12
|
+
return 409;
|
|
13
|
+
}
|
|
14
|
+
return reason === "destructive" ? 409 : 422;
|
|
15
|
+
};
|
|
16
|
+
const writeAtomic = (path, text) => {
|
|
17
|
+
const temporaryPath = `${path}.lunora-tmp`;
|
|
18
|
+
writeFileSync(temporaryPath, text, "utf8");
|
|
19
|
+
renameSync(temporaryPath, path);
|
|
20
|
+
};
|
|
21
|
+
const runCodegenForResponse = (request, okBody) => {
|
|
22
|
+
let diagnostics = [];
|
|
23
|
+
try {
|
|
24
|
+
runCodegen({ lunoraDirectory: request.schemaDirectory ?? "lunora", projectRoot: request.projectRoot });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof CodegenDiagnosticError) {
|
|
27
|
+
diagnostics = [error.message];
|
|
28
|
+
} else {
|
|
29
|
+
return { body: { error: error instanceof Error ? error.message : String(error), ok: false }, status: 500 };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { body: { ...okBody, diagnostics, ok: true }, status: 200 };
|
|
33
|
+
};
|
|
34
|
+
const refuseDestructive = (edit) => {
|
|
35
|
+
return {
|
|
36
|
+
body: {
|
|
37
|
+
edit,
|
|
38
|
+
message: "Rewriting an existing policy predicate changes evaluation semantics and must be done by hand. The scaffolder only adds new, deny-by-default rules.",
|
|
39
|
+
needsManualEdit: true,
|
|
40
|
+
ok: false
|
|
41
|
+
},
|
|
42
|
+
status: 409
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const handleScaffoldPolicy = (request, edit) => {
|
|
46
|
+
const result = scaffoldPolicyFile(edit);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
return { body: { error: result.reason, ok: false }, status: statusForFailure(result.reason) };
|
|
49
|
+
}
|
|
50
|
+
const lunoraDirectory = request.schemaDirectory ?? "lunora";
|
|
51
|
+
const targetPath = join(request.projectRoot, lunoraDirectory, result.fileName);
|
|
52
|
+
if (existsSync(targetPath)) {
|
|
53
|
+
return { body: { error: "file-exists", fileName: result.fileName, ok: false }, status: 409 };
|
|
54
|
+
}
|
|
55
|
+
writeAtomic(targetPath, result.source);
|
|
56
|
+
return runCodegenForResponse(request, { fileName: result.fileName });
|
|
57
|
+
};
|
|
58
|
+
const resolveProcedureFile = (projectRoot, lunoraDirectory, filePath) => {
|
|
59
|
+
if (typeof filePath !== "string" || filePath.length === 0 || isAbsolute(filePath) || filePath.includes("\\")) {
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
62
|
+
const lunoraRoot = join(projectRoot, lunoraDirectory);
|
|
63
|
+
const resolved = join(lunoraRoot, `${filePath}.ts`);
|
|
64
|
+
const relativePath = relative(lunoraRoot, resolved);
|
|
65
|
+
if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
};
|
|
70
|
+
const handleWireRls = (request, edit) => {
|
|
71
|
+
const procedurePath = resolveProcedureFile(request.projectRoot, request.schemaDirectory ?? "lunora", edit.filePath);
|
|
72
|
+
if (procedurePath === void 0 || !existsSync(procedurePath)) {
|
|
73
|
+
return { body: { error: "unknown-procedure", ok: false }, status: 404 };
|
|
74
|
+
}
|
|
75
|
+
const wired = wireRlsIntoProcedure(readFileSync(procedurePath, "utf8"), edit);
|
|
76
|
+
if (!wired.ok) {
|
|
77
|
+
return { body: { error: wired.reason, ok: false }, status: statusForFailure(wired.reason) };
|
|
78
|
+
}
|
|
79
|
+
writeAtomic(procedurePath, wired.text);
|
|
80
|
+
return runCodegenForResponse(request, { exportName: edit.exportName });
|
|
81
|
+
};
|
|
82
|
+
const handlePolicyScaffoldRequest = (request) => {
|
|
83
|
+
if (request.method !== "POST") {
|
|
84
|
+
return { body: { error: "method-not-allowed", ok: false }, status: 405 };
|
|
85
|
+
}
|
|
86
|
+
const { body } = request;
|
|
87
|
+
if (body === void 0 || typeof body !== "object" || typeof body.kind !== "string") {
|
|
88
|
+
return { body: { error: "invalid-edit", ok: false }, status: 400 };
|
|
89
|
+
}
|
|
90
|
+
const edit = body;
|
|
91
|
+
if (classifyPolicyEdit(edit) === "destructive") {
|
|
92
|
+
return refuseDestructive(edit);
|
|
93
|
+
}
|
|
94
|
+
if (edit.kind === "scaffoldPolicy") {
|
|
95
|
+
return handleScaffoldPolicy(request, edit);
|
|
96
|
+
}
|
|
97
|
+
if (edit.kind === "wireRls") {
|
|
98
|
+
return handleWireRls(request, edit);
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`unhandled additive policy edit kind: ${String(edit.kind)}`);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export { POLICY_SCAFFOLD_ENDPOINT, handlePolicyScaffoldRequest };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
2
|
+
import { runCodegen, CodegenDiagnosticError } from '@lunora/codegen';
|
|
3
|
+
import { classifyEdit, applyAdditiveEdit } from './applyAdditiveEdit-C-snTFEV.mjs';
|
|
4
|
+
import { parseSchema } from './parseSchema-DSeyktvG.mjs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const SCHEMA_EDIT_ENDPOINT = "/__lunora/schema-edit";
|
|
8
|
+
const statusForFailure = (reason) => {
|
|
9
|
+
if (reason === "destructive") {
|
|
10
|
+
return 409;
|
|
11
|
+
}
|
|
12
|
+
if (reason === "duplicate-table" || reason === "duplicate-column" || reason === "duplicate-index") {
|
|
13
|
+
return 409;
|
|
14
|
+
}
|
|
15
|
+
if (reason === "unknown-table") {
|
|
16
|
+
return 404;
|
|
17
|
+
}
|
|
18
|
+
if (reason === "invalid-identifier" || reason === "invalid-validator") {
|
|
19
|
+
return 400;
|
|
20
|
+
}
|
|
21
|
+
return 422;
|
|
22
|
+
};
|
|
23
|
+
const parseFailureResponse = (result) => {
|
|
24
|
+
return {
|
|
25
|
+
body: { error: result.reason, ok: false },
|
|
26
|
+
status: 422
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const readSchema = (schemaPath) => {
|
|
30
|
+
if (!existsSync(schemaPath)) {
|
|
31
|
+
return { response: { body: { error: "no-schema-file", ok: false }, status: 404 } };
|
|
32
|
+
}
|
|
33
|
+
const parsed = parseSchema(readFileSync(schemaPath, "utf8"));
|
|
34
|
+
if (!parsed.ok) {
|
|
35
|
+
return { response: parseFailureResponse(parsed) };
|
|
36
|
+
}
|
|
37
|
+
return { tables: parsed.tables };
|
|
38
|
+
};
|
|
39
|
+
const writeSchemaAtomic = (schemaPath, text) => {
|
|
40
|
+
const temporaryPath = `${schemaPath}.lunora-tmp`;
|
|
41
|
+
writeFileSync(temporaryPath, text, "utf8");
|
|
42
|
+
renameSync(temporaryPath, schemaPath);
|
|
43
|
+
};
|
|
44
|
+
const needsMigrationResponse = (edit) => {
|
|
45
|
+
return {
|
|
46
|
+
body: {
|
|
47
|
+
edit,
|
|
48
|
+
message: "This edit changes stored data and must go through a migration. Review the migration before applying.",
|
|
49
|
+
needsMigration: true,
|
|
50
|
+
ok: false
|
|
51
|
+
},
|
|
52
|
+
status: 409
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const handlePost = (request, schemaPath) => {
|
|
56
|
+
const edit = request.body;
|
|
57
|
+
if (edit === void 0 || typeof edit !== "object" || typeof edit.kind !== "string") {
|
|
58
|
+
return { body: { error: "invalid-edit", ok: false }, status: 400 };
|
|
59
|
+
}
|
|
60
|
+
if (classifyEdit(edit) === "destructive") {
|
|
61
|
+
return needsMigrationResponse(edit);
|
|
62
|
+
}
|
|
63
|
+
if (!existsSync(schemaPath)) {
|
|
64
|
+
return { body: { error: "no-schema-file", ok: false }, status: 404 };
|
|
65
|
+
}
|
|
66
|
+
const applied = applyAdditiveEdit(readFileSync(schemaPath, "utf8"), edit);
|
|
67
|
+
if (!applied.ok) {
|
|
68
|
+
return { body: { error: applied.reason, ok: false }, status: statusForFailure(applied.reason) };
|
|
69
|
+
}
|
|
70
|
+
writeSchemaAtomic(schemaPath, applied.text);
|
|
71
|
+
let diagnostics = [];
|
|
72
|
+
try {
|
|
73
|
+
runCodegen({ lunoraDirectory: request.schemaDirectory ?? "lunora", projectRoot: request.projectRoot });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof CodegenDiagnosticError) {
|
|
76
|
+
diagnostics = [error.message];
|
|
77
|
+
} else {
|
|
78
|
+
return { body: { error: error instanceof Error ? error.message : String(error), ok: false }, status: 500 };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const parsed = parseSchema(applied.text);
|
|
82
|
+
return {
|
|
83
|
+
body: { diagnostics, ok: true, tables: parsed.ok ? parsed.tables : [] },
|
|
84
|
+
status: 200
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const handleSchemaEditRequest = (request) => {
|
|
88
|
+
const schemaPath = join(request.projectRoot, request.schemaDirectory ?? "lunora", "schema.ts");
|
|
89
|
+
if (request.method === "GET") {
|
|
90
|
+
const read = readSchema(schemaPath);
|
|
91
|
+
return "response" in read ? read.response : { body: { ok: true, tables: read.tables }, status: 200 };
|
|
92
|
+
}
|
|
93
|
+
if (request.method === "POST") {
|
|
94
|
+
return handlePost(request, schemaPath);
|
|
95
|
+
}
|
|
96
|
+
return { body: { error: "method-not-allowed", ok: false }, status: 405 };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export { SCHEMA_EDIT_ENDPOINT, handleSchemaEditRequest };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { discoverSchema, schemaFromIr } from '@lunora/codegen';
|
|
3
|
+
import { seedPlan } from '@lunora/seed';
|
|
4
|
+
import { Project } from 'ts-morph';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const SEED_ENDPOINT = "/__lunora/seed";
|
|
8
|
+
const MAX_SEED_ROWS = 1e3;
|
|
9
|
+
const jsonReplacer = (_key, value) => {
|
|
10
|
+
if (typeof value === "bigint") {
|
|
11
|
+
return Number(value);
|
|
12
|
+
}
|
|
13
|
+
if (value instanceof ArrayBuffer) {
|
|
14
|
+
return [...new Uint8Array(value)];
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
const clampCount = (count) => {
|
|
19
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
20
|
+
return 10;
|
|
21
|
+
}
|
|
22
|
+
return Math.min(Math.max(1, Math.floor(count)), MAX_SEED_ROWS);
|
|
23
|
+
};
|
|
24
|
+
const handleSeedRequest = (request) => {
|
|
25
|
+
if (request.method !== "POST") {
|
|
26
|
+
return { body: { error: "method-not-allowed", ok: false }, status: 405 };
|
|
27
|
+
}
|
|
28
|
+
const { body } = request;
|
|
29
|
+
if (body === void 0 || typeof body !== "object") {
|
|
30
|
+
return { body: { error: "invalid-request", ok: false }, status: 400 };
|
|
31
|
+
}
|
|
32
|
+
const { count, existingIds, seed, table } = body;
|
|
33
|
+
if (typeof table !== "string" || table.length === 0) {
|
|
34
|
+
return { body: { error: "missing-table", ok: false }, status: 400 };
|
|
35
|
+
}
|
|
36
|
+
const lunoraDirectory = request.schemaDirectory ?? "lunora";
|
|
37
|
+
const schemaPath = join(request.projectRoot, lunoraDirectory, "schema.ts");
|
|
38
|
+
if (!existsSync(schemaPath)) {
|
|
39
|
+
return { body: { error: "schema-not-found", ok: false }, status: 404 };
|
|
40
|
+
}
|
|
41
|
+
const project = new Project({ skipAddingFilesFromTsConfig: true });
|
|
42
|
+
const ir = discoverSchema(project, schemaPath);
|
|
43
|
+
if (!ir.tables.some((candidate) => candidate.name === table)) {
|
|
44
|
+
return { body: { error: "unknown-table", ok: false, table }, status: 404 };
|
|
45
|
+
}
|
|
46
|
+
const schema = schemaFromIr(ir);
|
|
47
|
+
const plan = seedPlan(schema, {
|
|
48
|
+
// Pass every sampled parent table as `existingIds` so the generator links
|
|
49
|
+
// to live rows and never fabricates parents — matching the studio's
|
|
50
|
+
// "existing rows are not affected, FKs point at what's there" semantics.
|
|
51
|
+
existingIds: existingIds ?? {},
|
|
52
|
+
defaultCount: clampCount(count),
|
|
53
|
+
only: [table],
|
|
54
|
+
seed: seed ?? 0
|
|
55
|
+
});
|
|
56
|
+
const rows = plan.find((entry) => entry.table === table)?.rows ?? [];
|
|
57
|
+
const jsonSafeRows = JSON.parse(JSON.stringify(rows, jsonReplacer));
|
|
58
|
+
return { body: { ok: true, rows: jsonSafeRows }, status: 200 };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export { SEED_ENDPOINT, handleSeedRequest };
|