@fougere/core 0.1.0-alpha.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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -0
  3. package/dist/binding.d.ts +63 -0
  4. package/dist/binding.d.ts.map +1 -0
  5. package/dist/binding.js +107 -0
  6. package/dist/binding.js.map +1 -0
  7. package/dist/boot.d.ts +38 -0
  8. package/dist/boot.d.ts.map +1 -0
  9. package/dist/boot.js +53 -0
  10. package/dist/boot.js.map +1 -0
  11. package/dist/bootstrap.d.ts +4 -0
  12. package/dist/bootstrap.d.ts.map +1 -0
  13. package/dist/bootstrap.js +825 -0
  14. package/dist/bootstrap.js.map +1 -0
  15. package/dist/builtins/config.d.ts +4 -0
  16. package/dist/builtins/config.d.ts.map +1 -0
  17. package/dist/builtins/config.js +6 -0
  18. package/dist/builtins/config.js.map +1 -0
  19. package/dist/builtins/logger.d.ts +28 -0
  20. package/dist/builtins/logger.d.ts.map +1 -0
  21. package/dist/builtins/logger.js +76 -0
  22. package/dist/builtins/logger.js.map +1 -0
  23. package/dist/call.d.ts +172 -0
  24. package/dist/call.d.ts.map +1 -0
  25. package/dist/call.js +209 -0
  26. package/dist/call.js.map +1 -0
  27. package/dist/collector.d.ts +28 -0
  28. package/dist/collector.d.ts.map +1 -0
  29. package/dist/collector.js +32 -0
  30. package/dist/collector.js.map +1 -0
  31. package/dist/config-loader.d.ts +59 -0
  32. package/dist/config-loader.d.ts.map +1 -0
  33. package/dist/config-loader.js +97 -0
  34. package/dist/config-loader.js.map +1 -0
  35. package/dist/contract.d.ts +43 -0
  36. package/dist/contract.d.ts.map +1 -0
  37. package/dist/contract.js +29 -0
  38. package/dist/contract.js.map +1 -0
  39. package/dist/crud.d.ts +61 -0
  40. package/dist/crud.d.ts.map +1 -0
  41. package/dist/crud.js +85 -0
  42. package/dist/crud.js.map +1 -0
  43. package/dist/define.d.ts +8 -0
  44. package/dist/define.d.ts.map +1 -0
  45. package/dist/define.js +9 -0
  46. package/dist/define.js.map +1 -0
  47. package/dist/egress.d.ts +78 -0
  48. package/dist/egress.d.ts.map +1 -0
  49. package/dist/egress.js +185 -0
  50. package/dist/egress.js.map +1 -0
  51. package/dist/emit.d.ts +51 -0
  52. package/dist/emit.d.ts.map +1 -0
  53. package/dist/emit.js +57 -0
  54. package/dist/emit.js.map +1 -0
  55. package/dist/errors.d.ts +22 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +74 -0
  58. package/dist/errors.js.map +1 -0
  59. package/dist/frond-config.d.ts +80 -0
  60. package/dist/frond-config.d.ts.map +1 -0
  61. package/dist/frond-config.js +32 -0
  62. package/dist/frond-config.js.map +1 -0
  63. package/dist/graph.d.ts +33 -0
  64. package/dist/graph.d.ts.map +1 -0
  65. package/dist/graph.js +142 -0
  66. package/dist/graph.js.map +1 -0
  67. package/dist/handler-parser.d.ts +82 -0
  68. package/dist/handler-parser.d.ts.map +1 -0
  69. package/dist/handler-parser.js +505 -0
  70. package/dist/handler-parser.js.map +1 -0
  71. package/dist/imports.d.ts +22 -0
  72. package/dist/imports.d.ts.map +1 -0
  73. package/dist/imports.js +80 -0
  74. package/dist/imports.js.map +1 -0
  75. package/dist/index.d.ts +43 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +37 -0
  78. package/dist/index.js.map +1 -0
  79. package/dist/invocation.d.ts +20 -0
  80. package/dist/invocation.d.ts.map +1 -0
  81. package/dist/invocation.js +15 -0
  82. package/dist/invocation.js.map +1 -0
  83. package/dist/middleware/error-handler.d.ts +15 -0
  84. package/dist/middleware/error-handler.d.ts.map +1 -0
  85. package/dist/middleware/error-handler.js +29 -0
  86. package/dist/middleware/error-handler.js.map +1 -0
  87. package/dist/middleware/logger.d.ts +14 -0
  88. package/dist/middleware/logger.d.ts.map +1 -0
  89. package/dist/middleware/logger.js +25 -0
  90. package/dist/middleware/logger.js.map +1 -0
  91. package/dist/middleware.d.ts +75 -0
  92. package/dist/middleware.d.ts.map +1 -0
  93. package/dist/middleware.js +89 -0
  94. package/dist/middleware.js.map +1 -0
  95. package/dist/operation.d.ts +87 -0
  96. package/dist/operation.d.ts.map +1 -0
  97. package/dist/operation.js +95 -0
  98. package/dist/operation.js.map +1 -0
  99. package/dist/orm.d.ts +96 -0
  100. package/dist/orm.d.ts.map +1 -0
  101. package/dist/orm.js +33 -0
  102. package/dist/orm.js.map +1 -0
  103. package/dist/presenter.d.ts +63 -0
  104. package/dist/presenter.d.ts.map +1 -0
  105. package/dist/presenter.js +72 -0
  106. package/dist/presenter.js.map +1 -0
  107. package/dist/remote.d.ts +48 -0
  108. package/dist/remote.d.ts.map +1 -0
  109. package/dist/remote.js +135 -0
  110. package/dist/remote.js.map +1 -0
  111. package/dist/repository.d.ts +55 -0
  112. package/dist/repository.d.ts.map +1 -0
  113. package/dist/repository.js +26 -0
  114. package/dist/repository.js.map +1 -0
  115. package/dist/scan-cache.d.ts +10 -0
  116. package/dist/scan-cache.d.ts.map +1 -0
  117. package/dist/scan-cache.js +92 -0
  118. package/dist/scan-cache.js.map +1 -0
  119. package/dist/scanner.d.ts +34 -0
  120. package/dist/scanner.d.ts.map +1 -0
  121. package/dist/scanner.js +595 -0
  122. package/dist/scanner.js.map +1 -0
  123. package/dist/seed.d.ts +28 -0
  124. package/dist/seed.d.ts.map +1 -0
  125. package/dist/seed.js +110 -0
  126. package/dist/seed.js.map +1 -0
  127. package/dist/types.d.ts +387 -0
  128. package/dist/types.d.ts.map +1 -0
  129. package/dist/types.js +2 -0
  130. package/dist/types.js.map +1 -0
  131. package/dist/verify.d.ts +57 -0
  132. package/dist/verify.d.ts.map +1 -0
  133. package/dist/verify.js +147 -0
  134. package/dist/verify.js.map +1 -0
  135. package/package.json +47 -0
