@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/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { ServerResponse } from 'node:http';
2
3
  import * as fastify from 'fastify';
3
4
  import * as http from 'http';
4
5
 
@@ -317,8 +318,13 @@ type SseRoute = {
317
318
  loop: boolean;
318
319
  /** Stop after this many full cycles. Omit for infinite. */
319
320
  repeat?: number;
320
- /** Ordered list of events to emit. */
321
+ /** Ordered list of events to emit. Empty when {@link SseRoute.handler} drives the stream. */
321
322
  events: SseEvent[];
323
+ /**
324
+ * Name of an SSE handler exported from the handlers file (`_sse: { _handler: name }`).
325
+ * When set, the handler takes full control of the stream and `events` is not used.
326
+ */
327
+ handler?: string;
322
328
  };
323
329
  /**
324
330
  * In-memory store backed by a YAML file.
@@ -506,6 +512,39 @@ declare const yrestOptionsSchema: z.ZodObject<{
506
512
  */
507
513
  type YrestOptions = z.infer<typeof yrestOptionsSchema>;
508
514
 
515
+ /**
516
+ * Typed CRUD access to one collection of a running yrest instance.
517
+ *
518
+ * Writes go through the same service layer as the REST routes, so they are visible to
519
+ * HTTP clients immediately and persisted to `db.yml` exactly like a `POST` / `PATCH` /
520
+ * `DELETE` (a no-op for in-memory data).
521
+ *
522
+ * Returned items are copies: mutating them does not change the stored data — use
523
+ * {@link CollectionHandle.update} instead.
524
+ *
525
+ * @typeParam T - Item type, typically generated by `yrest types`.
526
+ */
527
+ interface CollectionHandle<T extends object = Resource> {
528
+ /** Every item in the collection, in stored order. */
529
+ findAll(): T[];
530
+ /** The item with the given id, or `undefined`. Ids compare as strings (`1` matches `"1"`). */
531
+ findById(id: string | number): T | undefined;
532
+ /** Every item for which `predicate` returns `true`. */
533
+ findWhere(predicate: (item: T) => boolean): T[];
534
+ /** Number of items in the collection. */
535
+ count(): number;
536
+ /** Adds an item. The `id` is generated with the server's id strategy when omitted. */
537
+ create(data: Omit<T, "id"> & {
538
+ id?: unknown;
539
+ }): T;
540
+ /** Merges `patch` into the item. The `id` cannot be changed. Returns `undefined` if not found. */
541
+ update(id: string | number, patch: Partial<Omit<T, "id">>): T | undefined;
542
+ /** Removes the item and returns it, or `undefined` if not found. */
543
+ delete(id: string | number): T | undefined;
544
+ }
545
+ /** Function that returns a {@link CollectionHandle} for a collection name. */
546
+ type CollectionAccessor = <T extends object = Resource>(name: string) => CollectionHandle<T>;
547
+
509
548
  /** Incoming request data passed to every handler function. */
