@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/index.mjs CHANGED
@@ -130,6 +130,62 @@ var init_parseRelations = __esm({
130
130
  }
131
131
  });
132
132
 
133
+ // src/storage/parseRoutes.ts
134
+ function parseRoutes(raw) {
135
+ if (!Array.isArray(raw)) return [];
136
+ return raw.map(normaliseRoute).filter((r) => r !== null);
137
+ }
138
+ function normaliseRoute(raw) {
139
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
140
+ const v = raw;
141
+ const method = v["_method"];
142
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
143
+ if ("_sse" in v) return null;
144
+ const path2 = v["_path"];
145
+ if (typeof method !== "string" || typeof path2 !== "string") return null;
146
+ const route = { method, path: path2 };
147
+ if (typeof v["_handler"] === "string") route.handler = v["_handler"];
148
+ if (typeof v["_delay"] === "number") route.delay = v["_delay"];
149
+ if (typeof v["_error"] === "number") route.error = v["_error"];
150
+ if (v["_errorBody"] !== void 0) route.errorBody = v["_errorBody"];
151
+ const response = normaliseResponse(v["_response"]);
152
+ if (response) route.response = response;
153
+ const otherwise = normaliseResponse(v["_otherwise"]);
154
+ if (otherwise) route.otherwise = otherwise;
155
+ const scenarios = normaliseScenarios(v["_scenarios"]);
156
+ if (scenarios.length > 0) route.scenarios = scenarios;
157
+ return route;
158
+ }
159
+ function normaliseResponse(raw) {
160
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
161
+ const v = raw;
162
+ const response = {};
163
+ if (typeof v["_status"] === "number") response.status = v["_status"];
164
+ if (v["_body"] !== void 0) response.body = v["_body"];
165
+ if (v["_headers"] && typeof v["_headers"] === "object" && !Array.isArray(v["_headers"]))
166
+ response.headers = v["_headers"];
167
+ return Object.keys(response).length > 0 ? response : void 0;
168
+ }
169
+ function normaliseScenarios(raw) {
170
+ if (!Array.isArray(raw)) return [];
171
+ return raw.map(normaliseScenario).filter((s) => s !== null);
172
+ }
173
+ function normaliseScenario(raw) {
174
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
175
+ const v = raw;
176
+ const when = v["_when"];
177
+ if (!when || typeof when !== "object") return null;
178
+ const response = normaliseResponse(v["_response"]);
179
+ if (!response) return null;
180
+ return { when, response };
181
+ }
182
+ var init_parseRoutes = __esm({
183
+ "src/storage/parseRoutes.ts"() {
184
+ "use strict";
185
+ init_esm_shims();
186
+ }
187
+ });
188
+
133
189
  // src/storage/parseSseRoutes.ts