package/dist/orm.d.ts ADDED
@@ -0,0 +1,96 @@
1
+ import type { SchemaLike } from '@fougere/schema';
2
+ /** Options for list queries — pagination, sorting, counting. */
3
+ export interface ListOptions {
4
+ /** Number of records to return. */
5
+ limit?: number;
6
+ /** Offset-based: skip N records. */
7
+ offset?: number;
8
+ /** Page-based: 1-indexed page number (requires limit). */
9
+ page?: number;
10
+ /** Cursor-based: fetch records after this ID. */
11
+ after?: string;
12
+ /** Field name to order by. */
13
+ orderBy?: string;
14
+ /** Sort direction (default: 'asc'). */
15
+ order?: 'asc' | 'desc';
16
+ /** If true, also returns total count (for pagination UIs). */
17
+ count?: boolean;
18
+ /**
19
+ * Equality criteria, field by field — `{ orderId: '…' }`. Named rather than spread
20
+ * across the options so an unknown key stays ignored instead of silently becoming a
21
+ * filter. `listBy(criteria)` is the same thing said as an intention.
22
+ */
23
+ where?: Record<string, unknown>;
24
+ }
25
+ /** Result of list() — extends Array so it's backward compatible. */
26
+ export interface ListResult<T> extends Array<T> {
27
+ /** Total number of matching records (only set when count: true). */
28
+ total?: number;
29
+ /** Cursor of the last item (for cursor-based pagination). */
30
+ endCursor?: string;
31
+ /** Whether more records exist after endCursor. */
32
+ hasMore?: boolean;
33
+ }
34
+ /**
35
+ * The keys `list()` answers to. Anything else is a mistake, and saying so is the point:
36
+ * the façade refuses an unknown key in a CLIENT's input (`Unknown field`) — this applies the
37
+ * same rule to the framework's own arguments. `list({ orderId })` used to be accepted and
38
+ * the filter dropped, so a one-to-many relation quietly returned the whole table.
39
+ */
40
+ export declare const LIST_OPTION_KEYS: readonly ["limit", "offset", "page", "after", "orderBy", "order", "count", "where", "select"];
41
+ /** Refuse an option the port does not answer to, naming it and what was expected. */
42
+ export declare function assertListOptions(options: object | undefined, entity: string): void;
43
+ /** Select option — restrict returned fields to those of a SchemaLike. */
44
+ export interface SelectOption {
45
+ select?: SchemaLike;
46
+ }
47
+ /** Per-entity ORM — scoped CRUD operations on a single entity type. */
48
+ export interface EntityOrm<T = Record<string, unknown>> {
49
+ list(options?: ListOptions & SelectOption): Promise<ListResult<T>>;
50
+ findById(id: string, options?: SelectOption): Promise<T | undefined>;
51
+ /**
52
+ * Read by criteria — `findBy({ email })` for the one, `findAllBy({ orderId })` for the
53
+ * many, which is what a one-to-many relation *is*.
54
+ *
55
+ * Both existed on the SQL implementation from the start and neither was declared here.
56
+ * A port that hides what it offers is a port nobody can use: `auth-better` cast its way
57
+ * in (`orm as OrmWithFindBy`), a presenter that needed the lines of an order read the
58
+ * whole table instead, and the GraphQL relation resolver passed criteria to `list()`
59
+ * — which drops what it does not know.
60
+ */
61
+ findBy(criteria: Partial<T> | Record<string, unknown>, options?: SelectOption): Promise<T | undefined>;
62
+ findAllBy(criteria: Partial<T> | Record<string, unknown>, options?: SelectOption): Promise<T[]>;
63
+ create(input: Partial<T>, options?: SelectOption): Promise<T>;
64
+ update(id: string, input: Partial<T>, options?: SelectOption): Promise<T>;
65
+ delete(id: string): Promise<boolean>;
66
+ /** Returns a scoped ORM that restricts all read results to the fields of the given schema. */
67
+ output(schema: SchemaLike): EntityOrm<T>;
68
+ /**
69
+ * What this ORM wraps — the Kysely instance for the SQL one, something else elsewhere.
70
+ *
71
+ * Every judge sits on the ORM's own methods, so a statement issued here meets none of
72
+ * them: a value the entity refuses lands in the table without a word. It is the port's
73
+ * own escape hatch rather than a handle on the side, so it keeps the scope the container
74
+ * gave you — `productOrm.client` reaches the products, not the whole database.
75
+ *
76
+ * `unknown` on purpose: the client belongs to the implementation, and narrowing it is
77
+ * the caller saying out loud which one they are standing on.
78
+ */
79
+ readonly client: unknown;
80
+ }
81
+ /**
82
+ * Factory that creates an EntityOrm for a given entity.
83
+ * Called by bootstrap for every scanned entity.
84
+ */
85
+ export type OrmFactory = (entity: SchemaLike, name: string) => EntityOrm;
86
+ /**
87
+ * Container key of an entity's storage — 'reading' → 'ReadingOrm'.
88
+ *
89
+ * The twin of {@link repositoryKeyOf} and of `facadeKeyOf`: the key of a thing
90
+ * lives with the thing. This one was spelled by hand in four places in
91
+ * `bootstrap.ts` and a fifth in `scanner.ts` — where the SCAN derives what a
92
+ * constructor asks for. Two readers of one convention, neither of them naming it,
93
+ * so a rename would have moved one and left the other resolving to nothing.
94
+ */
95
+ export declare function ormKeyOf(entity: string): string;
96
+ //# sourceMappingURL=orm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orm.d.ts","sourceRoot":"","sources":["../src/orm.ts"],"names":[],"mappings":"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"}
package/dist/orm.js ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The keys `list()` answers to. Anything else is a mistake, and saying so is the point:
3
+ * the façade refuses an unknown key in a CLIENT's input (`Unknown field`) — this applies the
4
+ * same rule to the framework's own arguments. `list({ orderId })` used to be accepted and
5
+ * the filter dropped, so a one-to-many relation quietly returned the whole table.
6
+ */
7
+ export const LIST_OPTION_KEYS = [
8
+ 'limit', 'offset', 'page', 'after', 'orderBy', 'order', 'count', 'where', 'select',
9
+ ];
10
+ /** Refuse an option the port does not answer to, naming it and what was expected. */
11
+ export function assertListOptions(options, entity) {
12
+ if (!options)
13
+ return;
14
+ const legal = new Set(LIST_OPTION_KEYS);
15
+ const strangers = Object.keys(options).filter((key) => !legal.has(key));
16
+ if (strangers.length === 0)
17
+ return;
18
+ throw new Error(`${entity}.list(): unknown option ${strangers.map((s) => `\`${s}\``).join(', ')}. ` +
19
+ `Known options are ${LIST_OPTION_KEYS.join(', ')} — to filter, pass \`where: { ${strangers[0]}: … }\`.`);
20
+ }
21
+ /**
22
+ * Container key of an entity's storage — 'reading' → 'ReadingOrm'.
23
+ *
24
+ * The twin of {@link repositoryKeyOf} and of `facadeKeyOf`: the key of a thing
25
+ * lives with the thing. This one was spelled by hand in four places in
26
+ * `bootstrap.ts` and a fifth in `scanner.ts` — where the SCAN derives what a
27
+ * constructor asks for. Two readers of one convention, neither of them naming it,
28
+ * so a rename would have moved one and left the other resolving to nothing.
29
+ */
30
+ export function ormKeyOf(entity) {
31
+ return `${entity[0].toUpperCase()}${entity.slice(1)}Orm`;
32
+ }
33
+ //# sourceMappingURL=orm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orm.js","sourceRoot":"","sources":["../src/orm.ts"],"names":[],"mappings":"AA2CA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ;CAC1E,CAAC;AAEX,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,OAA2B,EAAE,MAAc;IAC3E,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAS,gBAAgB,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACxE,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACnC,MAAM,IAAI,KAAK,CACb,GAAG,MAAM,2BAA2B,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;QACnF,qBAAqB,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,iCAAiC,SAAS,CAAC,CAAC,CAAC,UAAU,CACxG,CAAC;AACJ,CAAC;AAgDD;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Presenter(Entity) — enriches an entity's output with computed fields.
3
+ *
4
+ * Each method on the presenter is a computed field resolver:
5
+ * - Receives the entity record as first argument
6
+ * - Can be sync or async
7
+ * - Injected via DI (constructor params resolved by the container)
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * export default class PostPresenter extends Presenter(Post) {
12
+ * constructor(private commentOrm: CommentOrm) { super(); }
13
+ *
14
+ * excerpt(post: Post) {
15
+ * return post.body.slice(0, 200);
16
+ * }
17
+ *
18
+ * async commentCount(post: Post) {
19
+ * return this.commentOrm.countFor(post.id);
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ type EntityClass = abstract new (...args: any[]) => any;
25
+ declare const PRESENTER_TARGET: unique symbol;
26
+ declare const PRESENTER_VIEWS: unique symbol;
27
+ /**
28
+ * The view a computed field emits — `OrderItemView` for one, `[OrderItemView]` for many.
29
+ * A view is a schema, so it derives: `Order.pick('id', 'status')`, never a hand-written type.
30
+ */
31
+ export type PresenterViews = Record<string, EntityClass | [EntityClass]>;
32
+ /**
33
+ * `Presenter(Order, { items: [OrderItemView], user: UserCard })` — the second argument names
34
+ * the view each computed field emits, exactly as `Crud(Post, { list: PostCard })` names an
35
+ * op's. What fabricates a field declares its contract, and it declares it **once**: the
36
+ * façade applies the presenter (see `egress.ts`), and every surface reads the same statement
37
+ * instead of guessing.
38
+ *
39
+ * Guessing was the cost of not stating it. The scan can read a scalar off a return type and
40
+ * nothing more, so a field returning an object had no derivable shape: the GraphQL projection
41
+ * fell back to a serialized `String`, and a client asking `items { quantity }` got a schema
42
+ * error on a field REST served whole. Declaring is optional — a scalar field needs nothing,
43
+ * and an undeclared object keeps the old behaviour.
44
+ */
45
+ export declare function Presenter<E extends EntityClass>(entity: E, views?: PresenterViews): {
46
+ new (): {};
47
+ [PRESENTER_TARGET]: E;
48
+ [PRESENTER_VIEWS]: PresenterViews | undefined;
49
+ };
50
+ /** Get the entity class a presenter targets. */
51
+ export declare function getPresenterTarget(ctor: Function): EntityClass | undefined;
52
+ /**
53
+ * The views a presenter declares, walked up the prototype chain so a subclass of a
54
+ * presenter keeps what its base stated. Runtime, like `Crud.__ops`: an installed app whose
55
+ * source the scan cannot read still carries its contract.
56
+ */
57
+ export declare function getPresenterViews(ctor: Function): PresenterViews | undefined;
58
+ /** List computed field names from a presenter class (own methods minus constructor). */
59
+ export declare function getPresenterFields(ctor: Function): string[];
60
+ /** Container key of an entity's presenter — 'post' → 'PostPresenter'. */
61
+ export declare function presenterKeyOf(entity: string): string;
62
+ export {};
63
+ //# sourceMappingURL=presenter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenter.d.ts","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,KAAK,WAAW,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;AAExD,QAAA,MAAM,gBAAgB,eAAyC,CAAC;AAChE,QAAA,MAAM,eAAe,eAAwC,CAAC;AAE9D;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;AAEzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,cAAc;;;;EAMjF;AAED,gDAAgD;AAChD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,cAAc,GAAG,SAAS,CAM5E;AAED,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,EAAE,CAG3D;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Presenter(Entity) — enriches an entity's output with computed fields.
3
+ *
4
+ * Each method on the presenter is a computed field resolver:
5
+ * - Receives the entity record as first argument
6
+ * - Can be sync or async
7
+ * - Injected via DI (constructor params resolved by the container)
8
+ *
9
+ * Usage:
10
+ * ```ts
11
+ * export default class PostPresenter extends Presenter(Post) {
12
+ * constructor(private commentOrm: CommentOrm) { super(); }
13
+ *
14
+ * excerpt(post: Post) {
15
+ * return post.body.slice(0, 200);
16
+ * }
17
+ *
18
+ * async commentCount(post: Post) {
19
+ * return this.commentOrm.countFor(post.id);
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+ const PRESENTER_TARGET = Symbol.for('fougere:presenter_target');
25
+ const PRESENTER_VIEWS = Symbol.for('fougere:presenter_views');
26
+ /**
27
+ * `Presenter(Order, { items: [OrderItemView], user: UserCard })` — the second argument names
28
+ * the view each computed field emits, exactly as `Crud(Post, { list: PostCard })` names an
29
+ * op's. What fabricates a field declares its contract, and it declares it **once**: the
30
+ * façade applies the presenter (see `egress.ts`), and every surface reads the same statement
31
+ * instead of guessing.
32
+ *
33
+ * Guessing was the cost of not stating it. The scan can read a scalar off a return type and
34
+ * nothing more, so a field returning an object had no derivable shape: the GraphQL projection
35
+ * fell back to a serialized `String`, and a client asking `items { quantity }` got a schema
36
+ * error on a field REST served whole. Declaring is optional — a scalar field needs nothing,
37
+ * and an undeclared object keeps the old behaviour.
38
+ */
39
+ export function Presenter(entity, views) {
40
+ class PresenterBase {
41
+ static [PRESENTER_TARGET] = entity;
42
+ static [PRESENTER_VIEWS] = views;
43
+ }
44
+ return PresenterBase;
45
+ }
46
+ /** Get the entity class a presenter targets. */
47
+ export function getPresenterTarget(ctor) {
48
+ return ctor[PRESENTER_TARGET];
49
+ }
50
+ /**
51
+ * The views a presenter declares, walked up the prototype chain so a subclass of a
52
+ * presenter keeps what its base stated. Runtime, like `Crud.__ops`: an installed app whose
53
+ * source the scan cannot read still carries its contract.
54
+ */
55
+ export function getPresenterViews(ctor) {
56
+ for (let cur = ctor; cur; cur = Object.getPrototypeOf(cur)) {
57
+ const views = cur[PRESENTER_VIEWS];
58
+ if (views)
59
+ return views;
60
+ }
61
+ return undefined;
62
+ }
63
+ /** List computed field names from a presenter class (own methods minus constructor). */
64
+ export function getPresenterFields(ctor) {
65
+ return Object.getOwnPropertyNames(ctor.prototype)
66
+ .filter((name) => name !== 'constructor' && typeof ctor.prototype[name] === 'function');
67
+ }
68
+ /** Container key of an entity's presenter — 'post' → 'PostPresenter'. */
69
+ export function presenterKeyOf(entity) {
70
+ return `${entity[0].toUpperCase()}${entity.slice(1)}Presenter`;
71
+ }
72
+ //# sourceMappingURL=presenter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presenter.js","sourceRoot":"","sources":["../src/presenter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC;AAChE,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;AAQ9D;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAwB,MAAS,EAAE,KAAsB;IAChF,MAAM,aAAa;QACjB,MAAM,CAAC,CAAC,gBAAgB,CAAC,GAAG,MAAM,CAAC;QACnC,MAAM,CAAC,CAAC,eAAe,CAAC,GAAG,KAAK,CAAC;;IAEnC,OAAO,aAAa,CAAC;AACvB,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,kBAAkB,CAAC,IAAc;IAC/C,OAAQ,IAAY,CAAC,gBAAgB,CAAC,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAc;IAC9C,KAAK,IAAI,GAAG,GAAQ,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,KAAK,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC;QACnC,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,kBAAkB,CAAC,IAAc;IAC/C,OAAO,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC;SAC9C,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,aAAa,IAAI,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,CAAC;AAC5F,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC;AACjE,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Remote façade — what resolve() falls back to.
3
+ *
4
+ * When a handler isn't hosted locally and remotes are declared, resolve()
5
+ * returns a façade-shaped stand-in: same call surface as a local facade,
6
+ * every operation executed through a Transport. Routing is lazy — on the
7
+ * first miss, each declared remote is asked what it hosts (rpc.discover)
8
+ * and the answer is cached. A remote that can't be reached stays pending
9
+ * and is retried on the next miss instead of being cached as absent.
10
+ */
11
+ import type { Transport } from './call.js';
12
+ import { type InvocationContext } from './invocation.js';
13
+ import { type SchemaConstructor, type Fields } from '@fougere/schema';
14
+ interface Route {
15
+ frond: string;
16
+ transport: Transport;
17
+ /**
18
+ * The entity's schema, rebuilt from the identity card — the same
19
+ * `SchemaConstructor` shape `entity({...})` produces, live validation
20
+ * included. Reconstructed once per entity, at discovery time.
21
+ *
22
+ * **Absent when the door stores nothing.** A handler may carry no entity, so the card
23
+ * publishes it with ops and no shape; there is nothing to rebuild and pretending
24
+ * otherwise would hand callers an empty schema that validates everything.
25
+ */
26
+ schema?: SchemaConstructor<Fields>;
27
+ }
28
+ export interface RemoteRouter {
29
+ route(entity: string): Promise<Route>;
30
+ }
31
+ export declare function createRemoteRouter(remotes: Record<string, string>, makeTransport: (url: string) => Transport): RemoteRouter;
32
+ type Facade = Record<string, (invocation?: InvocationContext) => Promise<unknown>>;
33
+ /**
34
+ * Façade-shaped stand-in — the consumer can't tell it from a local facade.
35
+ *
36
+ * Every trap answers the same question, and that is the point: the runner asks
37
+ * `Object.hasOwn(facade, op)` before calling, so a proxy that only trapped `get`
38
+ * reported *no* operations and every split call came back `Unknown operation` —
39
+ * `get` said yes, `hasOwn` said no, and the runner believed `hasOwn`.
40
+ *
41
+ * The stand-in claims every legal op name because it cannot know better: routing is
42
+ * lazy on purpose (the card is fetched at the first miss), and the remote is the
43
+ * authority on its own surface anyway. An op it does not serve comes back as its
44
+ * NOT_FOUND — judged where it is owned, which is the same answer a local façade gives.
45
+ */
46
+ export declare function createRemoteFacade(entity: string, router: RemoteRouter): Facade;
47
+ export {};
48
+ //# sourceMappingURL=remote.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.d.ts","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAA2B,SAAS,EAAE,MAAM,WAAW,CAAC;AAEpE,OAAO,EAAoB,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE3E,OAAO,EAAe,KAAK,iBAAiB,EAAE,KAAK,MAAM,EAAyB,MAAM,iBAAiB,CAAC;AAE1G,UAAU,KAAK;IACb,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,SAAS,CAAC;IACrB;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;CACvC;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GACxC,YAAY,CA+Fd;AAED,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;AAWnF;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,CAiB/E"}
package/dist/remote.js ADDED
@@ -0,0 +1,135 @@
1
+ import { RPC_ENTITY } from './call.js';
2
+ import { EMPTY_INVOCATION } from './invocation.js';
3
+ import { FougereError, ErrorCode } from './middleware.js';
4
+ import { reconstruct } from '@fougere/schema';
5
+ export function createRemoteRouter(remotes, makeTransport) {
6
+ const byEntity = new Map();
7
+ // The remotes config key is a label for the address — the identity card is
8
+ // what decides which entities live behind it.
9
+ const pending = new Map(Object.entries(remotes));
10
+ const transports = new Map();
11
+ /** Which remote label claimed a door name — so a second claim can name the first. */
12
+ const claimedBy = new Map();
13
+ const discover = async () => {
14
+ // Asking is concurrent; INDEXING is not, and is done in the order `remotes` declares.
15
+ // Reading the cards inside the race made "which remote won" depend on who answered
16
+ // first, so the same two remotes could resolve differently between two runs.
17
+ const cards = await Promise.all([...pending].map(async ([label, url]) => {
18
+ const transport = transports.get(url) ?? makeTransport(url);
19
+ transports.set(url, transport);
20
+ try {
21
+ const card = (await transport({ entity: RPC_ENTITY, op: 'discover' }, EMPTY_INVOCATION));
22
+ return { label, transport, card };
23
+ }
24
+ catch {
25
+ // Unreachable — stays pending, retried on the next miss.
26
+ return undefined;
27
+ }
28
+ }));
29
+ for (const answered of cards) {
30
+ if (!answered)
31
+ continue;
32
+ const { label, transport, card } = answered;
33
+ pending.delete(label);
34
+ for (const frond of card.fronds) {
35
+ // Doors only. A fact is not routable — nobody calls it, it arrives — so
36
+ // adding one here would answer a call with a transport to a door that
37
+ // does not exist.
38
+ for (const door of frond.doors) {
39
+ const held = claimedBy.get(door.name);
40
+ /**
41
+ * Two remotes claiming one name is refused, not silently arbitrated.
42
+ *
43
+ * `byEntity` is keyed by the door name alone, so the second claim used to be
44
+ * dropped with `if (!byEntity.has(...))` — no warning, and the winner was
45
+ * whichever remote answered first. The local boot already refuses the same
46
+ * collision (`assertOneOwnerPerKey`) and it refused the OTHER duplicate: in
47
+ * process the last frond loaded won, here the first discovered did. One
48
+ * application, two topologies, two different handlers answering — which is
49
+ * precisely the gradient not holding.
50
+ *
51
+ * Refusing is the honest answer while a call names an entity and not a frond.
52
+ * `FrondCall` already carries an optional `frond`; the day it is required, this
53
+ * becomes a disambiguation instead of a refusal.
54
+ */
55
+ if (held !== undefined && held !== label) {
56
+ throw new FougereError({
57
+ code: ErrorCode.INTERNAL_ERROR,
58
+ message: `Two remotes serve '${door.name}': '${held}' and '${label}'.\n`
59
+ + ` A call names an entity, not a frond, so nothing could choose between them.\n`
60
+ + ` - Keep one of the two out of \`remotes:\`, or\n`
61
+ + ` - expose one of them under a different entity name.`,
62
+ entity: door.name,
63
+ });
64
+ }
65
+ claimedBy.set(door.name, label);
66
+ byEntity.set(door.name, {
67
+ frond: frond.name,
68
+ transport,
69
+ ...(door.schema ? { schema: reconstruct(door.schema) } : {}),
70
+ });
71
+ }
72
+ }
73
+ }
74
+ };
75
+ return {
76
+ async route(entity) {
77
+ if (!byEntity.has(entity) && pending.size > 0)
78
+ await discover();
79
+ const hit = byEntity.get(entity);
80
+ if (hit)
81
+ return hit;
82
+ if (pending.size > 0) {
83
+ throw new FougereError({
84
+ code: ErrorCode.SERVICE_UNAVAILABLE,
85
+ message: `No reachable remote hosts '${entity}' — unreachable: ${[...pending.keys()].join(', ')}`,
86
+ entity,
87
+ });
88
+ }
89
+ throw new FougereError({
90
+ code: ErrorCode.NOT_FOUND,
91
+ message: `No declared remote hosts '${entity}'`,
92
+ entity,
93
+ });
94
+ },
95
+ };
96
+ }
97
+ /**
98
+ * Does this name designate an operation at all? `then` is excluded so the façade is
99
+ * never mistaken for a thenable, and `Object.prototype`'s own names so `constructor`
100
+ * or `toString` cannot be called across the wire.
101
+ */
102
+ function isOpName(prop) {
103
+ return typeof prop === 'string' && prop !== 'then' && !Object.hasOwn(Object.prototype, prop);
104
+ }
105
+ /**
106
+ * Façade-shaped stand-in — the consumer can't tell it from a local facade.
107
+ *
108
+ * Every trap answers the same question, and that is the point: the runner asks
109
+ * `Object.hasOwn(facade, op)` before calling, so a proxy that only trapped `get`
110
+ * reported *no* operations and every split call came back `Unknown operation` —
111
+ * `get` said yes, `hasOwn` said no, and the runner believed `hasOwn`.
112
+ *
113
+ * The stand-in claims every legal op name because it cannot know better: routing is
114
+ * lazy on purpose (the card is fetched at the first miss), and the remote is the
115
+ * authority on its own surface anyway. An op it does not serve comes back as its
116
+ * NOT_FOUND — judged where it is owned, which is the same answer a local façade gives.
117
+ */
118
+ export function createRemoteFacade(entity, router) {
119
+ const opFn = (op) => async (invocation = EMPTY_INVOCATION) => {
120
+ const { frond, transport } = await router.route(entity);
121
+ const call = { frond, entity, op };
122
+ return transport(call, invocation);
123
+ };
124
+ return new Proxy({}, {
125
+ get: (_target, prop) => (isOpName(prop) ? opFn(prop) : undefined),
126
+ has: (_target, prop) => isOpName(prop),
127
+ getOwnPropertyDescriptor: (_target, prop) =>
128
+ // `configurable: true` is required: a proxy may not report a non-configurable
129
+ // property that the target does not actually have.
130
+ isOpName(prop)
131
+ ? { value: opFn(prop), writable: false, enumerable: true, configurable: true }
132
+ : undefined,
133
+ });
134
+ }
135
+ //# sourceMappingURL=remote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote.js","sourceRoot":"","sources":["../src/remote.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { EntityOrm } from './orm.js';
2
+ /**
3
+ * Repository(Entity) — where an entity's reads and writes are named.
4
+ *
5
+ * `EntityOrm` is a port: five generic gestures, no flavour of domain. "The readings
6
+ * of the last hour" is not one of them, so it used to be spelled at the call site —
7
+ * `orm.list({ orderBy: 'at', order: 'desc', limit: 4096 })` followed by a filter in
8
+ * JavaScript, sitting in the middle of the calculation it feeds. A query with no home
9
+ * squats in the answer.
10
+ *
11
+ * So the port gains a place to be extended, per entity:
12
+ *
13
+ * ```ts
14
+ * // repositories/ReadingRepository.ts
15
+ * export default class ReadingRepository extends Repository(Reading) {
16
+ * since(moment: Date) {
17
+ * return this.orm.list({ where: { at: { gte: moment } } });
18
+ * }
19
+ * }
20
+ * ```
21
+ *
22
+ * A handler then asks the question and never spells the storage:
23
+ *
24
+ * ```ts
25
+ * async hourly() {
26
+ * return average(await this.readings.since(anHourAgo()));
27
+ * }
28
+ * ```
29
+ *
30
+ * **Write none and you lose nothing.** The bootstrap registers a default repository
31
+ * for every entity — the guarded ORM itself — so `ReadingRepository` resolves whether
32
+ * or not the file exists. Declaring one wins, exactly as a `Crud` op redefined in the
33
+ * subclass wins over the prefab. Convention, not configuration.
34
+ *
35
+ * It is NOT a door: a repository has no façade, so nothing here is reachable from the
36
+ * wire. A judge still lives in the handler, which is the only place a refusal cannot
37
+ * be walked around.
38
+ */
39
+ type EntityClass = abstract new (...args: any[]) => any;
40
+ /** What a repository is handed, and what it exposes to whoever holds it. */
41
+ export interface RepositoryOf<T> {
42
+ /** The entity's port, already guarded — a value the shape forbids is refused here too. */
43
+ orm: EntityOrm<T>;
44
+ }
45
+ export interface RepositoryConstructor<T> {
46
+ new (orm: EntityOrm<T>): RepositoryOf<T>;
47
+ readonly __entity: unknown;
48
+ }
49
+ export declare function Repository<E extends EntityClass>(entity: E): RepositoryConstructor<InstanceType<E>>;
50
+ /** Get the entity class a repository targets. */
51
+ export declare function getRepositoryTarget(ctor: Function): EntityClass | undefined;
52
+ /** Container key of an entity's repository — 'reading' → 'ReadingRepository'. */
53
+ export declare function repositoryKeyOf(entity: string): string;
54
+ export {};
55
+ //# sourceMappingURL=repository.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,KAAK,WAAW,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;AAIxD,4EAA4E;AAC5E,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,0FAA0F;IAC1F,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;CACnB;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC;IACtC,KAAK,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,EAAE,MAAM,EAAE,CAAC,GAAG,qBAAqB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAWnG;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAO3E;AAED,iFAAiF;AACjF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD"}
@@ -0,0 +1,26 @@
1
+ const REPOSITORY_TARGET = Symbol.for('fougere:repository_target');
2
+ export function Repository(entity) {
3
+ class RepositoryBase {
4
+ orm;
5
+ static [REPOSITORY_TARGET] = entity;
6
+ static __entity = entity;
7
+ constructor(orm) {
8
+ this.orm = orm;
9
+ }
10
+ }
11
+ return RepositoryBase;
12
+ }
13
+ /** Get the entity class a repository targets. */
14
+ export function getRepositoryTarget(ctor) {
15
+ for (let cur = ctor; cur; cur = Object.getPrototypeOf(cur)) {
16
+ const target = cur[REPOSITORY_TARGET];
17
+ if (target)
18
+ return target;
19
+ }
20
+ return undefined;
21
+ }
22
+ /** Container key of an entity's repository — 'reading' → 'ReadingRepository'. */
23
+ export function repositoryKeyOf(entity) {
24
+ return `${entity[0].toUpperCase()}${entity.slice(1)}Repository`;
25
+ }
26
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AA0CA,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;AAalE,MAAM,UAAU,UAAU,CAAwB,MAAS;IAGzD,MAAM,cAAc;QAIC;QAHnB,MAAM,CAAC,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC;QACpC,MAAM,CAAU,QAAQ,GAAG,MAAM,CAAC;QAElC,YAAmB,GAAiB;YAAjB,QAAG,GAAH,GAAG,CAAc;QAAG,CAAC;;IAG1C,OAAO,cAAqD,CAAC;AAC/D,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,mBAAmB,CAAC,IAAc;IAChD,KAAK,IAAI,GAAG,GAAQ,IAAI,EAAE,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;QAChE,MAAM,MAAM,GAAG,GAAG,CAAC,iBAAiB,CAAC,CAAC;QACtC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;IAC5B,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC;AAClE,CAAC"}
@@ -0,0 +1,10 @@
1
+ export declare function setCacheRoot(dir: string): void;
2
+ export declare function hashFile(filePath: string): string;
3
+ /** Get cached parse result. Returns null if cache miss. */
4
+ export declare function getCached<T>(key: string, hash: string): T | null;
5
+ /** Store parse result in cache. */
6
+ export declare function setCached(key: string, hash: string, data: unknown): void;
7
+ export { getCached as getCachedMethods, setCached as setCachedMethods };
8
+ /** Write cache to disk if modified. */
9
+ export declare function flushCache(): void;
10
+ //# sourceMappingURL=scan-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-cache.d.ts","sourceRoot":"","sources":["../src/scan-cache.ts"],"names":[],"mappings":"AAoDA,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAG9C;AAsBD,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAKhE;AAED,mCAAmC;AACnC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAIxE;AAGD,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,SAAS,IAAI,gBAAgB,EAAE,CAAC;AAExE,uCAAuC;AACvC,wBAAgB,UAAU,IAAI,IAAI,CAOjC"}