510
549
  type HandlerRequest = {
511
550
  params: Record<string, string>;
@@ -522,10 +561,60 @@ type HandlerResponse = {
522
561
  /** Additional response headers to set on the reply. */
523
562
  headers?: Record<string, string>;
524
563
  };
564
+ /**
565
+ * Server context passed to every handler as its last argument.
566
+ *
567
+ * Gives handlers typed access to the live collections, so a mock can read and change the
568
+ * same data the REST routes serve (e.g. a login handler that looks the user up).
569
+ */
570
+ type HandlerContext = {
571
+ /** Returns a typed CRUD handle for a collection. Throws if the collection does not exist. */
572
+ collection: CollectionAccessor;
573
+ };
525
574
  /** A function that handles a custom route request and returns a response descriptor. */
526
- type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>;
527
- /** Map of exported function name → handler function. */
528
- type HandlerMap = Map<string, Handler>;
575
+ type Handler = (req: HandlerRequest, ctx: HandlerContext) => HandlerResponse | Promise<HandlerResponse>;
576
+ /**
577
+ * A function that takes full control of an SSE stream (`_sse: { _handler: name }`).
578
+ *
579
+ * yrest sets the SSE headers, flushes them and sends a keep-alive comment every 15 s.
580
+ * The handler writes frames to `raw` and decides when to `raw.end()`. Listen to
581
+ * `raw.on("close", ...)` to stop work when the client disconnects.
582
+ */
583
+ type SseHandler = (req: HandlerRequest, raw: ServerResponse, ctx: HandlerContext) => void | Promise<void>;
584
+ /** Map of exported function name → handler function (HTTP or SSE). */
585
+ type HandlerMap = Map<string, Handler | SseHandler>;
586
+ /**
587
+ * Identity wrapper that types a custom route handler. It has no runtime effect.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * import { defineHandler } from "@yrest/cli";
592
+ * import type { User } from "./db.types";
593
+ *
594
+ * export const login = defineHandler((req, { collection }) => {
595
+ * const { email } = (req.body ?? {}) as { email?: string };
596
+ * const user = collection<User>("users").findWhere((u) => u.email === email)[0];
597
+ * return user ? { body: { token: `tok-${user.id}` } } : { status: 401, body: { error: "Unknown user" } };
598
+ * });
599
+ * ```
600
+ */
601
+ declare function defineHandler(fn: Handler): Handler;
602
+ /**
603
+ * Identity wrapper that types an SSE stream handler. It has no runtime effect.
604
+ *
605
+ * @example
606
+ * ```ts
607
+ * import { defineSseHandler } from "@yrest/cli";
608
+ *
609
+ * export const clock = defineSseHandler((req, raw) => {
610
+ * const timer = setInterval(() => {
611
+ * raw.write(`event: tick\ndata: ${JSON.stringify({ now: Date.now() })}\n\n`);
612
+ * }, 1000);
613
+ * raw.on("close", () => clearInterval(timer));
614
+ * });
615
+ * ```
616
+ */
617
+ declare function defineSseHandler(fn: SseHandler): SseHandler;
529
618
 
530
619
  /**
531
620
  * Creates and configures a Fastify instance wired to the given YAML storage.
@@ -555,6 +644,21 @@ interface YrestServer {
555
644
  readonly port: number;
556
645
  /** The base URL of the server (e.g. `http://localhost:3070`). Only valid after `start()`. */
557
646
  readonly url: string;
647
+ /**
648
+ * Returns a typed CRUD handle for a collection of the running data.
649
+ * Changes are visible to HTTP clients immediately and persisted like REST writes.
650
+ *
651
+ * @example
652
+ * ```ts
653
+ * import type { User } from "./db.types"; // from `yrest types`
654
+ * const users = server.collection<User>("users");
655
+ * users.create({ name: "Ana" });
656
+ * expect(users.findAll()).toHaveLength(1);
657
+ * ```
658
+ *
659
+ * @throws {Error} If the collection does not exist, or (for `createYrestServer`) before `start()`.
660
+ */
661
+ collection<T extends object = Resource>(name: string): CollectionHandle<T>;
558
662
  }