134
190
  function parseSseRoutes(raw) {
135
191
  if (!Array.isArray(raw)) return [];
@@ -146,11 +202,12 @@ function normaliseSseRoute(raw) {
146
202
  return null;
147
203
  }
148
204
  const s = sse;
205
+ const handler = typeof s["_handler"] === "string" && s["_handler"].trim() ? s["_handler"].trim() : void 0;
149
206
  const rawEvents = s["_events"];
150
- if (!Array.isArray(rawEvents)) return null;
151
- const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
152
- if (!events.length) return null;
207
+ const events = Array.isArray(rawEvents) ? rawEvents.map(normaliseEvent).filter((e) => e !== null) : [];
208
+ if (!handler && !events.length) return null;
153
209
  return {
210
+ ...handler ? { handler } : {},
154
211
  path: path2,
155
212
  interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
156
213
  loop: s["_loop"] !== false,
@@ -252,62 +309,6 @@ var init_deepCopy = __esm({
252
309
  }
253
310
  });
254
311
 
255
- // src/storage/parseRoutes.ts
256
- function parseRoutes(raw) {
257
- if (!Array.isArray(raw)) return [];
258
- return raw.map(normaliseRoute).filter((r) => r !== null);
259
- }
260
- function normaliseRoute(raw) {
261
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
262
- const v = raw;
263
- const method = v["_method"];
264
- if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
265
- if ("_sse" in v) return null;
266
- const path2 = v["_path"];
267
- if (typeof method !== "string" || typeof path2 !== "string") return null;
268
- const route = { method, path: path2 };
269
- if (typeof v["_handler"] === "string") route.handler = v["_handler"];
270
- if (typeof v["_delay"] === "number") route.delay = v["_delay"];
271
- if (typeof v["_error"] === "number") route.error = v["_error"];
272
- if (v["_errorBody"] !== void 0) route.errorBody = v["_errorBody"];
273
- const response = normaliseResponse(v["_response"]);
274
- if (response) route.response = response;
275
- const otherwise = normaliseResponse(v["_otherwise"]);
276
- if (otherwise) route.otherwise = otherwise;
277
- const scenarios = normaliseScenarios(v["_scenarios"]);
278
- if (scenarios.length > 0) route.scenarios = scenarios;
279
- return route;
280
- }
281
- function normaliseResponse(raw) {
282
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
283
- const v = raw;
284
- const response = {};
285
- if (typeof v["_status"] === "number") response.status = v["_status"];
286
- if (v["_body"] !== void 0) response.body = v["_body"];
287
- if (v["_headers"] && typeof v["_headers"] === "object" && !Array.isArray(v["_headers"]))
288
- response.headers = v["_headers"];
289
- return Object.keys(response).length > 0 ? response : void 0;
290
- }
291
- function normaliseScenarios(raw) {
292
- if (!Array.isArray(raw)) return [];
293
- return raw.map(normaliseScenario).filter((s) => s !== null);
294
- }
295
- function normaliseScenario(raw) {
296
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
297
- const v = raw;
298
- const when = v["_when"];
299
- if (!when || typeof when !== "object") return null;
300
- const response = normaliseResponse(v["_response"]);
301
- if (!response) return null;
302
- return { when, response };
303
- }
304
- var init_parseRoutes = __esm({
305
- "src/storage/parseRoutes.ts"() {
306
- "use strict";
307
- init_esm_shims();
308
- }
309
- });
310
-
311
312
  // src/storage/parseData.ts
312
313
  function parseData(raw) {
313
314
  const block = raw["_data"];
@@ -591,6 +592,7 @@ function dedent(str) {
591
592
  // src/api/yrestServer.ts
592
593
  init_esm_shims();
593
594
  init_parseRelations();
595
+ init_parseRoutes();
594
596
  init_parseSseRoutes();
595
597
  init_parseSchema();
596
598
  import { resolve as resolve2 } from "path";
@@ -598,12 +600,18 @@ import { resolve as resolve2 } from "path";
598
600
  // src/utils/handlers.ts
599
601
  init_esm_shims();
600
602
  import { existsSync } from "fs";
601
- var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs"];
603
+ function defineHandler(fn) {
604
+ return fn;
605
+ }
606
+ function defineSseHandler(fn) {
607
+ return fn;
608
+ }
609
+ var ALLOWED_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts"];
602
610
  async function loadHandlers(filePath) {
603
611
  if (!existsSync(filePath)) return /* @__PURE__ */ new Map();
604
612
  if (!ALLOWED_EXTENSIONS.some((ext) => filePath.endsWith(ext))) {
605
613
  console.error(
606
- ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs and .cjs files are allowed\x1B[0m`
614
+ ` \x1B[31m[handlers] ${filePath} \u2014 only .js, .mjs, .cjs, .ts and .mts files are allowed\x1B[0m`
607
615
  );
608
616
  return /* @__PURE__ */ new Map();
609
617
  }
@@ -900,6 +908,10 @@ function handlersAccordion(handlers, routes, base) {
900
908
  function sseRoutesAccordion(routes, base) {
901
909
  if (!routes.length) return "";
902
910
  const rows = routes.map((r) => {
911
+ if (r.handler) {
912
+ const desc2 = `SSE stream&ensp;<span style="color:#0ea5e9;font-size:11px">\u2192 ${escapeHtml(r.handler)}()</span>`;
913
+ return endpointRow("SSE", `${base}${r.path}`, desc2);
914
+ }
903
915
  const tags = [
904
916
  `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
905
917
  `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
@@ -2210,6 +2222,33 @@ var CollectionRouteCommand = class {
2210
2222
  // src/router/routes/custom.routes.ts
2211
2223
  init_esm_shims();
2212
2224
 
2225
+ // src/api/collection.ts
2226
+ init_esm_shims();
2227
+ function createCollectionHandle(storage, name, idStrategy = "increment") {
2228
+ if (!storage.getCollection(name)) {
2229
+ const known = Object.keys(storage.getData()).join(", ") || "none";
2230
+ throw new Error(`Collection "${name}" does not exist (known collections: ${known})`);
2231
+ }
2232
+ const items = () => storage.getCollection(name) ?? [];
2233
+ const copy = (item) => item === void 0 ? void 0 : structuredClone(item);
2234
+ return {
2235
+ findAll: () => items().map((i) => copy(i)),
2236
+ findById: (id) => copy(findById(items(), String(id))),
2237
+ findWhere: (predicate) => items().map((i) => copy(i)).filter(predicate),
2238
+ count: () => items().length,
2239
+ create: (data) => copy(createItem(storage, name, { ...data }, idStrategy)),
2240
+ update: (id, patch) => {
2241
+ const rest = { ...patch };
2242
+ delete rest["id"];
2243
+ return copy(patchItem(storage, name, String(id), rest));
2244
+ },
2245
+ delete: (id) => copy(deleteItem(storage, name, String(id)))
2246
+ };
2247
+ }
2248
+ function collectionAccessor(storage, idStrategy = "increment") {
2249
+ return (name) => createCollectionHandle(storage, name, idStrategy);
2250
+ }
2251
+
2213
2252
  // src/utils/conditions.ts
2214
2253
  init_esm_shims();
2215
2254
  function resolveRequestPath(dotPath, req) {
@@ -2273,15 +2312,20 @@ function resolveBody(body, ctx) {
2273
2312
  return body ?? null;
2274
2313
  }
2275
2314
  var CustomRouteCommand = class {
2276
- constructor(storage, base, handlers = /* @__PURE__ */ new Map()) {
2315
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map(), idStrategy = "increment") {
2277
2316
  this.storage = storage;
2278
2317
  this.base = base;
2279
2318
  this.handlers = handlers;
2319
+ this.idStrategy = idStrategy;
2280
2320
  }
2281
2321
  storage;
2282
2322
  base;
2283
2323
  handlers;
2324
+ idStrategy;
2284
2325
  register(server) {
2326
+ const handlerCtx = {
2327
+ collection: collectionAccessor(this.storage, this.idStrategy)
2328
+ };
2285
2329
  for (const route of this.storage.getRoutes()) {
2286
2330
  const method = route.method?.toUpperCase();
2287
2331
  const path2 = route.path;
@@ -2318,7 +2362,7 @@ var CustomRouteCommand = class {
2318
2362
  body: req.body,
2319
2363
  headers: req.headers
2320
2364
  };
2321
- const result = await fn(ctx2);
2365
+ const result = await fn(ctx2, handlerCtx);
2322
2366
  const resStatus = result.status ?? 200;
2323
2367
  for (const [k, v] of Object.entries(result.headers ?? {})) {
2324
2368
  reply.header(k, v);
@@ -2933,26 +2977,64 @@ var SnapshotRouteCommand = class {
2933
2977
  // src/router/routes/sse.routes.ts
2934
2978
  init_esm_shims();
2935
2979
  var SSERouteCommand = class {
2936
- constructor(storage, base) {
2980
+ constructor(storage, base, handlers = /* @__PURE__ */ new Map(), idStrategy = "increment") {
2937
2981
  this.storage = storage;
2938
2982
  this.base = base;
2983
+ this.handlers = handlers;
2984
+ this.idStrategy = idStrategy;
2939
2985
  }
2940
2986
  storage;
2941
2987
  base;
2988
+ handlers;
2989
+ idStrategy;
2942
2990
  register(server) {
2991
+ const handlerCtx = {
2992
+ collection: collectionAccessor(this.storage, this.idStrategy)
2993
+ };
2943
2994
  for (const route of this.storage.getSseRoutes()) {
2944
2995
  const url = `${this.base}${route.path}`;
2945
2996
  server.route({
2946
2997
  method: "GET",
2947
2998
  url,
2948
2999
  handler: async (req, reply) => {
3000
+ if (route.handler) {
3001
+ const fn = this.handlers.get(route.handler);
3002
+ if (!fn) {
3003
+ return reply.status(501).send({ error: `Handler "${route.handler}" is not defined in the handlers file` });
3004
+ }
3005
+ reply.hijack();
3006
+ const raw2 = reply.raw;
3007
+ openStream(raw2);
3008
+ const keepAlive2 = setInterval(() => {
3009
+ if (!raw2.writableEnded) raw2.write(": ping\n\n");
3010
+ }, 15e3);
3011
+ raw2.on("close", () => clearInterval(keepAlive2));
3012
+ const ctx = {
3013
+ params: req.params,
3014
+ query: req.query,
3015
+ body: null,
3016
+ headers: req.headers
3017
+ };
3018
+ try {
3019
+ await fn(ctx, raw2, handlerCtx);
3020
+ } catch (err) {
3021
+ const msg = err instanceof Error ? err.message : String(err);
3022
+ clearInterval(keepAlive2);
3023
+ if (!raw2.writableEnded) {
3024
+ raw2.write(
3025
+ `event: error
3026
+ data: ${JSON.stringify({ error: `Handler "${route.handler}" threw an error: ${msg}` })}
3027
+
3028
+ `
3029
+ );
3030
+ raw2.end();
3031
+ }
3032
+ }
3033
+ return;
3034
+ }
2949
3035
  reply.hijack();
2950
3036
  const raw = reply.raw;
2951
- raw.setHeader("Content-Type", "text/event-stream");
2952
- raw.setHeader("Cache-Control", "no-cache");
2953
- raw.setHeader("Connection", "keep-alive");
2954
- raw.setHeader("Access-Control-Allow-Origin", "*");
2955
- raw.flushHeaders();
3037
+ openStream(raw);
2956
3038
  let alive = true;
2957
3039
  req.raw.on("close", () => {
2958
3040
  alive = false;
@@ -3003,6 +3085,13 @@ var SSERouteCommand = class {
3003
3085
  }
3004
3086
  }
3005
3087
  };
3088
+ function openStream(raw) {
3089
+ raw.setHeader("Content-Type", "text/event-stream");
3090
+ raw.setHeader("Cache-Control", "no-cache");
3091
+ raw.setHeader("Connection", "keep-alive");
3092
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3093
+ raw.flushHeaders();
3094
+ }
3006
3095
 
3007
3096
  // src/router/resource.router.ts
3008
3097
  function buildResourceRouteCommands(storage, options) {
@@ -3045,8 +3134,8 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3045
3134
  new AboutRouteCommand(storage, options, handlers),
3046
3135
  new OpenApiRouteCommand(storage, options),
3047
3136
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3048
- new SSERouteCommand(storage, options.base),
3049
- new CustomRouteCommand(storage, options.base, handlers),
3137
+ new SSERouteCommand(storage, options.base, handlers, options.idStrategy),
3138
+ new CustomRouteCommand(storage, options.base, handlers, options.idStrategy),
3050
3139
  ...buildResourceRouteCommands(storage, options)
3051
3140
  ];
3052
3141
  for (const command of commands) {
@@ -3080,6 +3169,9 @@ function createYrestServerFromStorage(storage, options, handlers = /* @__PURE__
3080
3169
  },
3081
3170
  get url() {
3082
3171
  return `http://${options.host}:${_port}${options.base}`;
3172
+ },
3173
+ collection(name) {
3174
+ return createCollectionHandle(storage, name, options.idStrategy);
3083
3175
  }
3084
3176
  };
3085
3177
  }
@@ -3103,6 +3195,10 @@ function createYrestServer(options) {
3103
3195
  },
3104
3196
  get url() {
3105
3197
  return _inner?.url ?? "";
3198
+ },
3199
+ collection(name) {
3200
+ if (!_inner) throw new Error("collection() is only available after start()");
3201
+ return _inner.collection(name);
3106
3202
  }
3107
3203
  };
3108
3204
  }
@@ -3126,7 +3222,7 @@ function createInMemoryStorage(data) {
3126
3222
  const raw = data;
3127
3223
  const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
3128
3224
  const relations = parseRelations(raw["_rel"]);
3129
- const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3225
+ const routes = parseRoutes(raw["_routes"]);
3130
3226
  const sseRoutes = parseSseRoutes(raw["_routes"]);
3131
3227
  const schema = parseSchema(raw["_schema"]);
3132
3228
  const collections = Object.fromEntries(
@@ -3217,11 +3313,239 @@ var yrestOptionsSchema = z.object({
3217
3313
  */
3218
3314
  idStrategy: z.enum(["increment", "uuid"]).default("increment")
3219
3315
  });
3316
+
3317
+ // src/codegen/generateTypes.ts
3318
+ init_esm_shims();
3319
+ var HEADER = "// Auto-generated by yrest types \u2014 do not edit manually.";
3320
+ var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3321
+ var MAX_DEPTH = 4;
3322
+ function generateTypes(input, options = {}) {
3323
+ const { data, schema = {}, relations = {} } = input;
3324
+ const collections = unique([...Object.keys(data), ...Object.keys(schema)]);
3325
+ const names = resolveInterfaceNames(collections);
3326
+ const out = [HEADER];
3327
+ if (options.source) out.push(`// Source: ${options.source}`);
3328
+ out.push("");
3329
+ for (const collection of collections) {
3330
+ out.push(
3331
+ renderInterface(
3332
+ names.get(collection),
3333
+ collection,
3334
+ data[collection] ?? [],
3335
+ schema[collection] ?? {},
3336
+ relations[collection] ?? {}
3337
+ )
3338
+ );
3339
+ out.push("");
3340
+ }
3341
+ out.push(`/** Union of all collection names in the source file. */`);
3342
+ out.push(
3343
+ collections.length > 0 ? `export type CollectionName = ${collections.map((c) => quote(c)).join(" | ")};` : "export type CollectionName = never;"
3344
+ );
3345
+ out.push("");
3346
+ out.push(`/** Maps each collection name to its item array type. */`);
3347
+ out.push("export interface Collections {");
3348
+ for (const collection of collections) {
3349
+ out.push(` ${propKey(collection)}: ${names.get(collection)}[];`);
3350
+ }
3351
+ out.push("}");
3352
+ return out.join("\n") + "\n";
3353
+ }
3354
+ function renderInterface(name, collection, items, fieldDefs, rels) {
3355
+ const records = items.filter(isPlainObject);
3356
+ const fields = collectFields(records);
3357
+ const fieldNames = unique([...fields.keys(), ...Object.keys(fieldDefs)]);
3358
+ const lines = [`/** Item of the \`${collection}\` collection. */`];
3359
+ lines.push(`export interface ${name} {`);
3360
+ for (const field of fieldNames) {
3361
+ const info = fields.get(field);
3362
+ const def = fieldDefs[field];
3363
+ const inferredRequired = info !== void 0 && info.count === records.length && records.length > 0;
3364
+ const required = def?.required ?? inferredRequired;
3365
+ let type = def ? typeFromDef(def, info) : typeOfValues(info.values, 0);
3366
+ if (def?.nullable === true && !type.split(" | ").includes("null")) type = `${type} | null`;
3367
+ for (const doc of fieldDoc(field, def, rels)) lines.push(` ${doc}`);
3368
+ lines.push(` ${propKey(field)}${required ? "" : "?"}: ${type};`);
3369
+ }
3370
+ if (fieldNames.length === 0) lines.push(" [key: string]: unknown;");
3371
+ lines.push("}");
3372
+ return lines.join("\n");
3373
+ }
3374
+ function fieldDoc(field, def, rels) {
3375
+ const text = [];
3376
+ const tags = [];
3377
+ if (def?.description) text.push(def.description.replace(/\*\//g, "*\\/"));
3378
+ const rel = Object.entries(rels).find(
3379
+ ([key, r]) => r.type !== "many2many" && (r.foreignKey ?? key) === field
3380
+ )?.[1];
3381
+ if (rel) text.push(`FK \u2192 ${rel.target}.id`);
3382
+ if (def?.format) tags.push(`@format ${def.format}`);
3383
+ if (def?.deprecated) tags.push("@deprecated");
3384
+ if (text.length === 0 && tags.length === 0) return [];
3385
+ if (tags.length === 0) return [`/** ${text.join(" \u2014 ")} */`];
3386
+ return ["/**", ...[...text, ...tags].map((l) => ` * ${l}`), " */"];
3387
+ }
3388
+ function collectFields(records) {
3389
+ const fields = /* @__PURE__ */ new Map();
3390
+ for (const record of records) {
3391
+ for (const [key, value] of Object.entries(record)) {
3392
+ let info = fields.get(key);
3393
+ if (!info) {
3394
+ info = { values: [], count: 0 };
3395
+ fields.set(key, info);
3396
+ }
3397
+ info.count++;
3398
+ info.values.push(value);
3399
+ }
3400
+ }
3401
+ return fields;
3402
+ }
3403
+ function typeOfValues(values, depth) {
3404
+ return unionOf(typeSetOf(values, depth));
3405
+ }
3406
+ function typeSetOf(values, depth) {
3407
+ const types = /* @__PURE__ */ new Set();
3408
+ const objects = [];
3409
+ const elements = [];
3410
+ let sawArray = false;
3411
+ for (const value of values) {
3412
+ if (value === null || value === void 0) types.add("null");
3413
+ else if (typeof value === "string") types.add("string");
3414
+ else if (typeof value === "number") types.add("number");
3415
+ else if (typeof value === "boolean") types.add("boolean");
3416
+ else if (Array.isArray(value)) {
3417
+ sawArray = true;
3418
+ elements.push(...value);
3419
+ } else if (isPlainObject(value)) objects.push(value);
3420
+ else types.add("unknown");
3421
+ }
3422
+ if (objects.length > 0) {
3423
+ types.add(depth >= MAX_DEPTH ? "Record<string, unknown>" : inlineObject(objects, depth + 1));
3424
+ }
3425
+ if (sawArray) {
3426
+ types.add(
3427
+ elements.length === 0 ? "unknown[]" : depth >= MAX_DEPTH ? "unknown[]" : arrayOf(typeSetOf(elements, depth + 1))
3428
+ );
3429
+ }
3430
+ return types;
3431
+ }
3432
+ function inlineObject(records, depth) {
3433
+ const fields = collectFields(records);
3434
+ if (fields.size === 0) return "Record<string, unknown>";
3435
+ const props = [...fields].map(([key, info]) => {
3436
+ const optional = info.count < records.length ? "?" : "";
3437
+ return `${propKey(key)}${optional}: ${typeOfValues(info.values, depth)}`;
3438
+ });
3439
+ return `{ ${props.join("; ")} }`;
3440
+ }
3441
+ function unionOf(types) {
3442
+ const list = [...types];
3443
+ if (list.length === 0) return "unknown";
3444
+ if (list.length === 1 && list[0] === "null") return "unknown";
3445
+ const hasConcreteArray = list.some((t) => t.endsWith("[]") && t !== "unknown[]");
3446
+ const filtered = hasConcreteArray ? list.filter((t) => t !== "unknown[]") : list;
3447
+ filtered.sort((a, b) => a === "null" ? 1 : b === "null" ? -1 : 0);
3448
+ return filtered.join(" | ");
3449
+ }
3450
+ function arrayOf(elements) {
3451
+ const element = unionOf(elements);
3452
+ return element.includes(" | ") && elements.size > 1 ? `(${element})[]` : `${element}[]`;
3453
+ }
3454
+ function typeFromDef(def, info) {
3455
+ if (def.enum && def.enum.length > 0) {
3456
+ return unique(def.enum.map((v) => literal(v))).join(" | ");
3457
+ }
3458
+ if (def.type) {
3459
+ if (def.type === "array") {
3460
+ const items = def.items;
3461
+ if (items?.enum && items.enum.length > 0)
3462
+ return arrayOf(new Set(items.enum.map((v) => literal(v))));
3463
+ if (items?.type) return arrayOf(/* @__PURE__ */ new Set([primitiveFor(items.type)]));
3464
+ const inferred = info ? typeOfValues(info.values, 0) : "";
3465
+ return inferred.endsWith("[]") && !inferred.includes(" | ") ? inferred : "unknown[]";
3466
+ }
3467
+ return primitiveFor(def.type);
3468
+ }
3469
+ return info ? typeOfValues(info.values, 0) : "unknown";
3470
+ }
3471
+ function primitiveFor(type) {
3472
+ switch (type) {
3473
+ case "integer":
3474
+ case "number":
3475
+ return "number";
3476
+ case "string":
3477
+ return "string";
3478
+ case "boolean":
3479
+ return "boolean";
3480
+ case "array":
3481
+ return "unknown[]";
3482
+ case "object":
3483
+ return "Record<string, unknown>";
3484
+ }
3485
+ }
3486
+ function literal(value) {
3487
+ if (typeof value === "string") return JSON.stringify(value);
3488
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
3489
+ if (value === null) return "null";
3490
+ return "unknown";
3491
+ }
3492
+ function resolveInterfaceNames(collections) {
3493
+ const names = /* @__PURE__ */ new Map();
3494
+ const used = /* @__PURE__ */ new Set();
3495
+ for (const collection of collections) {
3496
+ const words = splitWords(collection);
3497
+ const candidates = [
3498
+ toPascal([...words.slice(0, -1), singularize(words.at(-1) ?? "")]),
3499
+ toPascal(words)
3500
+ ];
3501
+ let name = candidates.find((c) => c && !used.has(c));
3502
+ if (!name) {
3503
+ const base = candidates[1] || "Collection";
3504
+ let i = 2;
3505
+ while (used.has(`${base}${i}`)) i++;
3506
+ name = `${base}${i}`;
3507
+ }
3508
+ if (/^[0-9]/.test(name)) name = `_${name}`;
3509
+ used.add(name);
3510
+ names.set(collection, name);
3511
+ }
3512
+ return names;
3513
+ }
3514
+ function splitWords(name) {
3515
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean);
3516
+ }
3517
+ function toPascal(words) {
3518
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
3519
+ }
3520
+ function singularize(word) {
3521
+ const lower = word.toLowerCase();
3522
+ if (lower.length <= 2) return word;
3523
+ if (/(ss|us|is)$/.test(lower)) return word;
3524
+ if (/[^aeiou]ies$/.test(lower)) return word.slice(0, -3) + "y";
3525
+ if (/(s|x|z|ch|sh)es$/.test(lower)) return word.slice(0, -2);
3526
+ if (lower.endsWith("s")) return word.slice(0, -1);
3527
+ return word;
3528
+ }
3529
+ function propKey(key) {
3530
+ return IDENT_RE.test(key) ? key : JSON.stringify(key);
3531
+ }
3532
+ function quote(value) {
3533
+ return JSON.stringify(value);
3534
+ }
3535
+ function unique(list) {
3536
+ return [...new Set(list)];
3537
+ }
3538
+ function isPlainObject(value) {
3539
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3540
+ }
3220
3541
  export {
3221
3542
  createServer,
3222
3543
  createYrestServer,
3223
3544
  createYrestServerFromStorage,
3224
3545
  createYrestStorage,
3546
+ defineHandler,
3547
+ defineSseHandler,
3548
+ generateTypes,
3225
3549
  yrest,
3226
3550
  yrestOptionsSchema
3227
3551
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.",
5
5
  "keywords": [
6
6
  "yrest",