@mandujs/core 0.53.0 → 0.53.2
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/package.json +38 -2
- package/src/a11y/run-audit.ts +6 -6
- package/src/brain/adapters/oauth-flow.ts +1 -1
- package/src/brain/architecture/analyzer.ts +0 -2
- package/src/brain/credentials.ts +2 -2
- package/src/bundler/__tests__/build-runner.ts +13 -1
- package/src/bundler/__tests__/extended-watch.test.ts +3 -2
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -12
- package/src/bundler/build.test.ts +30 -33
- package/src/bundler/build.ts +56 -27
- package/src/bundler/dev.ts +44 -39
- package/src/bundler/prerender.ts +63 -0
- package/src/bundler/safe-build.test.ts +2 -1
- package/src/bundler/types.ts +13 -6
- package/src/change/history.ts +1 -1
- package/src/change/snapshot.ts +18 -9
- package/src/client/Link.tsx +9 -9
- package/src/client/globals.ts +3 -2
- package/src/client/hooks.ts +0 -4
- package/src/client/hydrate.ts +1 -1
- package/src/client/router.ts +12 -12
- package/src/client/rpc.ts +1 -1
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-sse.ts +5 -5
- package/src/config/mcp-status.ts +0 -1
- package/src/config/validate.ts +4 -4
- package/src/config/watcher.ts +1 -1
- package/src/content/collection.ts +8 -12
- package/src/content/content-layer.ts +5 -5
- package/src/content/data-store.ts +2 -2
- package/src/content/loader-context.ts +0 -1
- package/src/content/loaders/file.ts +1 -1
- package/src/content/loaders/glob.ts +4 -4
- package/src/content/loaders/types.ts +1 -1
- package/src/content/meta-store.ts +1 -1
- package/src/content/types.ts +1 -1
- package/src/contract/define.ts +11 -12
- package/src/contract/index.ts +3 -3
- package/src/contract/protection.ts +2 -2
- package/src/contract/types.ts +1 -1
- package/src/contract/validator.ts +0 -1
- package/src/db/__tests__/db.test.ts +4 -7
- package/src/db/index.ts +138 -51
- package/src/db/migrations/index.ts +3 -0
- package/src/db/migrations/lock.ts +78 -23
- package/src/db/migrations/runner.ts +118 -101
- package/src/deploy/cache.ts +1 -0
- package/src/design/extract.ts +2 -1
- package/src/design/patch.ts +0 -1
- package/src/devtools/ai/mcp-connector.ts +24 -24
- package/src/devtools/client/components/kitchen-root.tsx +1 -1
- package/src/devtools/client/components/panel/errors-panel.tsx +1 -1
- package/src/devtools/client/state-manager.ts +1 -1
- package/src/devtools/init.ts +2 -3
- package/src/devtools/server/source-context.ts +1 -1
- package/src/devtools/worker/redaction-worker.ts +8 -4
- package/src/devtools/worker/worker-manager.ts +4 -3
- package/src/error/classifier.ts +1 -1
- package/src/error/domains.ts +1 -1
- package/src/filling/sse.ts +5 -5
- package/src/generator/generate.ts +2 -2
- package/src/guard/analyzer.ts +1 -1
- package/src/guard/ast-analyzer.ts +7 -7
- package/src/guard/auto-correct.ts +2 -2
- package/src/guard/config-guard.ts +0 -2
- package/src/guard/contract-guard.ts +1 -1
- package/src/guard/decision-memory.ts +2 -2
- package/src/guard/design-inline-class.ts +2 -1
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -41
- package/src/guard/negotiation.ts +3 -3
- package/src/guard/reporter.ts +1 -4
- package/src/guard/semantic-slots.ts +2 -2
- package/src/guard/suggestions.ts +2 -2
- package/src/guard/watcher.ts +5 -4
- package/src/index.ts +5 -5
- package/src/intent/index.ts +1 -1
- package/src/kitchen/api/agent-devtools-api.ts +544 -0
- package/src/kitchen/api/contract-api.ts +1 -1
- package/src/kitchen/api/file-api.ts +3 -3
- package/src/kitchen/kitchen-handler.ts +63 -38
- package/src/kitchen/kitchen-ui.ts +346 -62
- package/src/lockfile/validate.ts +0 -1
- package/src/openapi/generator.ts +2 -1
- package/src/openapi/openapi.test.ts +43 -14
- package/src/perf/user-marks.ts +1 -1
- package/src/plugins/registry.ts +1 -2
- package/src/plugins/types.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -15
- package/src/resource/ddl/__tests__/emit.test.ts +24 -0
- package/src/resource/ddl/diff.ts +0 -1
- package/src/resource/ddl/emit.ts +12 -1
- package/src/resource/generator-repo.ts +40 -20
- package/src/resource/generator.ts +1 -1
- package/src/resource/generators/contract.ts +1 -1
- package/src/resource/schema.ts +2 -1
- package/src/router/fs-routes.ts +6 -6
- package/src/router/fs-scanner.ts +1 -1
- package/src/runtime/env.ts +1 -1
- package/src/runtime/image-handler.ts +2 -1
- package/src/runtime/middleware.ts +2 -1
- package/src/runtime/server.ts +67 -60
- package/src/runtime/ssr.ts +3 -3
- package/src/runtime/streaming-ssr.ts +25 -27
- package/src/scheduler/validate.ts +1 -1
- package/src/seo/integration/ssr.ts +2 -3
- package/src/seo/render/opengraph.ts +1 -1
- package/src/seo/resolve/index.ts +0 -4
- package/src/slot/corrector.ts +1 -1
- package/src/slot/validator.ts +4 -4
- package/src/watcher/reporter.ts +1 -1
- package/src/watcher/watcher.ts +19 -19
|
@@ -16,9 +16,34 @@ import {
|
|
|
16
16
|
} from "./generator";
|
|
17
17
|
import type { OpenAPIDocument } from "./generator";
|
|
18
18
|
import type { RoutesManifest } from "../spec/schema";
|
|
19
|
-
import fs from "node:fs/promises";
|
|
20
|
-
import os from "node:os";
|
|
21
|
-
import path from "node:path";
|
|
19
|
+
import fs from "node:fs/promises";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
async function linkWorkspaceNodeModules(rootDir: string): Promise<void> {
|
|
24
|
+
const workspaceNodeModules = path.join(process.cwd(), "node_modules");
|
|
25
|
+
const fixtureNodeModules = path.join(rootDir, "node_modules");
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(workspaceNodeModules);
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await fs.symlink(
|
|
35
|
+
workspaceNodeModules,
|
|
36
|
+
fixtureNodeModules,
|
|
37
|
+
process.platform === "win32" ? "junction" : "dir"
|
|
38
|
+
);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
try {
|
|
41
|
+
await fs.access(fixtureNodeModules);
|
|
42
|
+
} catch {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
describe("zodToOpenAPISchema", () => {
|
|
24
49
|
describe("primitive types", () => {
|
|
@@ -321,9 +346,10 @@ describe("Real-world contract conversion", () => {
|
|
|
321
346
|
* exercise the full build pipeline (generate + write + read) without
|
|
322
347
|
* faking the dynamic import path.
|
|
323
348
|
*/
|
|
324
|
-
async function buildFixture(): Promise<{ rootDir: string; manifest: RoutesManifest; cleanup: () => Promise<void> }> {
|
|
325
|
-
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-"));
|
|
326
|
-
|
|
349
|
+
async function buildFixture(): Promise<{ rootDir: string; manifest: RoutesManifest; cleanup: () => Promise<void> }> {
|
|
350
|
+
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-"));
|
|
351
|
+
await linkWorkspaceNodeModules(rootDir);
|
|
352
|
+
const contractPath = "contracts/users.contract.ts";
|
|
327
353
|
const contractAbs = path.join(rootDir, contractPath);
|
|
328
354
|
await fs.mkdir(path.dirname(contractAbs), { recursive: true });
|
|
329
355
|
await fs.writeFile(
|
|
@@ -534,8 +560,9 @@ async function buildSharedSchemaFixture(opts: {
|
|
|
534
560
|
manifest: RoutesManifest;
|
|
535
561
|
cleanup: () => Promise<void>;
|
|
536
562
|
}> {
|
|
537
|
-
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-hoist-"));
|
|
538
|
-
|
|
563
|
+
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-hoist-"));
|
|
564
|
+
await linkWorkspaceNodeModules(rootDir);
|
|
565
|
+
const [nameA, nameB] = opts.contractNames ?? ["users", "admins"];
|
|
539
566
|
const bodyB =
|
|
540
567
|
opts.secondBody ??
|
|
541
568
|
`z.object({ name: z.string().min(2), email: z.string().email() })`;
|
|
@@ -642,9 +669,10 @@ describe("hoistSharedSchemas", () => {
|
|
|
642
669
|
});
|
|
643
670
|
|
|
644
671
|
test("one-off schema stays inline (no hoist, no components.schemas entry for it)", async () => {
|
|
645
|
-
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-oneoff-"));
|
|
646
|
-
try {
|
|
647
|
-
await
|
|
672
|
+
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-oneoff-"));
|
|
673
|
+
try {
|
|
674
|
+
await linkWorkspaceNodeModules(rootDir);
|
|
675
|
+
await fs.mkdir(path.join(rootDir, "contracts"), { recursive: true });
|
|
648
676
|
await fs.writeFile(
|
|
649
677
|
path.join(rootDir, "contracts/solo.contract.ts"),
|
|
650
678
|
`import { z } from "zod";
|
|
@@ -816,9 +844,10 @@ export default {
|
|
|
816
844
|
});
|
|
817
845
|
|
|
818
846
|
test("enum / primitive schemas are never hoisted", async () => {
|
|
819
|
-
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-enum-"));
|
|
820
|
-
try {
|
|
821
|
-
await
|
|
847
|
+
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "mandu-openapi-enum-"));
|
|
848
|
+
try {
|
|
849
|
+
await linkWorkspaceNodeModules(rootDir);
|
|
850
|
+
await fs.mkdir(path.join(rootDir, "contracts"), { recursive: true });
|
|
822
851
|
const contractBody = `import { z } from "zod";
|
|
823
852
|
export default {
|
|
824
853
|
name: NAME,
|
package/src/perf/user-marks.ts
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
*/
|
|
63
63
|
|
|
64
64
|
import { isPerfEnabled } from "./index";
|
|
65
|
-
import { getTracer,
|
|
65
|
+
import { getTracer, runWithSpan, type Span } from "../observability/tracing";
|
|
66
66
|
|
|
67
67
|
// ─── Types ──────────────────────────────────────────────────────────────
|
|
68
68
|
|
package/src/plugins/registry.ts
CHANGED
|
@@ -11,7 +11,6 @@ import type {
|
|
|
11
11
|
Plugin,
|
|
12
12
|
PluginApi,
|
|
13
13
|
PluginCategory,
|
|
14
|
-
PluginMeta,
|
|
15
14
|
GuardPresetPlugin,
|
|
16
15
|
BuildPlugin,
|
|
17
16
|
LoggerTransportPlugin,
|
|
@@ -22,7 +21,7 @@ import type {
|
|
|
22
21
|
/**
|
|
23
22
|
* 플러그인 등록 상태
|
|
24
23
|
*/
|
|
25
|
-
type
|
|
24
|
+
type _PluginState = "pending" | "loaded" | "error" | "unloaded";
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* 등록된 플러그인 정보 (discriminated union by state)
|
package/src/plugins/types.ts
CHANGED
|
@@ -574,7 +574,7 @@ describe("generateRepoSource — emission shape", () => {
|
|
|
574
574
|
expect(generateRepoSource(nonPersistent, { enable: false })).toBeNull();
|
|
575
575
|
});
|
|
576
576
|
|
|
577
|
-
test("postgres/sqlite emit INSERT ... RETURNING, mysql emits INSERT +
|
|
577
|
+
test("postgres/sqlite emit INSERT ... RETURNING, mysql emits INSERT + primary-key re-select", () => {
|
|
578
578
|
// Assert on SQL in the emitted code rather than on comments —
|
|
579
579
|
// comments reference "RETURNING" as prose across all three providers.
|
|
580
580
|
// The discriminator we check is the actual SQL keyword on an insert
|
|
@@ -585,20 +585,37 @@ describe("generateRepoSource — emission shape", () => {
|
|
|
585
585
|
const sqlite = generateRepoSource(persistentResource("user", "sqlite"))!;
|
|
586
586
|
expect(sqlite).toMatch(/INSERT INTO[\s\S]*?RETURNING/);
|
|
587
587
|
|
|
588
|
-
const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
|
|
589
|
-
expect(mysql).not.toMatch(/INSERT INTO[\s\S]*?RETURNING/);
|
|
590
|
-
expect(mysql).toContain("
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
expect(mysql).
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
expect(pg).
|
|
601
|
-
|
|
588
|
+
const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
|
|
589
|
+
expect(mysql).not.toMatch(/INSERT INTO[\s\S]*?RETURNING/);
|
|
590
|
+
expect(mysql).toContain("WHERE \\`id\\` = ${input.id}");
|
|
591
|
+
expect(mysql).not.toContain("WHERE \\`id\\` = LAST_INSERT_ID()");
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("mysql repo escapes backtick identifiers for generated template literals, postgres uses double quotes", () => {
|
|
595
|
+
const mysql = generateRepoSource(persistentResource("user", "mysql"))!;
|
|
596
|
+
expect(mysql).toContain("\\`users\\`");
|
|
597
|
+
expect(mysql).not.toContain(`"users"`);
|
|
598
|
+
|
|
599
|
+
const pg = generateRepoSource(persistentResource("user", "postgres"))!;
|
|
600
|
+
expect(pg).toContain(`"users"`);
|
|
601
|
+
expect(pg).not.toContain("\\`users\\`");
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
test("create input includes primary key unless the primary key has a DB default", () => {
|
|
605
|
+
const callerSuppliedPk = generateRepoSource(persistentResource("user", "postgres"))!;
|
|
606
|
+
expect(callerSuppliedPk).toContain("async create(input: User)");
|
|
607
|
+
expect(callerSuppliedPk).toContain("${input.id}");
|
|
608
|
+
expect(callerSuppliedPk).not.toContain('async create(input: Omit<User, "id">)');
|
|
609
|
+
|
|
610
|
+
const dbGeneratedPk = persistentResource("token", "postgres");
|
|
611
|
+
dbGeneratedPk.definition.fields.id = {
|
|
612
|
+
...dbGeneratedPk.definition.fields.id,
|
|
613
|
+
default: "gen_random_uuid()",
|
|
614
|
+
};
|
|
615
|
+
const src = generateRepoSource(dbGeneratedPk)!;
|
|
616
|
+
expect(src).toContain('async create(input: Omit<Token, "id">)');
|
|
617
|
+
expect(src).not.toMatch(/INSERT INTO[\s\S]*"id"[\s\S]*VALUES/);
|
|
618
|
+
});
|
|
602
619
|
|
|
603
620
|
test("shouldEmitRepo returns true only when persistence is declared", () => {
|
|
604
621
|
const persistent = persistentResource("user");
|
|
@@ -419,6 +419,30 @@ describe("emitChange — add-column", () => {
|
|
|
419
419
|
expect(emitChange(change, "sqlite"))
|
|
420
420
|
.toBe('ALTER TABLE "users" ADD COLUMN "age" REAL;');
|
|
421
421
|
});
|
|
422
|
+
|
|
423
|
+
test("indexed:true field emits ADD COLUMN plus auto CREATE INDEX", () => {
|
|
424
|
+
const indexed: Change = {
|
|
425
|
+
kind: "add-column",
|
|
426
|
+
resourceName: "users",
|
|
427
|
+
field: field({ name: "role", type: "string", indexed: true }),
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
expect(emitChange(indexed, "postgres")).toBe(
|
|
431
|
+
[
|
|
432
|
+
'ALTER TABLE "users" ADD COLUMN "role" TEXT NOT NULL;',
|
|
433
|
+
'CREATE INDEX "idx_users_role" ON "users" ("role");',
|
|
434
|
+
].join("\n"),
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("indexed:true unique or primary add-column does not create redundant index", () => {
|
|
439
|
+
const unique: Change = {
|
|
440
|
+
kind: "add-column",
|
|
441
|
+
resourceName: "users",
|
|
442
|
+
field: field({ name: "slug", type: "string", indexed: true, unique: true }),
|
|
443
|
+
};
|
|
444
|
+
expect(emitChange(unique, "postgres")).not.toContain("idx_users_slug");
|
|
445
|
+
});
|
|
422
446
|
});
|
|
423
447
|
|
|
424
448
|
describe("emitChange — drop-column", () => {
|
package/src/resource/ddl/diff.ts
CHANGED
package/src/resource/ddl/emit.ts
CHANGED
|
@@ -345,7 +345,18 @@ function emitAddColumn(
|
|
|
345
345
|
): string {
|
|
346
346
|
const table = quoteIdent(resourceName, provider);
|
|
347
347
|
const columnDef = emitColumnDef(field, provider);
|
|
348
|
-
|
|
348
|
+
const addColumn = `ALTER TABLE ${table} ADD COLUMN ${columnDef};`;
|
|
349
|
+
if (!field.indexed || field.unique || field.primary) return addColumn;
|
|
350
|
+
return [
|
|
351
|
+
addColumn,
|
|
352
|
+
emitCreateIndex(
|
|
353
|
+
resourceName,
|
|
354
|
+
autoIndexName(resourceName, field.name),
|
|
355
|
+
[field.name],
|
|
356
|
+
false,
|
|
357
|
+
provider,
|
|
358
|
+
),
|
|
359
|
+
].join("\n");
|
|
349
360
|
}
|
|
350
361
|
|
|
351
362
|
/**
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* return {
|
|
36
36
|
* async findById(id): User | null { ... },
|
|
37
37
|
* async findMany(limit = 100, offset = 0): User[] { ... },
|
|
38
|
-
* async create(row): User { ... }, // PG/SQLite: RETURNING *; MySQL: INSERT +
|
|
38
|
+
* async create(row): User { ... }, // PG/SQLite: RETURNING *; MySQL: INSERT + re-select
|
|
39
39
|
* async update(id, patch): User | null { ... },
|
|
40
40
|
* async delete(id): boolean { ... },
|
|
41
41
|
* };
|
|
@@ -65,7 +65,7 @@ import type { ParsedResource } from "./parser";
|
|
|
65
65
|
import type { ResourceField } from "./schema";
|
|
66
66
|
import { quoteIdent } from "./ddl/emit";
|
|
67
67
|
import { toSnakeCase } from "./ddl/snapshot";
|
|
68
|
-
import { asPersistence, type ExtendedResourcePersistence } from "./ddl/persistence-types";
|
|
68
|
+
import { asPersistence, type ExtendedResourcePersistence, type FieldOverride } from "./ddl/persistence-types";
|
|
69
69
|
import type { SqlProvider } from "./ddl/types";
|
|
70
70
|
|
|
71
71
|
// ============================================
|
|
@@ -172,9 +172,10 @@ function renderRepoFile(
|
|
|
172
172
|
const primaryKeyColumn = columns.find((c) => c.primary)?.column ?? "id";
|
|
173
173
|
const primaryKeyField = columns.find((c) => c.primary)?.field ?? "id";
|
|
174
174
|
const insertColumns = columns.filter((c) => !c.omitOnInsert);
|
|
175
|
+
const createInputType = createMethodInputType(pascalName, columns);
|
|
175
176
|
|
|
176
177
|
// Quoted identifiers — precomputed so each builder reads cleanly.
|
|
177
|
-
const q = (name: string) => quoteIdent(name, provider);
|
|
178
|
+
const q = (name: string) => escapeSqlTemplateText(quoteIdent(name, provider));
|
|
178
179
|
const qTable = q(table);
|
|
179
180
|
|
|
180
181
|
// Column list for SELECT: we don't use `*` because aliasing
|
|
@@ -194,7 +195,7 @@ function renderRepoFile(
|
|
|
194
195
|
const body = [
|
|
195
196
|
findByIdMethod(pascalName, qTable, selectList, q, primaryKeyColumn, primaryKeyField),
|
|
196
197
|
findManyMethod(pascalName, qTable, selectList),
|
|
197
|
-
createMethod(pascalName, table, qTable, selectList, insertColumns, primaryKeyColumn, primaryKeyField, q, provider),
|
|
198
|
+
createMethod(pascalName, table, qTable, selectList, insertColumns, createInputType, primaryKeyColumn, primaryKeyField, q, provider),
|
|
198
199
|
updateMethod(pascalName, table, qTable, selectList, columns, primaryKeyColumn, primaryKeyField, q, provider),
|
|
199
200
|
deleteMethod(pascalName, qTable, q, primaryKeyColumn, primaryKeyField, provider),
|
|
200
201
|
].join(",\n\n");
|
|
@@ -232,7 +233,7 @@ function renderHeader(
|
|
|
232
233
|
// - The repo never constructs a Db itself — callers (slots, scripts) pass
|
|
233
234
|
// in either \`ctx.deps.db\` or a module-level singleton.
|
|
234
235
|
// - Provider-specific SQL (INSERT ... RETURNING * on PG/SQLite vs
|
|
235
|
-
// INSERT +
|
|
236
|
+
// INSERT + re-select on MySQL) is resolved at generation
|
|
236
237
|
// time; the generated file has one code path per provider.
|
|
237
238
|
`;
|
|
238
239
|
}
|
|
@@ -348,6 +349,7 @@ function createMethod(
|
|
|
348
349
|
qTable: string,
|
|
349
350
|
selectList: string,
|
|
350
351
|
insertColumns: ResolvedColumn[],
|
|
352
|
+
createInputType: string,
|
|
351
353
|
pkColumn: string,
|
|
352
354
|
pkField: string,
|
|
353
355
|
q: (n: string) => string,
|
|
@@ -371,7 +373,7 @@ function createMethod(
|
|
|
371
373
|
* Insert a new row and return the inserted record. Errors from
|
|
372
374
|
* constraint violations (UNIQUE, NOT NULL, FK) bubble unchanged.
|
|
373
375
|
*/
|
|
374
|
-
async create(input:
|
|
376
|
+
async create(input: ${createInputType}): Promise<${pascalName}> {
|
|
375
377
|
const row = await db.one<${pascalName}>\`
|
|
376
378
|
INSERT INTO ${qTable} (${columnList})
|
|
377
379
|
VALUES (${valuePlaceholders})
|
|
@@ -382,17 +384,20 @@ function createMethod(
|
|
|
382
384
|
}`;
|
|
383
385
|
}
|
|
384
386
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
const mysqlLookup = insertColumns.some((c) => c.field === pkField)
|
|
388
|
+
? `${q(pkColumn)} = \${input.${pkField}}`
|
|
389
|
+
: `${q(pkColumn)} = LAST_INSERT_ID()`;
|
|
390
|
+
|
|
391
|
+
// MySQL: no RETURNING. INSERT then SELECT by the supplied primary key when
|
|
392
|
+
// the caller provides it; if the primary key is DB-generated, fall back to
|
|
393
|
+
// LAST_INSERT_ID().
|
|
389
394
|
return ` /**
|
|
390
395
|
* Insert a new row and return the inserted record via a follow-up SELECT.
|
|
391
396
|
* MySQL lacks RETURNING; the generator uses LAST_INSERT_ID() when the
|
|
392
397
|
* primary key is server-generated, otherwise it re-selects by the
|
|
393
398
|
* provided primary key value.
|
|
394
399
|
*/
|
|
395
|
-
async create(input:
|
|
400
|
+
async create(input: ${createInputType}): Promise<${pascalName}> {
|
|
396
401
|
await db\`
|
|
397
402
|
INSERT INTO ${qTable} (${columnList})
|
|
398
403
|
VALUES (${valuePlaceholders})
|
|
@@ -400,7 +405,7 @@ function createMethod(
|
|
|
400
405
|
const row = await db.one<${pascalName}>\`
|
|
401
406
|
SELECT ${selectList}
|
|
402
407
|
FROM ${qTable}
|
|
403
|
-
WHERE ${
|
|
408
|
+
WHERE ${mysqlLookup}
|
|
404
409
|
\`;
|
|
405
410
|
if (!row) throw new Error("${tableName} create: follow-up SELECT returned no row");
|
|
406
411
|
return row;
|
|
@@ -541,14 +546,7 @@ function resolveColumns(
|
|
|
541
546
|
const primary = declaredPkMatch || fieldLevelPk;
|
|
542
547
|
if (primary) pkCount++;
|
|
543
548
|
|
|
544
|
-
|
|
545
|
-
// - it's a UUID with a DB-side default (we don't know this at
|
|
546
|
-
// generation time for all providers); OR
|
|
547
|
-
// - it's an integer and the user didn't explicitly opt in.
|
|
548
|
-
// Safe default: let the user supply the PK. The generated
|
|
549
|
-
// `Omit<T, pkField>` on `create` means the caller CAN'T pass it
|
|
550
|
-
// anyway, so we omit PK from the insert column list unconditionally.
|
|
551
|
-
const omitOnInsert = primary;
|
|
549
|
+
const omitOnInsert = primary && hasDbDefault(field, override);
|
|
552
550
|
|
|
553
551
|
columns.push({ field: fieldKey, column, primary, omitOnInsert });
|
|
554
552
|
}
|
|
@@ -575,6 +573,28 @@ function resolveDeclaredPrimaryKey(
|
|
|
575
573
|
return declared[0];
|
|
576
574
|
}
|
|
577
575
|
|
|
576
|
+
function createMethodInputType(
|
|
577
|
+
pascalName: string,
|
|
578
|
+
columns: ResolvedColumn[],
|
|
579
|
+
): string {
|
|
580
|
+
const omittedFields = columns
|
|
581
|
+
.filter((c) => c.omitOnInsert)
|
|
582
|
+
.map((c) => JSON.stringify(c.field));
|
|
583
|
+
if (omittedFields.length === 0) return pascalName;
|
|
584
|
+
return `Omit<${pascalName}, ${omittedFields.join(" | ")}>`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function hasDbDefault(
|
|
588
|
+
field: ResourceField,
|
|
589
|
+
override: FieldOverride | undefined,
|
|
590
|
+
): boolean {
|
|
591
|
+
return override?.default !== undefined || field.default !== undefined;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function escapeSqlTemplateText(text: string): string {
|
|
595
|
+
return text.replace(/`/g, "\\`");
|
|
596
|
+
}
|
|
597
|
+
|
|
578
598
|
function resolveTableName(
|
|
579
599
|
resource: ParsedResource,
|
|
580
600
|
persistence: ExtendedResourcePersistence,
|
|
@@ -14,7 +14,7 @@ import { getPluralName, getEnabledEndpoints } from "../schema";
|
|
|
14
14
|
export function generateResourceContract(definition: ResourceDefinition): string {
|
|
15
15
|
const resourceName = definition.name;
|
|
16
16
|
const pascalName = toPascalCase(resourceName);
|
|
17
|
-
const
|
|
17
|
+
const _pluralName = getPluralName(definition);
|
|
18
18
|
const endpoints = getEnabledEndpoints(definition);
|
|
19
19
|
|
|
20
20
|
// Generate schema definitions
|
package/src/resource/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as __ManduDdlPersistenceTypesTypes0 from "./ddl/persistence-types";
|
|
1
2
|
/**
|
|
2
3
|
* Resource Schema Definition
|
|
3
4
|
* Resource-Centric Architecture의 핵심 스키마 정의
|
|
@@ -100,7 +101,7 @@ export interface ResourceOptions {
|
|
|
100
101
|
* });
|
|
101
102
|
* ```
|
|
102
103
|
*/
|
|
103
|
-
persistence?:
|
|
104
|
+
persistence?: __ManduDdlPersistenceTypesTypes0.ExtendedResourcePersistence;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
// ============================================
|
package/src/router/fs-routes.ts
CHANGED
|
@@ -451,12 +451,12 @@ export async function watchFSRoutes(
|
|
|
451
451
|
|
|
452
452
|
return {
|
|
453
453
|
close() {
|
|
454
|
-
if (debounceTimer) {
|
|
455
|
-
clearTimeout(debounceTimer);
|
|
456
|
-
}
|
|
457
|
-
routesWatcher.close();
|
|
458
|
-
specWatcher.close();
|
|
459
|
-
},
|
|
454
|
+
if (debounceTimer) {
|
|
455
|
+
clearTimeout(debounceTimer);
|
|
456
|
+
}
|
|
457
|
+
void routesWatcher.close();
|
|
458
|
+
void specWatcher.close();
|
|
459
|
+
},
|
|
460
460
|
async rescan() {
|
|
461
461
|
return triggerRescan();
|
|
462
462
|
},
|
package/src/router/fs-scanner.ts
CHANGED
|
@@ -228,7 +228,7 @@ export class FSScanner {
|
|
|
228
228
|
*/
|
|
229
229
|
private async createRouteConfigs(
|
|
230
230
|
files: ScannedFile[],
|
|
231
|
-
|
|
231
|
+
_rootDir: string
|
|
232
232
|
): Promise<{ routes: FSRouteConfig[]; routeErrors: ScanError[] }> {
|
|
233
233
|
const routes: FSRouteConfig[] = [];
|
|
234
234
|
const routeErrors: ScanError[] = [];
|
package/src/runtime/env.ts
CHANGED
|
@@ -93,7 +93,7 @@ function parseEnvFile(content: string): Record<string, string> {
|
|
|
93
93
|
/**
|
|
94
94
|
* 파일이 존재하는지 확인
|
|
95
95
|
*/
|
|
96
|
-
async function
|
|
96
|
+
async function _fileExists(filePath: string): Promise<boolean> {
|
|
97
97
|
try {
|
|
98
98
|
const file = Bun.file(filePath);
|
|
99
99
|
return await file.exists();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as __ManduFsTypes0 from "fs";
|
|
1
2
|
/**
|
|
2
3
|
* Mandu Image Handler
|
|
3
4
|
* /_mandu/image?url=...&w=...&q=... 엔드포인트
|
|
@@ -76,7 +77,7 @@ export async function handleImageRequest(
|
|
|
76
77
|
// realpath로 symlink를 해석한 후 allowedBaseDir 내부인지 검증
|
|
77
78
|
let resolvedPath: string;
|
|
78
79
|
try {
|
|
79
|
-
const realFs = require("fs") as typeof
|
|
80
|
+
const realFs = require("fs") as typeof __ManduFsTypes0;
|
|
80
81
|
resolvedPath = realFs.realpathSync(filePath);
|
|
81
82
|
const resolvedBase = realFs.realpathSync(allowedBaseDir);
|
|
82
83
|
if (!resolvedPath.startsWith(resolvedBase + path.sep) && resolvedPath !== resolvedBase) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type * as __ManduFsTypes0 from "fs";
|
|
1
2
|
/**
|
|
2
3
|
* Mandu Global Middleware
|
|
3
4
|
* 라우트 매칭 전에 실행되는 글로벌 미들웨어 시스템
|
|
@@ -227,7 +228,7 @@ export async function loadMiddleware(
|
|
|
227
228
|
export function loadMiddlewareSync(
|
|
228
229
|
rootDir: string
|
|
229
230
|
): { fn: MiddlewareFn; config: MiddlewareConfig | null } | null {
|
|
230
|
-
const fs = require("fs") as typeof
|
|
231
|
+
const fs = require("fs") as typeof __ManduFsTypes0;
|
|
231
232
|
const possiblePaths = [
|
|
232
233
|
`${rootDir}/middleware.ts`,
|
|
233
234
|
`${rootDir}/middleware.js`,
|