559
663
  /**
560
664
  * Creates a {@link YrestServer} from an already-initialised storage and parsed options.
@@ -642,4 +746,32 @@ declare function createYrestServer(options: YrestServerOptions): YrestServer;
642
746
 
643
747
  declare function createYrestStorage(filePath: string): YrestStorage;
644
748
 
645
- export { type CustomRoute, type Data, type Handler, type HandlerMap, type HandlerRequest, type HandlerResponse, type Relations, type Resource, type YrestOptions, type YrestServer, type YrestServerBaseOptions, type YrestServerOptions, type YrestStorage, createServer, createYrestServer, createYrestServerFromStorage, createYrestStorage, yrest, yrestOptionsSchema };
749
+ /** Input for {@link generateTypes}: the parsed pieces of a `db.yml` file. */
750
+ type GenerateTypesInput = {
751
+ /** Collections and their items. */
752
+ data: Data;
753
+ /** Field-level declarations from `_schema` (optional). */
754
+ schema?: SchemaBlock;
755
+ /** Relations from `_rel` — used to annotate FK fields (optional). */
756
+ relations?: Relations;
757
+ };
758
+ /** Options for {@link generateTypes}. */
759
+ type GenerateTypesOptions = {
760
+ /** Source file name shown in the header comment (e.g. `"db.yml"`). */
761
+ source?: string;
762
+ };
763
+ /**
764
+ * Generates TypeScript declarations from a yrest dataset.
765
+ *
766
+ * - One `interface` per collection, named in singular PascalCase (`post_tags` → `PostTag`).
767
+ * - Types are inferred from **all** items; a field missing on any item is optional.
768
+ * - `_schema` declarations override inference (`_type`, `_required`, `_enum`, `_nullable`, `_items`)
769
+ * and add JSDoc (`_description`, `_format`, `_deprecated`).
770
+ * - `_rel` many2one/one2one FK fields get a `FK → target.id` JSDoc note.
771
+ * - Ends with a `CollectionName` union and a `Collections` map (`{ users: User[] }`).
772
+ *
773
+ * Output is deterministic: collections and fields keep their order of appearance.
774
+ */
775
+ declare function generateTypes(input: GenerateTypesInput, options?: GenerateTypesOptions): string;
776
+
777
+ export { type CollectionAccessor, type CollectionHandle, type CustomRoute, type Data, type GenerateTypesInput, type GenerateTypesOptions, type Handler, type HandlerContext, type HandlerMap, type HandlerRequest, type HandlerResponse, type Relations, type Resource, type SseHandler, type YrestOptions, type YrestServer, type YrestServerBaseOptions, type YrestServerOptions, type YrestStorage, createServer, createYrestServer, createYrestServerFromStorage, createYrestStorage, defineHandler, defineSseHandler, generateTypes, yrest, yrestOptionsSchema };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { ServerResponse } from 'node:http';
2
3
  import * as fastify from 'fastify';
3
4
  import * as http from 'http';
4
5
 
@@ -317,8 +318,13 @@ type SseRoute = {
317
318
  loop: boolean;
318
319
  /** Stop after this many full cycles. Omit for infinite. */
319
320
  repeat?: number;
320
- /** Ordered list of events to emit. */
321
+ /** Ordered list of events to emit. Empty when {@link SseRoute.handler} drives the stream. */
321
322
  events: SseEvent[];
323
+ /**
324
+ * Name of an SSE handler exported from the handlers file (`_sse: { _handler: name }`).
325
+ * When set, the handler takes full control of the stream and `events` is not used.
326
+ */
327
+ handler?: string;
322
328
  };
323
329
  /**
324
330
  * In-memory store backed by a YAML file.
@@ -506,6 +512,39 @@ declare const yrestOptionsSchema: z.ZodObject<{
506
512
  */
507
513
  type YrestOptions = z.infer<typeof yrestOptionsSchema>;
508
514
 
