@mandujs/core 0.54.11 → 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.
Files changed (76) hide show
  1. package/package.json +208 -200
  2. package/scripts/postinstall-lock.ts +153 -153
  3. package/src/a11y/run-audit.ts +15 -15
  4. package/src/agent/__tests__/context.test.ts +49 -1
  5. package/src/agent/context.ts +535 -535
  6. package/src/agent/index.ts +6 -6
  7. package/src/agent/plan.ts +282 -282
  8. package/src/agent/repair.ts +171 -171
  9. package/src/agent/sync.ts +200 -200
  10. package/src/agent/types.ts +8 -0
  11. package/src/agent/verify.ts +100 -2
  12. package/src/brain/doctor/analyzer.ts +7 -7
  13. package/src/bundler/__tests__/build-runner.ts +5 -4
  14. package/src/bundler/__tests__/cold-start.test.ts +60 -60
  15. package/src/bundler/__tests__/css.test.ts +20 -20
  16. package/src/bundler/analyzer.ts +15 -15
  17. package/src/bundler/build.test.ts +73 -7
  18. package/src/bundler/build.ts +139 -31
  19. package/src/bundler/css.ts +42 -42
  20. package/src/bundler/manifest-schema.ts +21 -21
  21. package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +13 -13
  22. package/src/bundler/plugins/block-generated-imports.ts +13 -13
  23. package/src/bundler/types.ts +31 -31
  24. package/src/client/island.ts +79 -79
  25. package/src/config/validate.ts +1 -1
  26. package/src/contract/schema.ts +7 -0
  27. package/src/deploy/inference/context.ts +82 -82
  28. package/src/devtools/client/components/panel/islands-panel.tsx +16 -16
  29. package/src/devtools/client/components/panel/panel-container.tsx +1 -1
  30. package/src/error/formatter.ts +10 -1
  31. package/src/experimental/index.ts +10 -0
  32. package/src/filling/context.ts +17 -17
  33. package/src/filling/filling.ts +22 -1
  34. package/src/filling/index.ts +15 -1
  35. package/src/generator/generate.ts +30 -30
  36. package/src/generator/index.ts +3 -3
  37. package/src/generator/templates.ts +210 -210
  38. package/src/guard/check.ts +9 -9
  39. package/src/guard/config-guard.ts +13 -13
  40. package/src/guard/fs-routes-policy.ts +51 -51
  41. package/src/guard/index.ts +11 -11
  42. package/src/index.ts +0 -10
  43. package/src/internal/index.ts +25 -0
  44. package/src/kitchen/api/file-api.ts +11 -11
  45. package/src/report/index.ts +1 -1
  46. package/src/resource/__tests__/generator.test.ts +6 -6
  47. package/src/resource/__tests__/schema.test.ts +14 -14
  48. package/src/resource/ddl/__tests__/emit.test.ts +165 -165
  49. package/src/resource/ddl/emit.ts +146 -146
  50. package/src/resource/generator-schema.ts +11 -11
  51. package/src/resource/generators/slot.ts +72 -72
  52. package/src/resource/schema.ts +21 -21
  53. package/src/router/client-entry.test.ts +69 -33
  54. package/src/router/client-entry.ts +134 -74
  55. package/src/router/fs-routes.ts +24 -22
  56. package/src/router/fs-scanner.ts +21 -17
  57. package/src/router/fs-types.ts +8 -5
  58. package/src/runtime/__tests__/devtools-adapter.test.ts +68 -68
  59. package/src/runtime/__tests__/observability-lifecycle.test.ts +103 -103
  60. package/src/runtime/__tests__/page-render-response.test.ts +103 -103
  61. package/src/runtime/__tests__/request-middleware.test.ts +70 -70
  62. package/src/runtime/devtools-adapter.ts +68 -68
  63. package/src/runtime/escape.ts +34 -34
  64. package/src/runtime/image-feature.ts +15 -0
  65. package/src/runtime/observability-lifecycle.ts +290 -290
  66. package/src/runtime/page-render-response.ts +106 -106
  67. package/src/runtime/rate-limit.ts +231 -0
  68. package/src/runtime/request-middleware.ts +31 -31
  69. package/src/runtime/scheduler-lifecycle.ts +64 -0
  70. package/src/runtime/server.ts +27 -295
  71. package/src/runtime/ssr.ts +59 -59
  72. package/src/runtime/static-files.ts +289 -289
  73. package/src/runtime/streaming-ssr.ts +22 -22
  74. package/src/spec/schema.ts +4 -3
  75. package/src/watcher/__tests__/watcher.test.ts +59 -59
  76. package/src/watcher/watcher.ts +61 -61
@@ -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
  })`;
@@ -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