@yrest/cli 0.13.1 → 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.
@@ -698,11 +698,12 @@ function normaliseSseRoute(raw) {
698
698
  return null;
699
699
  }
700
700
  const s = sse;
701
+ const handler = typeof s["_handler"] === "string" && s["_handler"].trim() ? s["_handler"].trim() : void 0;
701
702
  const rawEvents = s["_events"];
702
- if (!Array.isArray(rawEvents)) return null;
703
- const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
704
- if (!events.length) return null;
703
+ const events = Array.isArray(rawEvents) ? rawEvents.map(normaliseEvent).filter((e) => e !== null) : [];
704
+ if (!handler && !events.length) return null;
705
705
  return {
706
+ ...handler ? { handler } : {},
706
707
  path,
707
708
  interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
708
709
  loop: s["_loop"] !== false,
@@ -1243,6 +1244,10 @@ function handlersAccordion(handlers, routes, base) {
1243
1244
  function sseRoutesAccordion(routes, base) {
1244
1245
  if (!routes.length) return "";
1245
1246
  const rows = routes.map((r) => {
1247
+ if (r.handler) {
1248
+ const desc2 = `SSE stream&ensp;<span style="color:#0ea5e9;font-size:11px">\u2192 ${escapeHtml(r.handler)}()</span>`;
1249
+ return endpointRow("SSE", `${base}${r.path}`, desc2);
1250
+ }
1246
1251
  const tags = [
1247
1252
  `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
1248
1253
  `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
@@ -2541,6 +2546,32 @@ var CollectionRouteCommand = class {
2541
2546
  }
2542
2547
  };
2543
2548
 
2549
+ // src/api/collection.ts
2550
+ function createCollectionHandle(storage, name, idStrategy = "increment") {
2551
+ if (!storage.getCollection(name)) {
2552
+ const known = Object.keys(storage.getData()).join(", ") || "none";
2553
+ throw new Error(`Collection "${name}" does not exist (known collections: ${known})`);
2554
+ }
2555
+ const items = () => storage.getCollection(name) ?? [];
2556
+ const copy = (item) => item === void 0 ? void 0 : structuredClone(item);
2557
+ return {
2558
+ findAll: () => items().map((i) => copy(i)),
2559
+ findById: (id) => copy(findById(items(), String(id))),
2560
+ findWhere: (predicate) => items().map((i) => copy(i)).filter(predicate),
2561
+ count: () => items().length,
2562
+ create: (data) => copy(createItem(storage, name, { ...data }, idStrategy)),
2563
+ update: (id, patch) => {
2564
+ const rest = { ...patch };
2565
+ delete rest["id"];
2566
+ return copy(patchItem(storage, name, String(id), rest));
2567
+ },
2568
+ delete: (id) => copy(deleteItem(storage, name, String(id)))
2569
+ };
2570
+ }
2571
+ function collectionAccessor(storage, idStrategy = "increment") {
2572
+ return (name) => createCollectionHandle(storage, name, idStrategy);
2573
+ }
2574
+
2544
2575
  // src/utils/conditions.ts
2545
2576
  function resolveRequestPath(dotPath, req) {
2546
2577
  const [root, ...rest] = dotPath.split(".");
@@ -2603,15 +2634,20 @@ function resolveBody(body, ctx) {
2603
2634
  return body ?? null;
2604
2635
  }
2605
2636
  var CustomRouteCommand = class {
2606
- constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
2637
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map(), idStrategy = "increment") {
2607
2638
  this.storage = storage;
2608
2639
  this.base = base;
2609
2640
  this.handlers = handlers;
2641
+ this.idStrategy = idStrategy;
2610
2642
  }
2611
2643
  storage;
2612
2644
  base;
2613
2645
  handlers;
2646
+ idStrategy;
2614
2647
  register(server) {
2648
+ const handlerCtx = {
2649
+ collection: collectionAccessor(this.storage, this.idStrategy)
2650
+ };
2615
2651
  for (const route of this.storage.getRoutes()) {
2616
2652
  const method = route.method?.toUpperCase();
2617
2653
  const path = route.path;
@@ -2627,7 +2663,7 @@ var CustomRouteCommand = class {
2627
2663
  url,
2628
2664
  handler: async (req, reply) => {
2629
2665
  if (route.delay && route.delay > 0) {
2630
- await new Promise((resolve6) => setTimeout(resolve6, route.delay));
2666
+ await new Promise((resolve7) => setTimeout(resolve7, route.delay));
2631
2667
  }
2632
2668
  if (route.error) {
2633
2669
  const body2 = route.errorBody ?? { error: `Forced error ${route.error}` };
@@ -2648,7 +2684,7 @@ var CustomRouteCommand = class {
2648
2684
  body: req.body,
2649
2685
  headers: req.headers
2650
2686
  };
2651
- const result = await fn(ctx2);
2687
+ const result = await fn(ctx2, handlerCtx);
2652
2688
  const resStatus = result.status ?? 200;
2653
2689
  for (const [k, v] of Object.entries(result.headers ?? {})) {
2654
2690
  reply.header(k, v);
@@ -3251,26 +3287,64 @@ var SnapshotRouteCommand = class {
3251
3287
 
3252
3288
  // src/router/routes/sse.routes.ts
3253
3289
  var SSERouteCommand = class {
3254
- constructor(storage, base) {
3290
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map(), idStrategy = "increment") {
3255
3291
  this.storage = storage;
3256
3292
  this.base = base;
3293
+ this.handlers = handlers;
3294
+ this.idStrategy = idStrategy;
3257
3295
  }
3258
3296
  storage;
3259
3297
  base;
3298
+ handlers;
3299
+ idStrategy;
3260
3300
  register(server) {
3301
+ const handlerCtx = {
3302
+ collection: collectionAccessor(this.storage, this.idStrategy)
3303
+ };
3261
3304
  for (const route of this.storage.getSseRoutes()) {
3262
3305
  const url = `${this.base}${route.path}`;
3263
3306
  server.route({
3264
3307
  method: "GET",
3265
3308
  url,
3266
3309
  handler: async (req, reply) => {
3310
+ if (route.handler) {
3311
+ const fn = this.handlers.get(route.handler);
3312
+ if (!fn) {
3313
+ return reply.status(501).send({ error: `Handler "${route.handler}" is not defined in the handlers file` });
3314
+ }
3315
+ reply.hijack();
3316
+ const raw2 = reply.raw;
3317
+ openStream(raw2);
3318
+ const keepAlive2 = setInterval(() => {
3319
+ if (!raw2.writableEnded) raw2.write(": ping\n\n");
3320
+ }, 15e3);
3321
+ raw2.on("close", () => clearInterval(keepAlive2));
3322
+ const ctx = {
3323
+ params: req.params,
3324
+ query: req.query,
3325
+ body: null,
3326
+ headers: req.headers
3327
+ };
3328
+ try {
3329
+ await fn(ctx, raw2, handlerCtx);
3330
+ } catch (err) {
3331
+ const msg = err instanceof Error ? err.message : String(err);
3332
+ clearInterval(keepAlive2);
3333
+ if (!raw2.writableEnded) {
3334
+ raw2.write(
3335
+ `event: error
3336
+ data: ${JSON.stringify({ error: `Handler "${route.handler}" threw an error: ${msg}` })}
3337
+
3338
+ `
3339
+ );
3340
+ raw2.end();
3341
+ }
3342
+ }
3343
+ return;
3344
+ }
3267
3345
  reply.hijack();
3268
3346
  const raw = reply.raw;
3269
- raw.setHeader("Content-Type", "text/event-stream");
3270
- raw.setHeader("Cache-Control", "no-cache");
3271
- raw.setHeader("Connection", "keep-alive");
3272
- raw.setHeader("Access-Control-Allow-Origin", "*");
3273
- raw.flushHeaders();
3347
+ openStream(raw);
3274
3348
  let alive = true;
3275
3349
  req.raw.on("close", () => {
3276
3350
  alive = false;
@@ -3321,6 +3395,13 @@ var SSERouteCommand = class {
3321
3395
  }
3322
3396
  }
3323
3397
  };
3398
+ function openStream(raw) {
3399
+ raw.setHeader("Content-Type", "text/event-stream");
3400
+ raw.setHeader("Cache-Control", "no-cache");
3401
+ raw.setHeader("Connection", "keep-alive");
3402
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3403
+ raw.flushHeaders();
3404
+ }
3324
3405
 
3325
3406
  // src/router/resource.router.ts
3326
3407
  function buildResourceRouteCommands(storage, options) {
@@ -3363,8 +3444,8 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3363
3444
  new AboutRouteCommand(storage, options, handlers),
3364
3445
  new OpenApiRouteCommand(storage, options),
3365
3446
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3366
- new SSERouteCommand(storage, options.base),
3367
- new CustomRouteCommand(storage, options.base, handlers),
3447
+ new SSERouteCommand(storage, options.base, handlers, options.idStrategy),
3448
+ new CustomRouteCommand(storage, options.base, handlers, options.idStrategy),
3368
3449
  ...buildResourceRouteCommands(storage, options)
3369
3450
  ];
3370
3451
  for (const command of commands) {
@@ -3397,6 +3478,9 @@ function createYrestServerFromStorage(storage, options, handlers = /* @__PURE__
3397
3478
  },
3398
3479
  get url() {
3399
3480
  return `http://${options.host}:${_port}${options.base}`;
3481
+ },
3482
+ collection(name) {
3483
+ return createCollectionHandle(storage, name, options.idStrategy);
3400
3484
  }
3401
3485
  };
3402
3486
  }
@@ -3459,12 +3543,12 @@ function loadConfigFile(configPath) {
3459
3543
 
3460
3544
  // src/utils/handlers.ts
3461
3545
  import { existsSync as existsSync3 } from "fs";
3462
- var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
3546
+ var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts"];
3463
3547
  async function loadHandlers(filePath) {
3464
3548
  if (!existsSync3(filePath)) return /* @__PURE__ */ new Map();
3465
3549
  if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
3466
3550
  console.error(
3467
- ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
3551
+ ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs, .cjs, .ts and .mts files are allowed\x1B[0m`
3468
3552
  );
3469
3553
  return /* @__PURE__ */ new Map();
3470
3554
  }
@@ -3504,7 +3588,7 @@ function registerServe(program2) {
3504
3588
  "Save a snapshot of the initial database state and expose /_snapshot endpoints"
3505
3589
  ).option(
3506
3590
  "--handlers <file>",
3507
- "Path to a JavaScript file exporting custom route handler functions"
3591
+ "Path to a JS or TS file (.js, .mjs, .cjs, .ts, .mts) exporting handler functions"
3508
3592
  ).option(
3509
3593
  "--id-strategy <strategy>",
3510
3594
  "Id generation strategy for new items: increment (default) or uuid",
@@ -3617,26 +3701,59 @@ function registerServe(program2) {
3617
3701
  import { existsSync as existsSync4, readFileSync as readFileSync4, appendFileSync, writeFileSync as writeFileSync3 } from "fs";
3618
3702
  import { join as join3, resolve as resolve4, basename } from "path";
3619
3703
  import { parse as parse3, stringify as stringify3 } from "yaml";
3620
- var HANDLERS_FILE_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
3621
- // Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
3622
- // See https://github.com/aggiovato/yaml-rest for full documentation
3704
+ var JS_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
3705
+ // Handler signature: (req: HandlerRequest, ctx: HandlerContext) => HandlerResponse | Promise<HandlerResponse>
3706
+ // See https://github.com/aggiovato/yRest for full documentation
3707
+ `;
3708
+ var TS_HEADER = `// yrest handlers \u2014 loaded via "handlers:" in yrest.config.yml
3709
+ // TypeScript handlers need Node >= 22.18 (built-in type stripping) or running yrest through tsx.
3710
+ // See https://github.com/aggiovato/yRest for full documentation
3711
+ import { defineHandler } from "@yrest/cli";
3623
3712
  `;
3624
- function buildStub(name, method, path) {
3713
+ function isTypeScriptFile(filePath) {
3714
+ return filePath.endsWith(".ts") || filePath.endsWith(".mts");
3715
+ }
3716
+ function handlersFileHeader(typescript) {
3717
+ return typescript ? TS_HEADER : JS_HEADER;
3718
+ }
3719
+ function hasHandler(content, name) {
3720
+ return content.includes(`function ${name}(`) || new RegExp(`\\bconst ${name}\\s*=`).test(content);
3721
+ }
3722
+ function buildStub(name, method, path, typescript = false) {
3625
3723
  const tag = method && path ? `Handler for ${method.toUpperCase()} ${path}` : `yrest handler function`;
3724
+ if (typescript) {
3725
+ return [
3726
+ "",
3727
+ `/** ${tag} */`,
3728
+ `export const ${name} = defineHandler(async (req, ctx) => {`,
3729
+ ` // TODO: implement handler logic \u2014 ctx.collection("name") gives typed access to the data`,
3730
+ ` return { status: 200, body: null };`,
3731
+ `});`,
3732
+ ""
3733
+ ].join("\n");
3734
+ }
3626
3735
  return [
3627
3736
  "",
3628
3737
  `/**`,
3629
3738
  ` * ${tag}`,
3630
3739
  ` * @param {import('@yrest/cli').HandlerRequest} req`,
3740
+ ` * @param {import('@yrest/cli').HandlerContext} ctx`,
3631
3741
  ` * @returns {Promise<import('@yrest/cli').HandlerResponse>}`,
3632
3742
  ` */`,
3633
- `export async function ${name}(req) {`,
3634
- ` // TODO: implement handler logic`,
3743
+ `export async function ${name}(req, ctx) {`,
3744
+ ` // TODO: implement handler logic \u2014 ctx.collection("name") gives access to the data`,
3635
3745
  ` return { status: 200, body: null };`,
3636
3746
  `}`,
3637
3747
  ""
3638
3748
  ].join("\n");
3639
3749
  }
3750
+ function addRouteEntry(raw, name, method, path) {
3751
+ if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
3752
+ const routes = raw["_routes"];
3753
+ if (routes.some((r) => r["_handler"] === name || r["handler"] === name)) return false;
3754
+ routes.push({ _method: method.toUpperCase(), _path: path, _handler: name });
3755
+ return true;
3756
+ }
3640
3757
  function registerHandler(program2) {
3641
3758
  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(
3642
3759
  "--register",
@@ -3647,20 +3764,18 @@ function registerHandler(program2) {
3647
3764
  fileConfig.handlers ?? "yrest.handlers.js"
3648
3765
  );
3649
3766
  const dbPath = resolve4(fileConfig.file ?? "db.yml");
3767
+ const typescript = isTypeScriptFile(handlersPath);
3768
+ const stub = buildStub(name, flags.method, flags.path, typescript);
3650
3769
  if (!existsSync4(handlersPath)) {
3651
- writeFileSync3(
3652
- handlersPath,
3653
- HANDLERS_FILE_HEADER + buildStub(name, flags.method, flags.path),
3654
- "utf8"
3655
- );
3770
+ writeFileSync3(handlersPath, handlersFileHeader(typescript) + stub, "utf8");
3656
3771
  console.log(` Created ${basename(handlersPath)}`);
3657
3772
  } else {
3658
3773
  const existing = readFileSync4(handlersPath, "utf8");
3659
- if (existing.includes(`function ${name}(`)) {
3774
+ if (hasHandler(existing, name)) {
3660
3775
  console.error(` Error: handler "${name}" already exists in ${basename(handlersPath)}`);
3661
3776
  process.exit(1);
3662
3777
  }
3663
- appendFileSync(handlersPath, buildStub(name, flags.method, flags.path), "utf8");
3778
+ appendFileSync(handlersPath, stub, "utf8");
3664
3779
  console.log(` Added handler "${name}" to ${basename(handlersPath)}`);
3665
3780
  }
3666
3781
  if (flags.register) {
@@ -3673,11 +3788,7 @@ function registerHandler(program2) {
3673
3788
  process.exit(1);
3674
3789
  }
3675
3790
  const raw = parse3(readFileSync4(dbPath, "utf8")) ?? {};
3676
- if (!Array.isArray(raw["_routes"])) raw["_routes"] = [];
3677
- const routes = raw["_routes"];
3678
- const alreadyRegistered = routes.some((r) => r["handler"] === name);
3679
- if (!alreadyRegistered) {
3680
- routes.push({ method: flags.method.toUpperCase(), path: flags.path, handler: name });
3791
+ if (addRouteEntry(raw, name, flags.method, flags.path)) {
3681
3792
  writeFileSync3(dbPath, stringify3(raw), "utf8");
3682
3793
  console.log(` Added _routes entry to ${basename(dbPath)}`);
3683
3794
  } else {
@@ -3728,6 +3839,283 @@ function registerOpenApi(program2) {
3728
3839
  });
3729
3840
  }
3730
3841
 
3842
+ // src/cli/commands/types.ts
3843
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, watchFile as watchFile2 } from "fs";
3844
+ import { basename as basename2, resolve as resolve6 } from "path";
3845
+ import { parse as parse4 } from "yaml";
3846
+
3847
+ // src/codegen/generateTypes.ts
3848
+ var HEADER = "// Auto-generated by yrest types \u2014 do not edit manually.";
3849
+ var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3850
+ var MAX_DEPTH = 4;
3851
+ function generateTypes(input, options = {}) {
3852
+ const { data, schema = {}, relations = {} } = input;
3853
+ const collections = unique([...Object.keys(data), ...Object.keys(schema)]);
3854
+ const names = resolveInterfaceNames(collections);
3855
+ const out = [HEADER];
3856
+ if (options.source) out.push(`// Source: ${options.source}`);
3857
+ out.push("");
3858
+ for (const collection of collections) {
3859
+ out.push(
3860
+ renderInterface(
3861
+ names.get(collection),
3862
+ collection,
3863
+ data[collection] ?? [],
3864
+ schema[collection] ?? {},
3865
+ relations[collection] ?? {}
3866
+ )
3867
+ );
3868
+ out.push("");
3869
+ }
3870
+ out.push(`/** Union of all collection names in the source file. */`);
3871
+ out.push(
3872
+ collections.length > 0 ? `export type CollectionName = ${collections.map((c) => quote(c)).join(" | ")};` : "export type CollectionName = never;"
3873
+ );
3874
+ out.push("");
3875
+ out.push(`/** Maps each collection name to its item array type. */`);
3876
+ out.push("export interface Collections {");
3877
+ for (const collection of collections) {
3878
+ out.push(` ${propKey(collection)}: ${names.get(collection)}[];`);
3879
+ }
3880
+ out.push("}");
3881
+ return out.join("\n") + "\n";
3882
+ }
3883
+ function renderInterface(name, collection, items, fieldDefs, rels) {
3884
+ const records = items.filter(isPlainObject);
3885
+ const fields = collectFields(records);
3886
+ const fieldNames = unique([...fields.keys(), ...Object.keys(fieldDefs)]);
3887
+ const lines = [`/** Item of the \`${collection}\` collection. */`];
3888
+ lines.push(`export interface ${name} {`);
3889
+ for (const field of fieldNames) {
3890
+ const info = fields.get(field);
3891
+ const def = fieldDefs[field];
3892
+ const inferredRequired = info !== void 0 && info.count === records.length && records.length > 0;
3893
+ const required = def?.required ?? inferredRequired;
3894
+ let type = def ? typeFromDef(def, info) : typeOfValues(info.values, 0);
3895
+ if (def?.nullable === true && !type.split(" | ").includes("null")) type = `${type} | null`;
3896
+ for (const doc of fieldDoc(field, def, rels)) lines.push(` ${doc}`);
3897
+ lines.push(` ${propKey(field)}${required ? "" : "?"}: ${type};`);
3898
+ }
3899
+ if (fieldNames.length === 0) lines.push(" [key: string]: unknown;");
3900
+ lines.push("}");
3901
+ return lines.join("\n");
3902
+ }
3903
+ function fieldDoc(field, def, rels) {
3904
+ const text = [];
3905
+ const tags = [];
3906
+ if (def?.description) text.push(def.description.replace(/\*\//g, "*\\/"));
3907
+ const rel = Object.entries(rels).find(
3908
+ ([key, r]) => r.type !== "many2many" && (r.foreignKey ?? key) === field
3909
+ )?.[1];
3910
+ if (rel) text.push(`FK \u2192 ${rel.target}.id`);
3911
+ if (def?.format) tags.push(`@format ${def.format}`);
3912
+ if (def?.deprecated) tags.push("@deprecated");
3913
+ if (text.length === 0 && tags.length === 0) return [];
3914
+ if (tags.length === 0) return [`/** ${text.join(" \u2014 ")} */`];
3915
+ return ["/**", ...[...text, ...tags].map((l) => ` * ${l}`), " */"];
3916
+ }
3917
+ function collectFields(records) {
3918
+ const fields = /* @__PURE__ */ new Map();
3919
+ for (const record of records) {
3920
+ for (const [key, value] of Object.entries(record)) {
3921
+ let info = fields.get(key);
3922
+ if (!info) {
3923
+ info = { values: [], count: 0 };
3924
+ fields.set(key, info);
3925
+ }
3926
+ info.count++;
3927
+ info.values.push(value);
3928
+ }
3929
+ }
3930
+ return fields;
3931
+ }
3932
+ function typeOfValues(values, depth) {
3933
+ return unionOf(typeSetOf(values, depth));
3934
+ }
3935
+ function typeSetOf(values, depth) {
3936
+ const types = /* @__PURE__ */ new Set();
3937
+ const objects = [];
3938
+ const elements = [];
3939
+ let sawArray = false;
3940
+ for (const value of values) {
3941
+ if (value === null || value === void 0) types.add("null");
3942
+ else if (typeof value === "string") types.add("string");
3943
+ else if (typeof value === "number") types.add("number");
3944
+ else if (typeof value === "boolean") types.add("boolean");
3945
+ else if (Array.isArray(value)) {
3946
+ sawArray = true;
3947
+ elements.push(...value);
3948
+ } else if (isPlainObject(value)) objects.push(value);
3949
+ else types.add("unknown");
3950
+ }
3951
+ if (objects.length > 0) {
3952
+ types.add(depth >= MAX_DEPTH ? "Record<string, unknown>" : inlineObject(objects, depth + 1));
3953
+ }
3954
+ if (sawArray) {
3955
+ types.add(
3956
+ elements.length === 0 ? "unknown[]" : depth >= MAX_DEPTH ? "unknown[]" : arrayOf(typeSetOf(elements, depth + 1))
3957
+ );
3958
+ }
3959
+ return types;
3960
+ }
3961
+ function inlineObject(records, depth) {
3962
+ const fields = collectFields(records);
3963
+ if (fields.size === 0) return "Record<string, unknown>";
3964
+ const props = [...fields].map(([key, info]) => {
3965
+ const optional = info.count < records.length ? "?" : "";
3966
+ return `${propKey(key)}${optional}: ${typeOfValues(info.values, depth)}`;
3967
+ });
3968
+ return `{ ${props.join("; ")} }`;
3969
+ }
3970
+ function unionOf(types) {
3971
+ const list = [...types];
3972
+ if (list.length === 0) return "unknown";
3973
+ if (list.length === 1 && list[0] === "null") return "unknown";
3974
+ const hasConcreteArray = list.some((t) => t.endsWith("[]") && t !== "unknown[]");
3975
+ const filtered = hasConcreteArray ? list.filter((t) => t !== "unknown[]") : list;
3976
+ filtered.sort((a, b) => a === "null" ? 1 : b === "null" ? -1 : 0);
3977
+ return filtered.join(" | ");
3978
+ }
3979
+ function arrayOf(elements) {
3980
+ const element = unionOf(elements);
3981
+ return element.includes(" | ") && elements.size > 1 ? `(${element})[]` : `${element}[]`;
3982
+ }
3983
+ function typeFromDef(def, info) {
3984
+ if (def.enum && def.enum.length > 0) {
3985
+ return unique(def.enum.map((v) => literal(v))).join(" | ");
3986
+ }
3987
+ if (def.type) {
3988
+ if (def.type === "array") {
3989
+ const items = def.items;
3990
+ if (items?.enum && items.enum.length > 0)
3991
+ return arrayOf(new Set(items.enum.map((v) => literal(v))));
3992
+ if (items?.type) return arrayOf(/* @__PURE__ */ new Set([primitiveFor(items.type)]));
3993
+ const inferred = info ? typeOfValues(info.values, 0) : "";
3994
+ return inferred.endsWith("[]") && !inferred.includes(" | ") ? inferred : "unknown[]";
3995
+ }
3996
+ return primitiveFor(def.type);
3997
+ }
3998
+ return info ? typeOfValues(info.values, 0) : "unknown";
3999
+ }
4000
+ function primitiveFor(type) {
4001
+ switch (type) {
4002
+ case "integer":
4003
+ case "number":
4004
+ return "number";
4005
+ case "string":
4006
+ return "string";
4007
+ case "boolean":
4008
+ return "boolean";
4009
+ case "array":
4010
+ return "unknown[]";
4011
+ case "object":
4012
+ return "Record<string, unknown>";
4013
+ }
4014
+ }
4015
+ function literal(value) {
4016
+ if (typeof value === "string") return JSON.stringify(value);
4017
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4018
+ if (value === null) return "null";
4019
+ return "unknown";
4020
+ }
4021
+ function resolveInterfaceNames(collections) {
4022
+ const names = /* @__PURE__ */ new Map();
4023
+ const used = /* @__PURE__ */ new Set();
4024
+ for (const collection of collections) {
4025
+ const words = splitWords(collection);
4026
+ const candidates = [
4027
+ toPascal([...words.slice(0, -1), singularize(words.at(-1) ?? "")]),
4028
+ toPascal(words)
4029
+ ];
4030
+ let name = candidates.find((c) => c && !used.has(c));
4031
+ if (!name) {
4032
+ const base = candidates[1] || "Collection";
4033
+ let i = 2;
4034
+ while (used.has(`${base}${i}`)) i++;
4035
+ name = `${base}${i}`;
4036
+ }
4037
+ if (/^[0-9]/.test(name)) name = `_${name}`;
4038
+ used.add(name);
4039
+ names.set(collection, name);
4040
+ }
4041
+ return names;
4042
+ }
4043
+ function splitWords(name) {
4044
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
4045
+ }
4046
+ function toPascal(words) {
4047
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
4048
+ }
4049
+ function singularize(word) {
4050
+ const lower = word.toLowerCase();
4051
+ if (lower.length <= 2) return word;
4052
+ if (/(ss|us|is)$/.test(lower)) return word;
4053
+ if (/[^aeiou]ies$/.test(lower)) return word.slice(0, -3) + "y";
4054
+ if (/(s|x|z|ch|sh)es$/.test(lower)) return word.slice(0, -2);
4055
+ if (lower.endsWith("s")) return word.slice(0, -1);
4056
+ return word;
4057
+ }
4058
+ function propKey(key) {
4059
+ return IDENT_RE.test(key) ? key : JSON.stringify(key);
4060
+ }
4061
+ function quote(value) {
4062
+ return JSON.stringify(value);
4063
+ }
4064
+ function unique(list) {
4065
+ return [...new Set(list)];
4066
+ }
4067
+ function isPlainObject(value) {
4068
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4069
+ }
4070
+
4071
+ // src/cli/commands/types.ts
4072
+ function generateTypesFromFile(filePath) {
4073
+ const absPath = resolve6(filePath);
4074
+ const raw = parse4(readFileSync5(absPath, "utf8")) ?? {};
4075
+ return generateTypes(
4076
+ {
4077
+ data: parseData(raw),
4078
+ schema: parseSchema(raw["_schema"]),
4079
+ relations: parseRelations(raw["_rel"])
4080
+ },
4081
+ { source: basename2(absPath) }
4082
+ );
4083
+ }
4084
+ function registerTypes(program2) {
4085
+ 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) => {
4086
+ if (opts.watch && !opts.output) {
4087
+ console.error("\u2717 --watch requires --output");
4088
+ process.exitCode = 1;
4089
+ return;
4090
+ }
4091
+ const run = () => {
4092
+ try {
4093
+ const output = generateTypesFromFile(file);
4094
+ if (!opts.output) {
4095
+ process.stdout.write(output);
4096
+ } else {
4097
+ const outFile = resolve6(opts.output);
4098
+ writeFileSync5(outFile, output, "utf8");
4099
+ console.log(`\u2713 Types written to ${outFile}`);
4100
+ }
4101
+ return true;
4102
+ } catch (err) {
4103
+ console.error(`\u2717 Could not generate types: ${err.message}`);
4104
+ return false;
4105
+ }
4106
+ };
4107
+ const ok2 = run();
4108
+ if (!opts.watch) {
4109
+ if (!ok2) process.exitCode = 1;
4110
+ return;
4111
+ }
4112
+ console.log(`\u{1F440} Watching ${resolve6(file)} \u2014 Ctrl+C to stop`);
4113
+ watchFile2(resolve6(file), { interval: 300 }, (curr, prev) => {
4114
+ if (curr.mtimeMs !== prev.mtimeMs) run();
4115
+ });
4116
+ });
4117
+ }
4118
+
3731
4119
  // src/cli/index.ts
3732
4120
  var require2 = createRequire(import.meta.url);
3733
4121
  var { version } = require2("../../package.json");
@@ -3736,4 +4124,5 @@ registerInit(program);
3736
4124
  registerServe(program);
3737
4125
  registerHandler(program);
3738
4126
  registerOpenApi(program);
4127
+ registerTypes(program);
3739
4128
  program.parse();