@mandujs/core 0.54.12 → 0.54.13
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 +9 -1
- package/scripts/postinstall-lock.ts +153 -153
- package/src/a11y/run-audit.ts +15 -15
- package/src/agent/__tests__/context.test.ts +49 -1
- package/src/agent/context.ts +535 -535
- package/src/agent/index.ts +6 -6
- package/src/agent/plan.ts +282 -282
- package/src/agent/repair.ts +171 -171
- package/src/agent/sync.ts +200 -200
- package/src/agent/types.ts +8 -0
- package/src/agent/verify.ts +100 -2
- package/src/brain/doctor/analyzer.ts +7 -7
- package/src/bundler/__tests__/build-runner.ts +5 -4
- package/src/bundler/__tests__/cold-start.test.ts +60 -60
- package/src/bundler/__tests__/css.test.ts +20 -20
- package/src/bundler/analyzer.ts +15 -15
- package/src/bundler/build.test.ts +66 -15
- package/src/bundler/build.ts +165 -103
- package/src/bundler/css.ts +42 -42
- package/src/bundler/manifest-schema.ts +21 -21
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
- package/src/bundler/plugins/block-generated-imports.ts +13 -13
- package/src/bundler/types.ts +31 -31
- package/src/client/island.ts +79 -79
- package/src/config/validate.ts +1 -1
- package/src/contract/schema.ts +7 -0
- package/src/deploy/inference/context.ts +82 -82
- package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
- package/src/devtools/client/components/panel/panel-container.tsx +1 -1
- package/src/error/formatter.ts +10 -1
- package/src/experimental/index.ts +10 -0
- package/src/filling/context.ts +17 -17
- package/src/filling/filling.ts +22 -1
- package/src/filling/index.ts +15 -1
- package/src/generator/generate.ts +30 -30
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +210 -210
- package/src/guard/check.ts +9 -9
- package/src/guard/config-guard.ts +13 -13
- package/src/guard/fs-routes-policy.ts +51 -51
- package/src/guard/index.ts +11 -11
- package/src/index.ts +0 -10
- package/src/internal/index.ts +25 -0
- package/src/kitchen/api/file-api.ts +11 -11
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +6 -6
- package/src/resource/__tests__/schema.test.ts +14 -14
- package/src/resource/ddl/__tests__/emit.test.ts +165 -165
- package/src/resource/ddl/emit.ts +146 -146
- package/src/resource/generator-schema.ts +11 -11
- package/src/resource/generators/slot.ts +72 -72
- package/src/resource/schema.ts +21 -21
- package/src/router/client-entry.test.ts +69 -33
- package/src/router/client-entry.ts +134 -74
- package/src/router/fs-routes.ts +24 -22
- package/src/router/fs-scanner.ts +21 -17
- package/src/router/fs-types.ts +8 -5
- package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
- package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
- package/src/runtime/__tests__/page-render-response.test.ts +103 -103
- package/src/runtime/__tests__/request-middleware.test.ts +70 -70
- package/src/runtime/devtools-adapter.ts +68 -68
- package/src/runtime/escape.ts +34 -34
- package/src/runtime/image-feature.ts +15 -0
- package/src/runtime/observability-lifecycle.ts +290 -290
- package/src/runtime/page-render-response.ts +106 -106
- package/src/runtime/rate-limit.ts +231 -0
- package/src/runtime/request-middleware.ts +31 -31
- package/src/runtime/scheduler-lifecycle.ts +64 -0
- package/src/runtime/server.ts +27 -295
- package/src/runtime/ssr.ts +59 -59
- package/src/runtime/static-files.ts +289 -289
- package/src/runtime/streaming-ssr.ts +22 -22
- package/src/spec/schema.ts +4 -3
- package/src/watcher/__tests__/watcher.test.ts +59 -59
- package/src/watcher/watcher.ts +61 -61
package/src/resource/ddl/emit.ts
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* 4. Value literals in `DEFAULT` clauses flow through `resolveDefault`
|
|
16
16
|
* which handles quote escaping. `kind: "sql"` is the explicit
|
|
17
17
|
* escape hatch — caller's responsibility.
|
|
18
|
-
* 5. Unsupported changes (v1 scope — `alter-column-type`, and some
|
|
19
|
-
* dialect-specific ALTER gaps) emit a `-- TODO:` comment block plus
|
|
20
|
-
* a deliberate failing sentinel statement. The generated migration
|
|
21
|
-
* is reviewable/editable, but `mandu db apply` will not record it
|
|
22
|
-
* until the operator replaces the TODO with real SQL.
|
|
18
|
+
* 5. Unsupported changes (v1 scope — `alter-column-type`, and some
|
|
19
|
+
* dialect-specific ALTER gaps) emit a `-- TODO:` comment block plus
|
|
20
|
+
* a deliberate failing sentinel statement. The generated migration
|
|
21
|
+
* is reviewable/editable, but `mandu db apply` will not record it
|
|
22
|
+
* until the operator replaces the TODO with real SQL.
|
|
23
23
|
*
|
|
24
24
|
* Determinism:
|
|
25
25
|
* - `emitCreateTable` emits columns in `DdlFieldDef` array order — the
|
|
@@ -339,16 +339,16 @@ export function emitChange(change: Change, provider: SqlProvider): string {
|
|
|
339
339
|
// Change emitters (internal — called only via emitChange dispatch).
|
|
340
340
|
// =====================================================================
|
|
341
341
|
|
|
342
|
-
function emitAddColumn(
|
|
343
|
-
resourceName: string,
|
|
344
|
-
field: DdlFieldDef,
|
|
345
|
-
provider: SqlProvider,
|
|
346
|
-
): string {
|
|
347
|
-
validateAddColumn(resourceName, field, provider);
|
|
348
|
-
const table = quoteIdent(resourceName, provider);
|
|
349
|
-
const columnDef = emitColumnDef(field, provider);
|
|
350
|
-
const addColumn = `ALTER TABLE ${table} ADD COLUMN ${columnDef};`;
|
|
351
|
-
if (!field.indexed || field.unique || field.primary) return addColumn;
|
|
342
|
+
function emitAddColumn(
|
|
343
|
+
resourceName: string,
|
|
344
|
+
field: DdlFieldDef,
|
|
345
|
+
provider: SqlProvider,
|
|
346
|
+
): string {
|
|
347
|
+
validateAddColumn(resourceName, field, provider);
|
|
348
|
+
const table = quoteIdent(resourceName, provider);
|
|
349
|
+
const columnDef = emitColumnDef(field, provider);
|
|
350
|
+
const addColumn = `ALTER TABLE ${table} ADD COLUMN ${columnDef};`;
|
|
351
|
+
if (!field.indexed || field.unique || field.primary) return addColumn;
|
|
352
352
|
return [
|
|
353
353
|
addColumn,
|
|
354
354
|
emitCreateIndex(
|
|
@@ -358,85 +358,85 @@ function emitAddColumn(
|
|
|
358
358
|
false,
|
|
359
359
|
provider,
|
|
360
360
|
),
|
|
361
|
-
].join("\n");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function validateAddColumn(
|
|
365
|
-
resourceName: string,
|
|
366
|
-
field: DdlFieldDef,
|
|
367
|
-
provider: SqlProvider,
|
|
368
|
-
): void {
|
|
369
|
-
if (provider !== "sqlite") return;
|
|
370
|
-
|
|
371
|
-
if (field.primary) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
`SQLite cannot add PRIMARY KEY column "${field.name}" to existing table "${resourceName}" via ALTER TABLE. ` +
|
|
374
|
-
`Create a manual table-rebuild migration instead.`,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (field.unique) {
|
|
379
|
-
throw new Error(
|
|
380
|
-
`SQLite cannot add UNIQUE column "${field.name}" to existing table "${resourceName}" via ALTER TABLE. ` +
|
|
381
|
-
`Add a nullable/non-unique column first, backfill it, then create a unique index manually.`,
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const defaultIssue = sqliteAddColumnDefaultIssue(field);
|
|
386
|
-
if (defaultIssue) {
|
|
387
|
-
throw new Error(
|
|
388
|
-
`SQLite cannot add column "${field.name}" to existing table "${resourceName}" with this DEFAULT: ` +
|
|
389
|
-
`${defaultIssue} Use a scalar literal default or write a manual backfill migration.`,
|
|
390
|
-
);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (field.nullable) return;
|
|
394
|
-
if (field.default !== undefined && !isSqliteNullDefault(field.default)) return;
|
|
395
|
-
|
|
396
|
-
throw new Error(
|
|
397
|
-
`SQLite cannot add required column "${field.name}" to existing table "${resourceName}" without a non-NULL constant DEFAULT. ` +
|
|
398
|
-
`Add a scalar default, set required:false, or write a manual backfill migration.`,
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function sqliteAddColumnDefaultIssue(field: DdlFieldDef): string | null {
|
|
403
|
-
const def = field.default;
|
|
404
|
-
if (!def) return null;
|
|
405
|
-
if (def.kind === "now") {
|
|
406
|
-
return `default "now" maps to CURRENT_TIMESTAMP, which SQLite rejects in ADD COLUMN.`;
|
|
407
|
-
}
|
|
408
|
-
if (def.kind !== "sql") return null;
|
|
409
|
-
|
|
410
|
-
const expr = def.expr.trim();
|
|
411
|
-
if (/^CURRENT_(?:TIME|DATE|TIMESTAMP)\b/i.test(expr)) {
|
|
412
|
-
return `SQLite rejects CURRENT_TIME/CURRENT_DATE/CURRENT_TIMESTAMP in ADD COLUMN.`;
|
|
413
|
-
}
|
|
414
|
-
if (/\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(expr)) {
|
|
415
|
-
return `SQLite rejects non-constant function defaults in ADD COLUMN.`;
|
|
416
|
-
}
|
|
417
|
-
if (!isSqliteAddColumnConstantSqlDefault(expr)) {
|
|
418
|
-
return `SQLite ADD COLUMN only accepts literal constant defaults; raw SQL expression ${JSON.stringify(expr)} is not safe to emit automatically.`;
|
|
419
|
-
}
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function isSqliteNullDefault(def: NonNullable<DdlFieldDef["default"]>): boolean {
|
|
424
|
-
return def.kind === "null" || (def.kind === "sql" && /^NULL$/i.test(def.expr.trim()));
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function isSqliteAddColumnConstantSqlDefault(expr: string): boolean {
|
|
428
|
-
let value = expr.trim();
|
|
429
|
-
while (value.startsWith("(") && value.endsWith(")")) {
|
|
430
|
-
value = value.slice(1, -1).trim();
|
|
431
|
-
}
|
|
432
|
-
return (
|
|
433
|
-
/^NULL$/i.test(value) ||
|
|
434
|
-
/^(?:TRUE|FALSE)$/i.test(value) ||
|
|
435
|
-
/^[+-]?(?:\d+|\d+\.\d+|\.\d+)(?:e[+-]?\d+)?$/i.test(value) ||
|
|
436
|
-
/^'(?:''|[^'])*'$/.test(value) ||
|
|
437
|
-
/^X'(?:[0-9a-f]{2})*'$/i.test(value)
|
|
438
|
-
);
|
|
439
|
-
}
|
|
361
|
+
].join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function validateAddColumn(
|
|
365
|
+
resourceName: string,
|
|
366
|
+
field: DdlFieldDef,
|
|
367
|
+
provider: SqlProvider,
|
|
368
|
+
): void {
|
|
369
|
+
if (provider !== "sqlite") return;
|
|
370
|
+
|
|
371
|
+
if (field.primary) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`SQLite cannot add PRIMARY KEY column "${field.name}" to existing table "${resourceName}" via ALTER TABLE. ` +
|
|
374
|
+
`Create a manual table-rebuild migration instead.`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (field.unique) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`SQLite cannot add UNIQUE column "${field.name}" to existing table "${resourceName}" via ALTER TABLE. ` +
|
|
381
|
+
`Add a nullable/non-unique column first, backfill it, then create a unique index manually.`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const defaultIssue = sqliteAddColumnDefaultIssue(field);
|
|
386
|
+
if (defaultIssue) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`SQLite cannot add column "${field.name}" to existing table "${resourceName}" with this DEFAULT: ` +
|
|
389
|
+
`${defaultIssue} Use a scalar literal default or write a manual backfill migration.`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (field.nullable) return;
|
|
394
|
+
if (field.default !== undefined && !isSqliteNullDefault(field.default)) return;
|
|
395
|
+
|
|
396
|
+
throw new Error(
|
|
397
|
+
`SQLite cannot add required column "${field.name}" to existing table "${resourceName}" without a non-NULL constant DEFAULT. ` +
|
|
398
|
+
`Add a scalar default, set required:false, or write a manual backfill migration.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function sqliteAddColumnDefaultIssue(field: DdlFieldDef): string | null {
|
|
403
|
+
const def = field.default;
|
|
404
|
+
if (!def) return null;
|
|
405
|
+
if (def.kind === "now") {
|
|
406
|
+
return `default "now" maps to CURRENT_TIMESTAMP, which SQLite rejects in ADD COLUMN.`;
|
|
407
|
+
}
|
|
408
|
+
if (def.kind !== "sql") return null;
|
|
409
|
+
|
|
410
|
+
const expr = def.expr.trim();
|
|
411
|
+
if (/^CURRENT_(?:TIME|DATE|TIMESTAMP)\b/i.test(expr)) {
|
|
412
|
+
return `SQLite rejects CURRENT_TIME/CURRENT_DATE/CURRENT_TIMESTAMP in ADD COLUMN.`;
|
|
413
|
+
}
|
|
414
|
+
if (/\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(expr)) {
|
|
415
|
+
return `SQLite rejects non-constant function defaults in ADD COLUMN.`;
|
|
416
|
+
}
|
|
417
|
+
if (!isSqliteAddColumnConstantSqlDefault(expr)) {
|
|
418
|
+
return `SQLite ADD COLUMN only accepts literal constant defaults; raw SQL expression ${JSON.stringify(expr)} is not safe to emit automatically.`;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isSqliteNullDefault(def: NonNullable<DdlFieldDef["default"]>): boolean {
|
|
424
|
+
return def.kind === "null" || (def.kind === "sql" && /^NULL$/i.test(def.expr.trim()));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isSqliteAddColumnConstantSqlDefault(expr: string): boolean {
|
|
428
|
+
let value = expr.trim();
|
|
429
|
+
while (value.startsWith("(") && value.endsWith(")")) {
|
|
430
|
+
value = value.slice(1, -1).trim();
|
|
431
|
+
}
|
|
432
|
+
return (
|
|
433
|
+
/^NULL$/i.test(value) ||
|
|
434
|
+
/^(?:TRUE|FALSE)$/i.test(value) ||
|
|
435
|
+
/^[+-]?(?:\d+|\d+\.\d+|\.\d+)(?:e[+-]?\d+)?$/i.test(value) ||
|
|
436
|
+
/^'(?:''|[^'])*'$/.test(value) ||
|
|
437
|
+
/^X'(?:[0-9a-f]{2})*'$/i.test(value)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
440
|
|
|
441
441
|
/**
|
|
442
442
|
* DROP COLUMN.
|
|
@@ -462,8 +462,8 @@ function emitDropColumn(
|
|
|
462
462
|
*
|
|
463
463
|
* Output: a multi-line SQL comment block naming the resource + field +
|
|
464
464
|
* fromType → toType, followed by the literal TODO message and a no-op
|
|
465
|
-
* a deliberate failing sentinel so the migration runner refuses to mark
|
|
466
|
-
* this TODO as applied until the operator edits the migration manually.
|
|
465
|
+
* a deliberate failing sentinel so the migration runner refuses to mark
|
|
466
|
+
* this TODO as applied until the operator edits the migration manually.
|
|
467
467
|
*/
|
|
468
468
|
function emitAlterColumnTypeStub(
|
|
469
469
|
resourceName: string,
|
|
@@ -477,13 +477,13 @@ function emitAlterColumnTypeStub(
|
|
|
477
477
|
`-- from: ${fromType}`,
|
|
478
478
|
`-- to: ${toType}`,
|
|
479
479
|
`-- TODO: Mandu does not auto-generate ALTER COLUMN TYPE in v1.`,
|
|
480
|
-
`-- Please write the migration manually, then re-run \`mandu db apply\`.`,
|
|
481
|
-
`-- ================================================================`,
|
|
482
|
-
manualMigrationRequiredStatement(
|
|
483
|
-
`Mandu cannot auto-generate ALTER COLUMN TYPE for ${resourceName}.${fieldName}`,
|
|
484
|
-
),
|
|
485
|
-
].join("\n");
|
|
486
|
-
}
|
|
480
|
+
`-- Please write the migration manually, then re-run \`mandu db apply\`.`,
|
|
481
|
+
`-- ================================================================`,
|
|
482
|
+
manualMigrationRequiredStatement(
|
|
483
|
+
`Mandu cannot auto-generate ALTER COLUMN TYPE for ${resourceName}.${fieldName}`,
|
|
484
|
+
),
|
|
485
|
+
].join("\n");
|
|
486
|
+
}
|
|
487
487
|
|
|
488
488
|
function emitAlterColumnNullable(
|
|
489
489
|
resourceName: string,
|
|
@@ -500,34 +500,34 @@ function emitAlterColumnNullable(
|
|
|
500
500
|
: `ALTER TABLE ${table} ALTER COLUMN ${col} SET NOT NULL;`;
|
|
501
501
|
}
|
|
502
502
|
if (provider === "sqlite") {
|
|
503
|
-
// SQLite cannot toggle NOT NULL on an existing column without a full
|
|
504
|
-
// table recreate. Emit a failing stub so the user handles it manually.
|
|
505
|
-
return [
|
|
506
|
-
`-- ================================================================`,
|
|
507
|
-
`-- Nullability change: ${resourceName}.${fieldName} → ${nullable ? "NULL" : "NOT NULL"}`,
|
|
508
|
-
`-- TODO: SQLite cannot toggle NOT NULL in place. Recreate the table`,
|
|
509
|
-
`-- manually (CREATE new, INSERT SELECT, DROP old, RENAME) and re-run.`,
|
|
510
|
-
`-- ================================================================`,
|
|
511
|
-
manualMigrationRequiredStatement(
|
|
512
|
-
`SQLite nullability change requires manual table rebuild for ${resourceName}.${fieldName}`,
|
|
513
|
-
),
|
|
514
|
-
].join("\n");
|
|
515
|
-
}
|
|
503
|
+
// SQLite cannot toggle NOT NULL on an existing column without a full
|
|
504
|
+
// table recreate. Emit a failing stub so the user handles it manually.
|
|
505
|
+
return [
|
|
506
|
+
`-- ================================================================`,
|
|
507
|
+
`-- Nullability change: ${resourceName}.${fieldName} → ${nullable ? "NULL" : "NOT NULL"}`,
|
|
508
|
+
`-- TODO: SQLite cannot toggle NOT NULL in place. Recreate the table`,
|
|
509
|
+
`-- manually (CREATE new, INSERT SELECT, DROP old, RENAME) and re-run.`,
|
|
510
|
+
`-- ================================================================`,
|
|
511
|
+
manualMigrationRequiredStatement(
|
|
512
|
+
`SQLite nullability change requires manual table rebuild for ${resourceName}.${fieldName}`,
|
|
513
|
+
),
|
|
514
|
+
].join("\n");
|
|
515
|
+
}
|
|
516
516
|
// MySQL requires the full column spec; we cannot reconstruct it here.
|
|
517
517
|
// Emit a stub — Agent B's diff emits `alter-column-nullable` only when
|
|
518
518
|
// everything else matches, so this is narrow-scope.
|
|
519
519
|
return [
|
|
520
520
|
`-- ================================================================`,
|
|
521
521
|
`-- Nullability change: ${resourceName}.${fieldName} → ${nullable ? "NULL" : "NOT NULL"}`,
|
|
522
|
-
`-- TODO: MySQL MODIFY COLUMN requires the full column type; Mandu v1`,
|
|
523
|
-
`-- cannot emit this automatically. Please edit this migration to use`,
|
|
524
|
-
`-- \`ALTER TABLE ${resourceName} MODIFY COLUMN ${fieldName} <TYPE> ${nullable ? "NULL" : "NOT NULL"}\``,
|
|
525
|
-
`-- ================================================================`,
|
|
526
|
-
manualMigrationRequiredStatement(
|
|
527
|
-
`MySQL nullability change requires manual MODIFY COLUMN for ${resourceName}.${fieldName}`,
|
|
528
|
-
),
|
|
529
|
-
].join("\n");
|
|
530
|
-
}
|
|
522
|
+
`-- TODO: MySQL MODIFY COLUMN requires the full column type; Mandu v1`,
|
|
523
|
+
`-- cannot emit this automatically. Please edit this migration to use`,
|
|
524
|
+
`-- \`ALTER TABLE ${resourceName} MODIFY COLUMN ${fieldName} <TYPE> ${nullable ? "NULL" : "NOT NULL"}\``,
|
|
525
|
+
`-- ================================================================`,
|
|
526
|
+
manualMigrationRequiredStatement(
|
|
527
|
+
`MySQL nullability change requires manual MODIFY COLUMN for ${resourceName}.${fieldName}`,
|
|
528
|
+
),
|
|
529
|
+
].join("\n");
|
|
530
|
+
}
|
|
531
531
|
|
|
532
532
|
function emitAlterColumnDefault(
|
|
533
533
|
resourceName: string,
|
|
@@ -543,17 +543,17 @@ function emitAlterColumnDefault(
|
|
|
543
543
|
: `ALTER TABLE ${table} ALTER COLUMN ${col} DROP DEFAULT;`;
|
|
544
544
|
}
|
|
545
545
|
if (provider === "sqlite") {
|
|
546
|
-
// Same constraint as nullability — SQLite needs a table recreate.
|
|
547
|
-
return [
|
|
548
|
-
`-- ================================================================`,
|
|
549
|
-
`-- Default change: ${resourceName}.${fieldName}`,
|
|
550
|
-
`-- TODO: SQLite cannot ALTER DEFAULT in place. Recreate the table.`,
|
|
551
|
-
`-- ================================================================`,
|
|
552
|
-
manualMigrationRequiredStatement(
|
|
553
|
-
`SQLite default change requires manual table rebuild for ${resourceName}.${fieldName}`,
|
|
554
|
-
),
|
|
555
|
-
].join("\n");
|
|
556
|
-
}
|
|
546
|
+
// Same constraint as nullability — SQLite needs a table recreate.
|
|
547
|
+
return [
|
|
548
|
+
`-- ================================================================`,
|
|
549
|
+
`-- Default change: ${resourceName}.${fieldName}`,
|
|
550
|
+
`-- TODO: SQLite cannot ALTER DEFAULT in place. Recreate the table.`,
|
|
551
|
+
`-- ================================================================`,
|
|
552
|
+
manualMigrationRequiredStatement(
|
|
553
|
+
`SQLite default change requires manual table rebuild for ${resourceName}.${fieldName}`,
|
|
554
|
+
),
|
|
555
|
+
].join("\n");
|
|
556
|
+
}
|
|
557
557
|
// MySQL — ALTER COLUMN ... SET DEFAULT / DROP DEFAULT is actually supported.
|
|
558
558
|
return def
|
|
559
559
|
? `ALTER TABLE ${table} ALTER COLUMN ${col} SET DEFAULT ${resolveDefault(def, provider)};`
|
|
@@ -597,7 +597,7 @@ function emitRenameTable(
|
|
|
597
597
|
return `ALTER TABLE ${from} RENAME TO ${to};`;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
function emitRenameColumn(
|
|
600
|
+
function emitRenameColumn(
|
|
601
601
|
resourceName: string,
|
|
602
602
|
oldName: string,
|
|
603
603
|
newName: string,
|
|
@@ -609,15 +609,15 @@ function emitRenameColumn(
|
|
|
609
609
|
// All three dialects use `ALTER TABLE ... RENAME COLUMN ... TO ...` in
|
|
610
610
|
// their modern versions (PG >=9.2, MySQL >=8.0.3, SQLite >=3.25).
|
|
611
611
|
return `ALTER TABLE ${table} RENAME COLUMN ${from} TO ${to};`;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function manualMigrationRequiredStatement(message: string): string {
|
|
615
|
-
return `SELECT mandu_manual_migration_required(${sqlStringLiteral(message)});`;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function sqlStringLiteral(value: string): string {
|
|
619
|
-
return `'${value.replace(/'/g, "''")}'`;
|
|
620
|
-
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function manualMigrationRequiredStatement(message: string): string {
|
|
615
|
+
return `SELECT mandu_manual_migration_required(${sqlStringLiteral(message)});`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function sqlStringLiteral(value: string): string {
|
|
619
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
620
|
+
}
|
|
621
621
|
|
|
622
622
|
// =====================================================================
|
|
623
623
|
// Validation helpers.
|
|
@@ -363,17 +363,17 @@ ${result.migrationSql}`;
|
|
|
363
363
|
// ============================================
|
|
364
364
|
|
|
365
365
|
/**
|
|
366
|
-
* Compose the sequence of `Change` → SQL emission.
|
|
367
|
-
*
|
|
368
|
-
* Do not wrap with `BEGIN` / `COMMIT`: the migration runner already applies
|
|
369
|
-
* each file inside a transaction, and SQLite rejects nested BEGIN with
|
|
370
|
-
* "cannot start a transaction within a transaction".
|
|
371
|
-
*/
|
|
372
|
-
function composeMigrationSql(changes: readonly Change[], provider: SqlProvider): string {
|
|
373
|
-
const body = emitChanges(changes, provider);
|
|
374
|
-
if (body.length === 0) return "";
|
|
375
|
-
return body;
|
|
376
|
-
}
|
|
366
|
+
* Compose the sequence of `Change` → SQL emission.
|
|
367
|
+
*
|
|
368
|
+
* Do not wrap with `BEGIN` / `COMMIT`: the migration runner already applies
|
|
369
|
+
* each file inside a transaction, and SQLite rejects nested BEGIN with
|
|
370
|
+
* "cannot start a transaction within a transaction".
|
|
371
|
+
*/
|
|
372
|
+
function composeMigrationSql(changes: readonly Change[], provider: SqlProvider): string {
|
|
373
|
+
const body = emitChanges(changes, provider);
|
|
374
|
+
if (body.length === 0) return "";
|
|
375
|
+
return body;
|
|
376
|
+
}
|
|
377
377
|
|
|
378
378
|
// ============================================
|
|
379
379
|
// Internals — filesystem I/O
|
|
@@ -19,7 +19,7 @@ export function generateResourceSlot(definition: ResourceDefinition): string {
|
|
|
19
19
|
const endpoints = getEnabledEndpoints(definition);
|
|
20
20
|
|
|
21
21
|
// Generate endpoint handlers
|
|
22
|
-
const handlers = generateHandlers(definition, endpoints, pascalName, pluralName);
|
|
22
|
+
const handlers = generateHandlers(definition, endpoints, pascalName, pluralName);
|
|
23
23
|
|
|
24
24
|
return `// 🥟 Mandu Filling - ${resourceName} Resource
|
|
25
25
|
// Pattern: /api/${pluralName}
|
|
@@ -46,33 +46,33 @@ ${handlers}
|
|
|
46
46
|
/**
|
|
47
47
|
* Generate handlers for enabled endpoints
|
|
48
48
|
*/
|
|
49
|
-
function generateHandlers(
|
|
50
|
-
definition: ResourceDefinition,
|
|
51
|
-
endpoints: string[],
|
|
52
|
-
pascalName: string,
|
|
53
|
-
pluralName: string
|
|
54
|
-
): string {
|
|
55
|
-
const handlers: string[] = [];
|
|
56
|
-
|
|
57
|
-
if (endpoints.includes("list")) {
|
|
58
|
-
handlers.push(generateListHandler(pascalName, pluralName));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (endpoints.includes("get")) {
|
|
62
|
-
handlers.push(generateGetHandler(pascalName, pluralName));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (endpoints.includes("create")) {
|
|
66
|
-
handlers.push(generateCreateHandler(pascalName, pluralName));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (endpoints.includes("update")) {
|
|
70
|
-
handlers.push(generateUpdateHandler(pascalName, pluralName));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (endpoints.includes("delete")) {
|
|
74
|
-
handlers.push(generateDeleteHandler(pascalName, pluralName));
|
|
75
|
-
}
|
|
49
|
+
function generateHandlers(
|
|
50
|
+
definition: ResourceDefinition,
|
|
51
|
+
endpoints: string[],
|
|
52
|
+
pascalName: string,
|
|
53
|
+
pluralName: string
|
|
54
|
+
): string {
|
|
55
|
+
const handlers: string[] = [];
|
|
56
|
+
|
|
57
|
+
if (endpoints.includes("list")) {
|
|
58
|
+
handlers.push(generateListHandler(pascalName, pluralName));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (endpoints.includes("get")) {
|
|
62
|
+
handlers.push(generateGetHandler(pascalName, pluralName));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (endpoints.includes("create")) {
|
|
66
|
+
handlers.push(generateCreateHandler(pascalName, pluralName));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (endpoints.includes("update")) {
|
|
70
|
+
handlers.push(generateUpdateHandler(pascalName, pluralName));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (endpoints.includes("delete")) {
|
|
74
|
+
handlers.push(generateDeleteHandler(pascalName, pluralName));
|
|
75
|
+
}
|
|
76
76
|
|
|
77
77
|
return handlers.join("\n\n");
|
|
78
78
|
}
|
|
@@ -80,16 +80,16 @@ function generateHandlers(
|
|
|
80
80
|
/**
|
|
81
81
|
* Generate LIST handler (GET /api/resources)
|
|
82
82
|
*/
|
|
83
|
-
function generateListHandler(pascalName: string, pluralName: string): string {
|
|
84
|
-
return ` // 📋 List ${pascalName}s
|
|
85
|
-
.get(async (ctx) => {
|
|
86
|
-
const input = await ctx.input(contract, "GET", ctx.params);
|
|
87
|
-
const { page, limit } = input;
|
|
88
|
-
|
|
89
|
-
// TODO: Implement database query
|
|
90
|
-
// const offset = (page - 1) * limit;
|
|
91
|
-
// const items = await db.select().from(${pluralName}).limit(limit).offset(offset);
|
|
92
|
-
// const total = await db.select({ count: count() }).from(${pluralName});
|
|
83
|
+
function generateListHandler(pascalName: string, pluralName: string): string {
|
|
84
|
+
return ` // 📋 List ${pascalName}s
|
|
85
|
+
.get(async (ctx) => {
|
|
86
|
+
const input = await ctx.input(contract, "GET", ctx.params);
|
|
87
|
+
const { page, limit } = input;
|
|
88
|
+
|
|
89
|
+
// TODO: Implement database query
|
|
90
|
+
// const offset = (page - 1) * limit;
|
|
91
|
+
// const items = await db.select().from(${pluralName}).limit(limit).offset(offset);
|
|
92
|
+
// const total = await db.select({ count: count() }).from(${pluralName});
|
|
93
93
|
|
|
94
94
|
const mockData = {
|
|
95
95
|
data: [], // Replace with actual data
|
|
@@ -107,14 +107,14 @@ function generateListHandler(pascalName: string, pluralName: string): string {
|
|
|
107
107
|
/**
|
|
108
108
|
* Generate GET handler (GET /api/resources/:id)
|
|
109
109
|
*/
|
|
110
|
-
function generateGetHandler(pascalName: string, pluralName: string): string {
|
|
111
|
-
return ` // 📄 Get Single ${pascalName}
|
|
112
|
-
.get(async (ctx) => {
|
|
113
|
-
const { id } = ctx.params;
|
|
114
|
-
|
|
115
|
-
// TODO: Implement database query
|
|
116
|
-
// const item = await db.select().from(${pluralName}).where(eq(${pluralName}.id, id)).limit(1);
|
|
117
|
-
// if (!item) return ctx.notFound("${pascalName} not found");
|
|
110
|
+
function generateGetHandler(pascalName: string, pluralName: string): string {
|
|
111
|
+
return ` // 📄 Get Single ${pascalName}
|
|
112
|
+
.get(async (ctx) => {
|
|
113
|
+
const { id } = ctx.params;
|
|
114
|
+
|
|
115
|
+
// TODO: Implement database query
|
|
116
|
+
// const item = await db.select().from(${pluralName}).where(eq(${pluralName}.id, id)).limit(1);
|
|
117
|
+
// if (!item) return ctx.notFound("${pascalName} not found");
|
|
118
118
|
|
|
119
119
|
const mockData = {
|
|
120
120
|
data: { id, message: "${pascalName} details" }, // Replace with actual data
|
|
@@ -127,13 +127,13 @@ function generateGetHandler(pascalName: string, pluralName: string): string {
|
|
|
127
127
|
/**
|
|
128
128
|
* Generate CREATE handler (POST /api/resources)
|
|
129
129
|
*/
|
|
130
|
-
function generateCreateHandler(pascalName: string, pluralName: string): string {
|
|
131
|
-
return ` // ➕ Create ${pascalName}
|
|
132
|
-
.post(async (ctx) => {
|
|
133
|
-
const input = await ctx.input(contract, "POST", ctx.params);
|
|
134
|
-
|
|
135
|
-
// TODO: Implement database insertion
|
|
136
|
-
// const [created] = await db.insert(${pluralName}).values(input).returning();
|
|
130
|
+
function generateCreateHandler(pascalName: string, pluralName: string): string {
|
|
131
|
+
return ` // ➕ Create ${pascalName}
|
|
132
|
+
.post(async (ctx) => {
|
|
133
|
+
const input = await ctx.input(contract, "POST", ctx.params);
|
|
134
|
+
|
|
135
|
+
// TODO: Implement database insertion
|
|
136
|
+
// const [created] = await db.insert(${pluralName}).values(input).returning();
|
|
137
137
|
|
|
138
138
|
const mockData = {
|
|
139
139
|
data: { id: "new-id", ...input }, // Replace with actual created data
|
|
@@ -146,17 +146,17 @@ function generateCreateHandler(pascalName: string, pluralName: string): string {
|
|
|
146
146
|
/**
|
|
147
147
|
* Generate UPDATE handler (PUT /api/resources/:id)
|
|
148
148
|
*/
|
|
149
|
-
function generateUpdateHandler(pascalName: string, pluralName: string): string {
|
|
150
|
-
return ` // ✏️ Update ${pascalName}
|
|
151
|
-
.put(async (ctx) => {
|
|
152
|
-
const { id } = ctx.params;
|
|
153
|
-
const input = await ctx.input(contract, "PUT", ctx.params);
|
|
154
|
-
|
|
155
|
-
// TODO: Implement database update
|
|
156
|
-
// const [updated] = await db.update(${pluralName})
|
|
157
|
-
// .set(input)
|
|
158
|
-
// .where(eq(${pluralName}.id, id))
|
|
159
|
-
// .returning();
|
|
149
|
+
function generateUpdateHandler(pascalName: string, pluralName: string): string {
|
|
150
|
+
return ` // ✏️ Update ${pascalName}
|
|
151
|
+
.put(async (ctx) => {
|
|
152
|
+
const { id } = ctx.params;
|
|
153
|
+
const input = await ctx.input(contract, "PUT", ctx.params);
|
|
154
|
+
|
|
155
|
+
// TODO: Implement database update
|
|
156
|
+
// const [updated] = await db.update(${pluralName})
|
|
157
|
+
// .set(input)
|
|
158
|
+
// .where(eq(${pluralName}.id, id))
|
|
159
|
+
// .returning();
|
|
160
160
|
// if (!updated) return ctx.notFound("${pascalName} not found");
|
|
161
161
|
|
|
162
162
|
const mockData = {
|
|
@@ -170,14 +170,14 @@ function generateUpdateHandler(pascalName: string, pluralName: string): string {
|
|
|
170
170
|
/**
|
|
171
171
|
* Generate DELETE handler (DELETE /api/resources/:id)
|
|
172
172
|
*/
|
|
173
|
-
function generateDeleteHandler(pascalName: string, pluralName: string): string {
|
|
174
|
-
return ` // 🗑️ Delete ${pascalName}
|
|
175
|
-
.delete(async (ctx) => {
|
|
176
|
-
const { id } = ctx.params;
|
|
177
|
-
|
|
178
|
-
// TODO: Implement database deletion
|
|
179
|
-
// const deleted = await db.delete(${pluralName}).where(eq(${pluralName}.id, id));
|
|
180
|
-
// if (!deleted) return ctx.notFound("${pascalName} not found");
|
|
173
|
+
function generateDeleteHandler(pascalName: string, pluralName: string): string {
|
|
174
|
+
return ` // 🗑️ Delete ${pascalName}
|
|
175
|
+
.delete(async (ctx) => {
|
|
176
|
+
const { id } = ctx.params;
|
|
177
|
+
|
|
178
|
+
// TODO: Implement database deletion
|
|
179
|
+
// const deleted = await db.delete(${pluralName}).where(eq(${pluralName}.id, id));
|
|
180
|
+
// if (!deleted) return ctx.notFound("${pascalName} not found");
|
|
181
181
|
|
|
182
182
|
return ctx.output(contract, 200, { data: { message: "${pascalName} deleted" } });
|
|
183
183
|
})`;
|
package/src/resource/schema.ts
CHANGED
|
@@ -237,27 +237,27 @@ function validateField(resourceName: string, fieldName: string, field: ResourceF
|
|
|
237
237
|
/**
|
|
238
238
|
* Get plural name for resource
|
|
239
239
|
*/
|
|
240
|
-
export function getPluralName(definition: ResourceDefinition): string {
|
|
241
|
-
if (definition.options?.pluralName) {
|
|
242
|
-
return definition.options.pluralName;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (definition.options?.autoPlural === false) {
|
|
246
|
-
return definition.name;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return pluralize(definition.name);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function pluralize(singular: string): string {
|
|
253
|
-
if (/[^aeiou]y$/i.test(singular)) {
|
|
254
|
-
return singular.slice(0, -1) + "ies";
|
|
255
|
-
}
|
|
256
|
-
if (/(?:s|x|z|ch|sh)$/i.test(singular)) {
|
|
257
|
-
return singular + "es";
|
|
258
|
-
}
|
|
259
|
-
return singular + "s";
|
|
260
|
-
}
|
|
240
|
+
export function getPluralName(definition: ResourceDefinition): string {
|
|
241
|
+
if (definition.options?.pluralName) {
|
|
242
|
+
return definition.options.pluralName;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (definition.options?.autoPlural === false) {
|
|
246
|
+
return definition.name;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return pluralize(definition.name);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function pluralize(singular: string): string {
|
|
253
|
+
if (/[^aeiou]y$/i.test(singular)) {
|
|
254
|
+
return singular.slice(0, -1) + "ies";
|
|
255
|
+
}
|
|
256
|
+
if (/(?:s|x|z|ch|sh)$/i.test(singular)) {
|
|
257
|
+
return singular + "es";
|
|
258
|
+
}
|
|
259
|
+
return singular + "s";
|
|
260
|
+
}
|
|
261
261
|
|
|
262
262
|
/**
|
|
263
263
|
* Get enabled endpoints
|