515
+ /**
516
+ * Typed CRUD access to one collection of a running yrest instance.
517
+ *
518
+ * Writes go through the same service layer as the REST routes, so they are visible to
519
+ * HTTP clients immediately and persisted to `db.yml` exactly like a `POST` / `PATCH` /
520
+ * `DELETE` (a no-op for in-memory data).
521
+ *
522
+ * Returned items are copies: mutating them does not change the stored data — use
523
+ * {@link CollectionHandle.update} instead.
524
+ *
525
+ * @typeParam T - Item type, typically generated by `yrest types`.
526
+ */
527
+ interface CollectionHandle<T extends object = Resource> {
528
+ /** Every item in the collection, in stored order. */
529
+ findAll(): T[];
530
+ /** The item with the given id, or `undefined`. Ids compare as strings (`1` matches `"1"`). */
531
+ findById(id: string | number): T | undefined;
532
+ /** Every item for which `predicate` returns `true`. */
533
+ findWhere(predicate: (item: T) => boolean): T[];
534
+ /** Number of items in the collection. */
535
+ count(): number;
536
+ /** Adds an item. The `id` is generated with the server's id strategy when omitted. */
537
+ create(data: Omit<T, "id"> & {
538
+ id?: unknown;
539
+ }): T;
540
+ /** Merges `patch` into the item. The `id` cannot be changed. Returns `undefined` if not found. */
541
+ update(id: string | number, patch: Partial<Omit<T, "id">>): T | undefined;
542
+ /** Removes the item and returns it, or `undefined` if not found. */
543
+ delete(id: string | number): T | undefined;
544
+ }
545
+ /** Function that returns a {@link CollectionHandle} for a collection name. */
546
+ type CollectionAccessor = <T extends object = Resource>(name: string) => CollectionHandle<T>;
547
+
509
548
  /** Incoming request data passed to every handler function. */
510
549
  type HandlerRequest = {
511
550
  params: Record<string, string>;
@@ -522,10 +561,60 @@ type HandlerResponse = {
522
561
  /** Additional response headers to set on the reply. */
523
562
  headers?: Record<string, string>;
524
563
  };
564
+ /**
565
+ * Server context passed to every handler as its last argument.
566
+ *
567
+ * Gives handlers typed access to the live collections, so a mock can read and change the
568
+ * same data the REST routes serve (e.g. a login handler that looks the user up).
569
+ */
570
+ type HandlerContext = {
571
+ /** Returns a typed CRUD handle for a collection. Throws if the collection does not exist. */
572
+ collection: CollectionAccessor;
573
+ };
525
574
  /** A function that handles a custom route request and returns a response descriptor. */
526
- type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerResponse>;
527
- /** Map of exported function name → handler function. */
528
- type HandlerMap = Map<string, Handler>;
575
+ type Handler = (req: HandlerRequest, ctx: HandlerContext) => HandlerResponse | Promise<HandlerResponse>;
576
+ /**
577
+ * A function that takes full control of an SSE stream (`_sse: { _handler: name }`).
578
+ *
579
+ * yrest sets the SSE headers, flushes them and sends a keep-alive comment every 15 s.
580
+ * The handler writes frames to `raw` and decides when to `raw.end()`. Listen to
581
+ * `raw.on("close", ...)` to stop work when the client disconnects.
582
+ */
583
+ type SseHandler = (req: HandlerRequest, raw: ServerResponse, ctx: HandlerContext) => void | Promise<void>;
584
+ /** Map of exported function name → handler function (HTTP or SSE). */
585
+ type HandlerMap = Map<string, Handler | SseHandler>;
586
+ /**
587
+ * Identity wrapper that types a custom route handler. It has no runtime effect.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * import { defineHandler } from "@yrest/cli";
592
+ * import type { User } from "./db.types";
593
+ *
594
+ * export const login = defineHandler((req, { collection }) => {
595
+ * const { email } = (req.body ?? {}) as { email?: string };
596
+ * const user = collection<User>("users").findWhere((u) => u.email === email)[0];
597
+ * return user ? { body: { token: `tok-${user.id}` } } : { status: 401, body: { error: "Unknown user" } };
598
+ * });
599
+ * ```
600
+ */
601
+ declare function defineHandler(fn: Handler): Handler;
602
+ /**
603
+ * Identity wrapper that types an SSE stream handler. It has no runtime effect.
604
+ *
605
+ * @example
606
+ * ```ts
607
+ * import { defineSseHandler } from "@yrest/cli";
608
+ *
609
+ * export const clock = defineSseHandler((req, raw) => {
610
+ * const timer = setInterval(() => {
611
+ * raw.write(`event: tick\ndata: ${JSON.stringify({ now: Date.now() })}\n\n`);
612
+ * }, 1000);
613
+ * raw.on("close", () => clearInterval(timer));
614
+ * });
615
+ * ```
616
+ */
617
+ declare function defineSseHandler(fn: SseHandler): SseHandler;
529
618
 
530
619
  /**
531
620
  * Creates and configures a Fastify instance wired to the given YAML storage.
@@ -555,6 +644,21 @@ interface YrestServer {
555
644
  readonly port: number;
556
645
  /** The base URL of the server (e.g. `http://localhost:3070`). Only valid after `start()`. */
557
646
  readonly url: string;
647
+ /**
648
+ * Returns a typed CRUD handle for a collection of the running data.
649
+ * Changes are visible to HTTP clients immediately and persisted like REST writes.
650
+ *
651
+ * @example
652
+ * ```ts
653
+ * import type { User } from "./db.types"; // from `yrest types`
654
+ * const users = server.collection<User>("users");
655
+ * users.create({ name: "Ana" });
656
+ * expect(users.findAll()).toHaveLength(1);
657
+ * ```
658
+ *
659
+ * @throws {Error} If the collection does not exist, or (for `createYrestServer`) before `start()`.
660
+ */
661
+ collection<T extends object = Resource>(name: string): CollectionHandle<T>;
558
662
  }
