@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.
- package/README.md +232 -37
- package/dist/cli/index.js +424 -35
- package/dist/cli/index.mjs +424 -35
- package/dist/index.d.mts +137 -5
- package/dist/index.d.ts +137 -5
- package/dist/index.js +399 -72
- package/dist/index.mjs +396 -72
- package/package.json +1 -1
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
|
-
|
|
730
|
-
|
|
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 <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((
|
|
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
|
|
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 .
|
|
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
|
|
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
|
|
3648
|
-
// Handler signature: (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>
|
|
3649
|
-
// See https://github.com/aggiovato/
|
|
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
|
|
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
|
|
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,
|
|
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 (
|
|
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();
|