@yrest/cli 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -725,11 +725,12 @@ function normaliseSseRoute(raw) {
725
725
  return null;
726
726
  }
727
727
  const s = sse;
728
+ const handler = typeof s["_handler"] === "string" && s["_handler"].trim() ? s["_handler"].trim() : void 0;
728
729
  const rawEvents = s["_events"];
729
- if (!Array.isArray(rawEvents)) return null;
730
- const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
731
- if (!events.length) return null;
730
+ const events = Array.isArray(rawEvents) ? rawEvents.map(normaliseEvent).filter((e) => e !== null) : [];
731
+ if (!handler && !events.length) return null;
732
732
  return {
733
+ ...handler ? { handler } : {},
733
734
  path,
734
735
  interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
735
736
  loop: s["_loop"] !== false,
@@ -1270,6 +1271,10 @@ function handlersAccordion(handlers, routes, base) {
1270
1271
  function sseRoutesAccordion(routes, base) {
1271
1272
  if (!routes.length) return "";
1272
1273
  const rows = routes.map((r) => {
1274
+ if (r.handler) {
1275
+ const desc2 = `SSE stream&ensp;<span style="color:#0ea5e9;font-size:11px">\u2192 ${escapeHtml(r.handler)}()</span>`;
1276
+ return endpointRow("SSE", `${base}${r.path}`, desc2);
1277
+ }
1273
1278
  const tags = [
1274
1279
  `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
1275
1280
  `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
@@ -2568,6 +2573,32 @@ var CollectionRouteCommand = class {
2568
2573
  }
2569
2574
  };
2570
2575
 
2576
+ // src/api/collection.ts
2577
+ function createCollectionHandle(storage, name, idStrategy = "increment") {
2578
+ if (!storage.getCollection(name)) {
2579
+ const known = Object.keys(storage.getData()).join(", ") || "none";
2580
+ throw new Error(`Collection "${name}" does not exist (known collections: ${known})`);
2581
+ }
2582
+ const items = () => storage.getCollection(name) ?? [];
2583
+ const copy = (item) => item === void 0 ? void 0 : structuredClone(item);
2584
+ return {
2585
+ findAll: () => items().map((i) => copy(i)),
2586
+ findById: (id) => copy(findById(items(), String(id))),
2587
+ findWhere: (predicate) => items().map((i) => copy(i)).filter(predicate),
2588
+ count: () => items().length,
2589
+ create: (data) => copy(createItem(storage, name, { ...data }, idStrategy)),
2590
+ update: (id, patch) => {
2591
+ const rest = { ...patch };
2592
+ delete rest["id"];
2593
+ return copy(patchItem(storage, name, String(id), rest));
2594
+ },
2595
+ delete: (id) => copy(deleteItem(storage, name, String(id)))
2596
+ };
2597
+ }
2598
+ function collectionAccessor(storage, idStrategy = "increment") {
2599
+ return (name) => createCollectionHandle(storage, name, idStrategy);
2600
+ }
2601
+
2571
2602
  // src/utils/conditions.ts
2572
2603
  function resolveRequestPath(dotPath, req) {
2573
2604
  const [root, ...rest] = dotPath.split(".");
@@ -2630,15 +2661,20 @@ function resolveBody(body, ctx) {
2630
2661
  return body ?? null;
2631
2662
  }
2632
2663
  var CustomRouteCommand = class {
2633
- constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
2664
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map(), idStrategy = "increment") {
2634
2665
  this.storage = storage;
2635
2666
  this.base = base;
2636
2667
  this.handlers = handlers;
2668
+ this.idStrategy = idStrategy;
2637
2669
  }
2638
2670
  storage;
2639
2671
  base;
2640
2672
  handlers;
2673
+ idStrategy;
2641
2674
  register(server) {
2675
+ const handlerCtx = {
2676
+ collection: collectionAccessor(this.storage, this.idStrategy)
2677
+ };
2642
2678
  for (const route of this.storage.getRoutes()) {
2643
2679
  const method = route.method?.toUpperCase();
2644
2680
  const path = route.path;
@@ -2654,7 +2690,7 @@ var CustomRouteCommand = class {
2654
2690
  url,
2655
2691
  handler: async (req, reply) => {
2656
2692
  if (route.delay && route.delay > 0) {
2657
- await new Promise((resolve6) => setTimeout(resolve6, route.delay));
2693
+ await new Promise((resolve7) => setTimeout(resolve7, route.delay));
2658
2694
  }
2659
2695
  if (route.error) {
2660
2696
  const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
@@ -2675,7 +2711,7 @@ var CustomRouteCommand = class {
2675
2711
  body: req.body,
2676
2712
  headers: req.headers
2677
2713
  };
2678
- const result = await fn(ctx2);
2714
+ const result = await fn(ctx2, handlerCtx);
2679
2715
  const resStatus = result.status ?? 200;
2680
2716
  for (const [k, v] of Object.entries(result.headers ?? {})) {
2681
2717
  reply.header(k, v);
@@ -3278,26 +3314,64 @@ var SnapshotRouteCommand = class {
3278
3314
 
3279
3315
  // src/router/routes/sse.routes.ts
3280
3316
  var SSERouteCommand = class {
3281
- constructor(storage, base) {
3317
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map(), idStrategy = "increment") {
3282
3318
  this.storage = storage;
3283
3319
  this.base = base;
3320
+ this.handlers = handlers;
3321
+ this.idStrategy = idStrategy;
3284
3322
  }
3285
3323
  storage;
3286
3324
  base;
3325
+ handlers;
3326
+ idStrategy;
3287
3327
  register(server) {
3328
+ const handlerCtx = {
3329
+ collection: collectionAccessor(this.storage, this.idStrategy)
3330
+ };
3288
3331
  for (const route of this.storage.getSseRoutes()) {
3289
3332
  const url = `${this.base}${route.path}`;
3290
3333
  server.route({
3291
3334
  method: "GET",
3292
3335
  url,
3293
3336
  handler: async (req, reply) => {
3337
+ if (route.handler) {
3338
+ const fn = this.handlers.get(route.handler);
3339
+ if (!fn) {
3340
+ return reply.status(501).send({ error: `Handler "${route.handler}" is not defined in the handlers file` });
3341
+ }
3342
+ reply.hijack();
3343
+ const raw2 = reply.raw;
3344
+ openStream(raw2);
3345
+ const keepAlive2 = setInterval(() => {
3346
+ if (!raw2.writableEnded) raw2.write(": ping\n\n");
3347
+ }, 15e3);
3348
+ raw2.on("close", () => clearInterval(keepAlive2));
3349
+ const ctx = {
3350
+ params: req.params,
3351
+ query: req.query,
3352
+ body: null,
3353
+ headers: req.headers
3354
+ };
3355
+ try {
3356
+ await fn(ctx, raw2, handlerCtx);
3357
+ } catch (err) {
3358
+ const msg = err instanceof Error ? err.message : String(err);
3359
+ clearInterval(keepAlive2);
3360
+ if (!raw2.writableEnded) {
3361
+ raw2.write(
3362
+ `event: error
3363
+ data: ${JSON.stringify({ error: `Handler "${route.handler}" threw an error: ${msg}` })}
3364
+
3365
+ `
3366
+ );
3367
+ raw2.end();
3368
+ }
3369
+ }
3370
+ return;
3371
+ }
3294
3372
  reply.hijack();
3295
3373
  const raw = reply.raw;
3296
- raw.setHeader("Content-Type", "text/event-stream");
3297
- raw.setHeader("Cache-Control", "no-cache");
3298
- raw.setHeader("Connection", "keep-alive");
3299
- raw.setHeader("Access-Control-Allow-Origin", "*");
3300
- raw.flushHeaders();
3374
+ openStream(raw);
3301
3375
  let alive = true;
3302
3376
  req.raw.on("close", () => {
3303
3377
  alive = false;
@@ -3348,6 +3422,13 @@ var SSERouteCommand = class {
3348
3422
  }
3349
3423
  }
3350
3424
  };
3425
+ function openStream(raw) {
3426
+ raw.setHeader("Content-Type", "text/event-stream");
3427
+ raw.setHeader("Cache-Control", "no-cache");
3428
+ raw.setHeader("Connection", "keep-alive");
3429
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3430
+ raw.flushHeaders();
3431
+ }
3351
3432
 
3352
3433
  // src/router/resource.router.ts
3353
3434
  function buildResourceRouteCommands(storage, options) {
@@ -3390,8 +3471,8 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3390
3471
  new AboutRouteCommand(storage, options, handlers),
3391
3472
  new OpenApiRouteCommand(storage, options),
3392
3473
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3393
- new SSERouteCommand(storage, options.base),
3394
- new CustomRouteCommand(storage, options.base, handlers),
3474
+ new SSERouteCommand(storage, options.base, handlers, options.idStrategy),
3475
+ new CustomRouteCommand(storage, options.base, handlers, options.idStrategy),
3395
3476
  ...buildResourceRouteCommands(storage, options)
3396
3477
  ];
3397
3478
  for (const command of commands) {
@@ -3424,6 +3505,9 @@ function createYrestServerFromStorage(storage, options, handlers = /* @__PURE__
3424
3505
  },
3425
3506
  get url() {
3426
3507
  return `http://${options.host}:${_port}${options.base}`;
3508
+ },
3509
+ collection(name) {
3510
+ return createCollectionHandle(storage, name, options.idStrategy);
3427
3511
  }
3428
3512
  };
3429
3513
  }
@@ -3486,12 +3570,12 @@ function loadConfigFile(configPath) {
3486
3570
 
3487
3571
  // src/utils/handlers.ts
3488
3572
  var import_node_fs5 = require("fs");
3489
- var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
3573
+ var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts"];
3490
3574
  async function loadHandlers(filePath) {
3491
3575
  if (!(0, import_node_fs5.existsSync)(filePath)) return /* @__PURE__ */ new Map();
3492
3576
  if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
3493
3577
  console.error(
3494
- ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
3578
+ ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs, .cjs, .ts and .mts files are allowed\x1B[0m`
3495
3579
  );
3496
3580
  return /* @__PURE__ */ new Map();
3497
3581
  }
@@ -3531,7 +3615,7 @@ function registerServe(program2) {
3531
3615
  "Save a snapshot of the initial database state and expose /_snapshot endpoints"
3532
3616
  ).option(
3533
3617
  "--handlers <file>",
3534
- "Path to a JavaScript file exporting custom route handler functions"
3618
+ "Path to a JS or TS file (.js, .mjs, .cjs, .ts, .mts) exporting handler functions"
3535
3619
  ).option(
3536
3620
  "--id-strategy <strategy>",
3537
3621
  "Id generation strategy for new items: increment (default) or uuid",
@@ -3644,26 +3728,59 @@ function registerServe(program2) {
3644
3728
  var import_node_fs7 = require("fs");
3645
3729
  var import_node_path5 = require("path");
3646
3730
  var import_yaml4 = require("yaml");
3647
- var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
3648
- // Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
3649
- // See https://github.com/aggiovato/yaml-rest for full documentation
3731
+ var JS_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
3732
+ // Handler signature: (req: HandlerRequest, ctx: HandlerContext) => HandlerResponse | Promise<HandlerResponse>
3733
+ // See https://github.com/aggiovato/yRest for full documentation
3734
+ `;
3735
+ var TS_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
3736
+ // TypeScript handlers need Node >= 22.18 (built-in type stripping) or running yrest through tsx.
3737
+ // See https://github.com/aggiovato/yRest for full documentation
3738
+ import { defineHandler } from "@yrest/cli";
3650
3739
  `;
3651
- function buildStub(name, method, path) {
3740
+ function isTypeScriptFile(filePath) {
3741
+ return filePath.endsWith(".ts") || filePath.endsWith(".mts");
3742
+ }
3743
+ function handlersFileHeader(typescript) {
3744
+ return typescript ? TS_HEADER : JS_HEADER;
3745
+ }
3746
+ function hasHandler(content, name) {
3747
+ return content.includes(`function ${name}(`) || new RegExp(`\\bconst ${name}\\s*=`).test(content);
3748
+ }
3749
+ function buildStub(name, method, path, typescript = false) {
3652
3750
  const tag = method && path ? `Handler for ${method.toUpperCase()} ${path}` : `yrest handler function`;
3751
+ if (typescript) {
3752
+ return [
3753
+ "",
3754
+ `/** ${tag} */`,
3755
+ `export const ${name} = defineHandler(async (req, ctx) => {`,
3756
+ ` // TODO: implement handler logic \u2014 ctx.collection("name") gives typed access to the data`,
3757
+ ` return { status: 200, body: null };`,
3758
+ `});`,
3759
+ ""
3760
+ ].join("\n");
3761
+ }
3653
3762
  return [
3654
3763
  "",
3655
3764
  `/**`,
3656
3765
  ` * ${tag}`,
3657
3766
  ` * @param {import('@yrest/cli').HandlerRequest} req`,
3767
+ ` * @param {import('@yrest/cli').HandlerContext} ctx`,
3658
3768
  ` * @returns {Promise<import('@yrest/cli').HandlerResponse>}`,
3659
3769
  ` */`,
3660
- `export async function ${name}(req) {`,
3661
- ` // TODO: implement handler logic`,
3770
+ `export async function ${name}(req, ctx) {`,
3771
+ ` // TODO: implement handler logic \u2014 ctx.collection("name") gives access to the data`,
3662
3772
  ` return { status: 200, body: null };`,
3663
3773
  `}`,
3664
3774
  ""
3665
3775
  ].join("\n");
3666
3776
  }
3777
+ function addRouteEntry(raw, name, method, path) {
3778
+ if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
3779
+ const routes = raw["_routes"];
3780
+ if (routes.some((r) => r["_handler"] === name || r["handler"] === name)) return false;
3781
+ routes.push({ _method: method.toUpperCase(), _path: path, _handler: name });
3782
+ return true;
3783
+ }
3667
3784
  function registerHandler(program2) {
3668
3785
  program2.command("handler").description("Scaffold a handler function stub in the handlers file").argument("<name>", "Name of the handler function to create").option("-m, --method <method>", "HTTP method hint for the stub JSDoc (e.g. POST)").option("-p, --path <path>", "Route path hint for the stub JSDoc (e.g. /login)").option(
3669
3786
  "--register",
@@ -3674,20 +3791,18 @@ function registerHandler(program2) {
3674
3791
  fileConfig.handlers ?? "yrest.handlers.js"
3675
3792
  );
3676
3793
  const dbPath = (0, import_node_path5.resolve)(fileConfig.file ?? "db.yml");
3794
+ const typescript = isTypeScriptFile(handlersPath);
3795
+ const stub = buildStub(name, flags.method, flags.path, typescript);
3677
3796
  if (!(0, import_node_fs7.existsSync)(handlersPath)) {
3678
- (0, import_node_fs7.writeFileSync)(
3679
- handlersPath,
3680
- HANDLERS_FILE_HEADER + buildStub(name, flags.method, flags.path),
3681
- "utf8"
3682
- );
3797
+ (0, import_node_fs7.writeFileSync)(handlersPath, handlersFileHeader(typescript) + stub, "utf8");
3683
3798
  console.log(` Created ${(0, import_node_path5.basename)(handlersPath)}`);
3684
3799
  } else {
3685
3800
  const existing = (0, import_node_fs7.readFileSync)(handlersPath, "utf8");
3686
- if (existing.includes(`function ${name}(`)) {
3801
+ if (hasHandler(existing, name)) {
3687
3802
  console.error(` Error: handler "${name}" already exists in ${(0, import_node_path5.basename)(handlersPath)}`);
3688
3803
  process.exit(1);
3689
3804
  }
3690
- (0, import_node_fs7.appendFileSync)(handlersPath, buildStub(name, flags.method, flags.path), "utf8");
3805
+ (0, import_node_fs7.appendFileSync)(handlersPath, stub, "utf8");
3691
3806
  console.log(` Added handler "${name}" to ${(0, import_node_path5.basename)(handlersPath)}`);
3692
3807
  }
3693
3808
  if (flags.register) {
@@ -3700,11 +3815,7 @@ function registerHandler(program2) {
3700
3815
  process.exit(1);
3701
3816
  }
3702
3817
  const raw = (0, import_yaml4.parse)((0, import_node_fs7.readFileSync)(dbPath, "utf8")) ?? {};
3703
- if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
3704
- const routes = raw["_routes"];
3705
- const alreadyRegistered = routes.some((r) => r["handler"] === name);
3706
- if (!alreadyRegistered) {
3707
- routes.push({ method: flags.method.toUpperCase(), path: flags.path, handler: name });
3818
+ if (addRouteEntry(raw, name, flags.method, flags.path)) {
3708
3819
  (0, import_node_fs7.writeFileSync)(dbPath, (0, import_yaml4.stringify)(raw), "utf8");
3709
3820
  console.log(` Added _routes entry to ${(0, import_node_path5.basename)(dbPath)}`);
3710
3821
  } else {
@@ -3755,6 +3866,283 @@ function registerOpenApi(program2) {
3755
3866
  });
3756
3867
  }
3757
3868
 
3869
+ // src/cli/commands/types.ts
3870
+ var import_node_fs9 = require("fs");
3871
+ var import_node_path7 = require("path");
3872
+ var import_yaml6 = require("yaml");
3873
+
3874
+ // src/codegen/generateTypes.ts
3875
+ var HEADER = "// Auto-generated by yrest types \u2014 do not edit manually.";
3876
+ var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3877
+ var MAX_DEPTH = 4;
3878
+ function generateTypes(input, options = {}) {
3879
+ const { data, schema = {}, relations = {} } = input;
3880
+ const collections = unique([...Object.keys(data), ...Object.keys(schema)]);
3881
+ const names = resolveInterfaceNames(collections);
3882
+ const out = [HEADER];
3883
+ if (options.source) out.push(`// Source: ${options.source}`);
3884
+ out.push("");
3885
+ for (const collection of collections) {
3886
+ out.push(
3887
+ renderInterface(
3888
+ names.get(collection),
3889
+ collection,
3890
+ data[collection] ?? [],
3891
+ schema[collection] ?? {},
3892
+ relations[collection] ?? {}
3893
+ )
3894
+ );
3895
+ out.push("");
3896
+ }
3897
+ out.push(`/** Union of all collection names in the source file. */`);
3898
+ out.push(
3899
+ collections.length > 0 ? `export type CollectionName = ${collections.map((c) => quote(c)).join(" | ")};` : "export type CollectionName = never;"
3900
+ );
3901
+ out.push("");
3902
+ out.push(`/** Maps each collection name to its item array type. */`);
3903
+ out.push("export interface Collections {");
3904
+ for (const collection of collections) {
3905
+ out.push(` ${propKey(collection)}: ${names.get(collection)}[];`);
3906
+ }
3907
+ out.push("}");
3908
+ return out.join("\n") + "\n";
3909
+ }
3910
+ function renderInterface(name, collection, items, fieldDefs, rels) {
3911
+ const records = items.filter(isPlainObject);
3912
+ const fields = collectFields(records);
3913
+ const fieldNames = unique([...fields.keys(), ...Object.keys(fieldDefs)]);
3914
+ const lines = [`/** Item of the \`${collection}\` collection. */`];
3915
+ lines.push(`export interface ${name} {`);
3916
+ for (const field of fieldNames) {
3917
+ const info = fields.get(field);
3918
+ const def = fieldDefs[field];
3919
+ const inferredRequired = info !== void 0 && info.count === records.length && records.length > 0;
3920
+ const required = def?.required ?? inferredRequired;
3921
+ let type = def ? typeFromDef(def, info) : typeOfValues(info.values, 0);
3922
+ if (def?.nullable === true && !type.split(" | ").includes("null")) type = `${type} | null`;
3923
+ for (const doc of fieldDoc(field, def, rels)) lines.push(` ${doc}`);
3924
+ lines.push(` ${propKey(field)}${required ? "" : "?"}: ${type};`);
3925
+ }
3926
+ if (fieldNames.length === 0) lines.push(" [key: string]: unknown;");
3927
+ lines.push("}");
3928
+ return lines.join("\n");
3929
+ }
3930
+ function fieldDoc(field, def, rels) {
3931
+ const text = [];
3932
+ const tags = [];
3933
+ if (def?.description) text.push(def.description.replace(/\*\//g, "*\\/"));
3934
+ const rel = Object.entries(rels).find(
3935
+ ([key, r]) => r.type !== "many2many" && (r.foreignKey ?? key) === field
3936
+ )?.[1];
3937
+ if (rel) text.push(`FK \u2192 ${rel.target}.id`);
3938
+ if (def?.format) tags.push(`@format ${def.format}`);
3939
+ if (def?.deprecated) tags.push("@deprecated");
3940
+ if (text.length === 0 && tags.length === 0) return [];
3941
+ if (tags.length === 0) return [`/** ${text.join(" \u2014 ")} */`];
3942
+ return ["/**", ...[...text, ...tags].map((l) => ` * ${l}`), " */"];
3943
+ }
3944
+ function collectFields(records) {
3945
+ const fields = /* @__PURE__ */ new Map();
3946
+ for (const record of records) {
3947
+ for (const [key, value] of Object.entries(record)) {
3948
+ let info = fields.get(key);
3949
+ if (!info) {
3950
+ info = { values: [], count: 0 };
3951
+ fields.set(key, info);
3952
+ }
3953
+ info.count++;
3954
+ info.values.push(value);
3955
+ }
3956
+ }
3957
+ return fields;
3958
+ }
3959
+ function typeOfValues(values, depth) {
3960
+ return unionOf(typeSetOf(values, depth));
3961
+ }
3962
+ function typeSetOf(values, depth) {
3963
+ const types = /* @__PURE__ */ new Set();
3964
+ const objects = [];
3965
+ const elements = [];
3966
+ let sawArray = false;
3967
+ for (const value of values) {
3968
+ if (value === null || value === void 0) types.add("null");
3969
+ else if (typeof value === "string") types.add("string");
3970
+ else if (typeof value === "number") types.add("number");
3971
+ else if (typeof value === "boolean") types.add("boolean");
3972
+ else if (Array.isArray(value)) {
3973
+ sawArray = true;
3974
+ elements.push(...value);
3975
+ } else if (isPlainObject(value)) objects.push(value);
3976
+ else types.add("unknown");
3977
+ }
3978
+ if (objects.length > 0) {
3979
+ types.add(depth >= MAX_DEPTH ? "Record<string, unknown>" : inlineObject(objects, depth + 1));
3980
+ }
3981
+ if (sawArray) {
3982
+ types.add(
3983
+ elements.length === 0 ? "unknown[]" : depth >= MAX_DEPTH ? "unknown[]" : arrayOf(typeSetOf(elements, depth + 1))
3984
+ );
3985
+ }
3986
+ return types;
3987
+ }
3988
+ function inlineObject(records, depth) {
3989
+ const fields = collectFields(records);
3990
+ if (fields.size === 0) return "Record<string, unknown>";
3991
+ const props = [...fields].map(([key, info]) => {
3992
+ const optional = info.count < records.length ? "?" : "";
3993
+ return `${propKey(key)}${optional}: ${typeOfValues(info.values, depth)}`;
3994
+ });
3995
+ return `{ ${props.join("; ")} }`;
3996
+ }
3997
+ function unionOf(types) {
3998
+ const list = [...types];
3999
+ if (list.length === 0) return "unknown";
4000
+ if (list.length === 1 && list[0] === "null") return "unknown";
4001
+ const hasConcreteArray = list.some((t) => t.endsWith("[]") && t !== "unknown[]");
4002
+ const filtered = hasConcreteArray ? list.filter((t) => t !== "unknown[]") : list;
4003
+ filtered.sort((a, b) => a === "null" ? 1 : b === "null" ? -1 : 0);
4004
+ return filtered.join(" | ");
4005
+ }
4006
+ function arrayOf(elements) {
4007
+ const element = unionOf(elements);
4008
+ return element.includes(" | ") && elements.size > 1 ? `(${element})[]` : `${element}[]`;
4009
+ }
4010
+ function typeFromDef(def, info) {
4011
+ if (def.enum && def.enum.length > 0) {
4012
+ return unique(def.enum.map((v) => literal(v))).join(" | ");
4013
+ }
4014
+ if (def.type) {
4015
+ if (def.type === "array") {
4016
+ const items = def.items;
4017
+ if (items?.enum && items.enum.length > 0)
4018
+ return arrayOf(new Set(items.enum.map((v) => literal(v))));
4019
+ if (items?.type) return arrayOf(/* @__PURE__ */ new Set([primitiveFor(items.type)]));
4020
+ const inferred = info ? typeOfValues(info.values, 0) : "";
4021
+ return inferred.endsWith("[]") && !inferred.includes(" | ") ? inferred : "unknown[]";
4022
+ }
4023
+ return primitiveFor(def.type);
4024
+ }
4025
+ return info ? typeOfValues(info.values, 0) : "unknown";
4026
+ }
4027
+ function primitiveFor(type) {
4028
+ switch (type) {
4029
+ case "integer":
4030
+ case "number":
4031
+ return "number";
4032
+ case "string":
4033
+ return "string";
4034
+ case "boolean":
4035
+ return "boolean";
4036
+ case "array":
4037
+ return "unknown[]";
4038
+ case "object":
4039
+ return "Record<string, unknown>";
4040
+ }
4041
+ }
4042
+ function literal(value) {
4043
+ if (typeof value === "string") return JSON.stringify(value);
4044
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4045
+ if (value === null) return "null";
4046
+ return "unknown";
4047
+ }
4048
+ function resolveInterfaceNames(collections) {
4049
+ const names = /* @__PURE__ */ new Map();
4050
+ const used = /* @__PURE__ */ new Set();
4051
+ for (const collection of collections) {
4052
+ const words = splitWords(collection);
4053
+ const candidates = [
4054
+ toPascal([...words.slice(0, -1), singularize(words.at(-1) ?? "")]),
4055
+ toPascal(words)
4056
+ ];
4057
+ let name = candidates.find((c) => c && !used.has(c));
4058
+ if (!name) {
4059
+ const base = candidates[1] || "Collection";
4060
+ let i = 2;
4061
+ while (used.has(`${base}${i}`)) i++;
4062
+ name = `${base}${i}`;
4063
+ }
4064
+ if (/^[0-9]/.test(name)) name = `_${name}`;
4065
+ used.add(name);
4066
+ names.set(collection, name);
4067
+ }
4068
+ return names;
4069
+ }
4070
+ function splitWords(name) {
4071
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
4072
+ }
4073
+ function toPascal(words) {
4074
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
4075
+ }
4076
+ function singularize(word) {
4077
+ const lower = word.toLowerCase();
4078
+ if (lower.length <= 2) return word;
4079
+ if (/(ss|us|is)$/.test(lower)) return word;
4080
+ if (/[^aeiou]ies$/.test(lower)) return word.slice(0, -3) + "y";
4081
+ if (/(s|x|z|ch|sh)es$/.test(lower)) return word.slice(0, -2);
4082
+ if (lower.endsWith("s")) return word.slice(0, -1);
4083
+ return word;
4084
+ }
4085
+ function propKey(key) {
4086
+ return IDENT_RE.test(key) ? key : JSON.stringify(key);
4087
+ }
4088
+ function quote(value) {
4089
+ return JSON.stringify(value);
4090
+ }
4091
+ function unique(list) {
4092
+ return [...new Set(list)];
4093
+ }
4094
+ function isPlainObject(value) {
4095
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4096
+ }
4097
+
4098
+ // src/cli/commands/types.ts
4099
+ function generateTypesFromFile(filePath) {
4100
+ const absPath = (0, import_node_path7.resolve)(filePath);
4101
+ const raw = (0, import_yaml6.parse)((0, import_node_fs9.readFileSync)(absPath, "utf8")) ?? {};
4102
+ return generateTypes(
4103
+ {
4104
+ data: parseData(raw),
4105
+ schema: parseSchema(raw["_schema"]),
4106
+ relations: parseRelations(raw["_rel"])
4107
+ },
4108
+ { source: (0, import_node_path7.basename)(absPath) }
4109
+ );
4110
+ }
4111
+ function registerTypes(program2) {
4112
+ program2.command("types <file>").description("Generate TypeScript interfaces from a db.yml file").option("-o, --output <file>", "Write to a file instead of stdout (e.g. db.types.ts)").option("-w, --watch", "Regenerate on every change of <file> (requires --output)").action((file, opts) => {
4113
+ if (opts.watch && !opts.output) {
4114
+ console.error("\u2717 --watch requires --output");
4115
+ process.exitCode = 1;
4116
+ return;
4117
+ }
4118
+ const run = () => {
4119
+ try {
4120
+ const output = generateTypesFromFile(file);
4121
+ if (!opts.output) {
4122
+ process.stdout.write(output);
4123
+ } else {
4124
+ const outFile = (0, import_node_path7.resolve)(opts.output);
4125
+ (0, import_node_fs9.writeFileSync)(outFile, output, "utf8");
4126
+ console.log(`\u2713 Types written to ${outFile}`);
4127
+ }
4128
+ return true;
4129
+ } catch (err) {
4130
+ console.error(`\u2717 Could not generate types: ${err.message}`);
4131
+ return false;
4132
+ }
4133
+ };
4134
+ const ok2 = run();
4135
+ if (!opts.watch) {
4136
+ if (!ok2) process.exitCode = 1;
4137
+ return;
4138
+ }
4139
+ console.log(`\u{1F440} Watching ${(0, import_node_path7.resolve)(file)} \u2014 Ctrl+C to stop`);
4140
+ (0, import_node_fs9.watchFile)((0, import_node_path7.resolve)(file), { interval: 300 }, (curr, prev) => {
4141
+ if (curr.mtimeMs !== prev.mtimeMs) run();
4142
+ });
4143
+ });
4144
+ }
4145
+
3758
4146
  // src/cli/index.ts
3759
4147
  var require2 = (0, import_module.createRequire)(importMetaUrl);
3760
4148
  var { version } = require2("../../package.json");
@@ -3763,4 +4151,5 @@ registerInit(import_commander.program);
3763
4151
  registerServe(import_commander.program);
3764
4152
  registerHandler(import_commander.program);
3765
4153
  registerOpenApi(import_commander.program);
4154
+ registerTypes(import_commander.program);
3766
4155
  import_commander.program.parse();