559
663
  /**
560
664
  * Creates a {@link YrestServer} from an already-initialised storage and parsed options.
@@ -642,4 +746,32 @@ declare function createYrestServer(options: YrestServerOptions): YrestServer;
642
746
 
643
747
  declare function createYrestStorage(filePath: string): YrestStorage;
644
748
 
645
- export { type CustomRoute, type Data, type Handler, type HandlerMap, type HandlerRequest, type HandlerResponse, type Relations, type Resource, type YrestOptions, type YrestServer, type YrestServerBaseOptions, type YrestServerOptions, type YrestStorage, createServer, createYrestServer, createYrestServerFromStorage, createYrestStorage, yrest, yrestOptionsSchema };
749
+ /** Input for {@link generateTypes}: the parsed pieces of a `db.yml` file. */
750
+ type GenerateTypesInput = {
751
+ /** Collections and their items. */
752
+ data: Data;
753
+ /** Field-level declarations from `_schema` (optional). */
754
+ schema?: SchemaBlock;
755
+ /** Relations from `_rel` — used to annotate FK fields (optional). */
756
+ relations?: Relations;
757
+ };
758
+ /** Options for {@link generateTypes}. */
759
+ type GenerateTypesOptions = {
760
+ /** Source file name shown in the header comment (e.g. `"db.yml"`). */
761
+ source?: string;
762
+ };
763
+ /**
764
+ * Generates TypeScript declarations from a yrest dataset.
765
+ *
766
+ * - One `interface` per collection, named in singular PascalCase (`post_tags` → `PostTag`).
767
+ * - Types are inferred from **all** items; a field missing on any item is optional.
768
+ * - `_schema` declarations override inference (`_type`, `_required`, `_enum`, `_nullable`, `_items`)
769
+ * and add JSDoc (`_description`, `_format`, `_deprecated`).
770
+ * - `_rel` many2one/one2one FK fields get a `FK → target.id` JSDoc note.
771
+ * - Ends with a `CollectionName` union and a `Collections` map (`{ users: User[] }`).
772
+ *
773
+ * Output is deterministic: collections and fields keep their order of appearance.
774
+ */
775
+ declare function generateTypes(input: GenerateTypesInput, options?: GenerateTypesOptions): string;
776
+
777
+ export { type CollectionAccessor, type CollectionHandle, type CustomRoute, type Data, type GenerateTypesInput, type GenerateTypesOptions, type Handler, type HandlerContext, type HandlerMap, type HandlerRequest, type HandlerResponse, type Relations, type Resource, type SseHandler, type YrestOptions, type YrestServer, type YrestServerBaseOptions, type YrestServerOptions, type YrestStorage, createServer, createYrestServer, createYrestServerFromStorage, createYrestStorage, defineHandler, defineSseHandler, generateTypes, yrest, yrestOptionsSchema };