@beignet/core 0.0.2 → 0.0.3

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 (56) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +55 -6
  3. package/dist/jobs/index.d.ts +138 -4
  4. package/dist/jobs/index.d.ts.map +1 -1
  5. package/dist/jobs/index.js +161 -1
  6. package/dist/jobs/index.js.map +1 -1
  7. package/dist/outbox/index.d.ts +5 -0
  8. package/dist/outbox/index.d.ts.map +1 -1
  9. package/dist/outbox/index.js +59 -3
  10. package/dist/outbox/index.js.map +1 -1
  11. package/dist/providers/instrumentation.d.ts +1 -1
  12. package/dist/providers/instrumentation.d.ts.map +1 -1
  13. package/dist/providers/instrumentation.js.map +1 -1
  14. package/dist/server/hooks/auth.d.ts +50 -65
  15. package/dist/server/hooks/auth.d.ts.map +1 -1
  16. package/dist/server/hooks/auth.js +44 -55
  17. package/dist/server/hooks/auth.js.map +1 -1
  18. package/dist/server/hooks/index.d.ts +1 -1
  19. package/dist/server/hooks/index.d.ts.map +1 -1
  20. package/dist/server/hooks/index.js.map +1 -1
  21. package/dist/server/http.d.ts +52 -0
  22. package/dist/server/http.d.ts.map +1 -1
  23. package/dist/server/http.js +20 -1
  24. package/dist/server/http.js.map +1 -1
  25. package/dist/server/index.d.ts +1 -1
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +1 -1
  28. package/dist/server/index.js.map +1 -1
  29. package/dist/server/server.d.ts +54 -13
  30. package/dist/server/server.d.ts.map +1 -1
  31. package/dist/server/server.js +56 -35
  32. package/dist/server/server.js.map +1 -1
  33. package/dist/testing/index.d.ts +4 -0
  34. package/dist/testing/index.d.ts.map +1 -1
  35. package/dist/testing/index.js +8 -0
  36. package/dist/testing/index.js.map +1 -1
  37. package/dist/uploads/client.d.ts +278 -0
  38. package/dist/uploads/client.d.ts.map +1 -0
  39. package/dist/uploads/client.js +428 -0
  40. package/dist/uploads/client.js.map +1 -0
  41. package/dist/uploads/index.d.ts +361 -0
  42. package/dist/uploads/index.d.ts.map +1 -0
  43. package/dist/uploads/index.js +543 -0
  44. package/dist/uploads/index.js.map +1 -0
  45. package/package.json +11 -2
  46. package/src/jobs/index.ts +326 -5
  47. package/src/outbox/index.ts +83 -3
  48. package/src/providers/instrumentation.ts +7 -1
  49. package/src/server/hooks/auth.ts +89 -162
  50. package/src/server/hooks/index.ts +1 -5
  51. package/src/server/http.ts +79 -0
  52. package/src/server/index.ts +1 -0
  53. package/src/server/server.ts +191 -23
  54. package/src/testing/index.ts +11 -0
  55. package/src/uploads/client.ts +861 -0
  56. package/src/uploads/index.ts +1067 -0
@@ -31,6 +31,7 @@ import type {
31
31
  HttpResponse,
32
32
  HttpResponseLike,
33
33
  ResolvedRoute,
34
+ RouteHook,
34
35
  ServerCaughtErrorHook,
35
36
  ServerHook,
36
37
  ServerUnhandledErrorMapper,
@@ -48,29 +49,65 @@ import {
48
49
  * apps keep these in `features/<feature>/routes.ts` and compose them with
49
50
  * `defineRoutes(...)`.
50
51
  */
51
- export type RouteDef<Ctx, CLike extends ContractLike = ContractLike> = {
52
+ export type RouteDef<
53
+ Ctx,
54
+ CLike extends ContractLike = ContractLike,
55
+ Hooks extends readonly RouteHook<Ctx, object>[] = readonly RouteHook<
56
+ Ctx,
57
+ object
58
+ >[],
59
+ > = {
52
60
  /**
53
61
  * Contract builder or plain contract config for this route.
54
62
  */
55
63
  contract: CLike;
64
+ /**
65
+ * Route-scoped hooks that run after group hooks and before the handler.
66
+ */
67
+ hooks?: Hooks;
56
68
  /**
57
69
  * Handler that implements the contract.
58
70
  */
59
- handle: Handler<Ctx, ResolveContract<CLike>>;
71
+ handle: Handler<Ctx & AddedCtxFromHooks<Hooks>, ResolveContract<CLike>>;
60
72
  };
61
73
 
74
+ type AddedCtxFromHook<Hook> =
75
+ Hook extends RouteHook<infer _Ctx, infer AddedCtx> ? AddedCtx : unknown;
76
+
77
+ type UnionToIntersection<Union> = (
78
+ Union extends unknown
79
+ ? (value: Union) => void
80
+ : never
81
+ ) extends (value: infer Intersection) => void
82
+ ? Intersection
83
+ : never;
84
+
85
+ type AddedCtxFromHooks<Hooks extends readonly unknown[]> =
86
+ Hooks extends readonly []
87
+ ? unknown
88
+ : UnionToIntersection<AddedCtxFromHook<Hooks[number]>>;
89
+
90
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
91
+ type AnyRouteDef = RouteDef<any, any>;
92
+
93
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
94
+ type PlainRouteDef<Ctx> = RouteDef<Ctx, any, readonly []>;
95
+
96
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at collection boundaries
97
+ type AnyContractRouteDef<Ctx> = RouteDef<Ctx, any>;
98
+
62
99
  const ROUTE_GROUP_KIND = "beignet.route-group";
63
100
 
64
101
  /**
65
102
  * Named collection of related route registrations.
66
103
  *
67
- * Route groups are an organization helper only. `defineRoutes(...)` flattens
68
- * them before server registration.
104
+ * Route groups colocate feature routes and can apply scoped route hooks to
105
+ * every route in the group. `defineRoutes(...)` flattens them before server
106
+ * registration while preserving those hooks.
69
107
  */
70
108
  export type RouteGroup<
71
109
  Ctx,
72
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
73
- Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
110
+ Routes extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
74
111
  > = {
75
112
  /**
76
113
  * Internal marker used by `defineRoutes(...)`.
@@ -80,6 +117,10 @@ export type RouteGroup<
80
117
  * Human-readable group name.
81
118
  */
82
119
  name: string;
120
+ /**
121
+ * Hooks applied to every route in this group.
122
+ */
123
+ hooks?: readonly RouteHook<Ctx, object>[];
83
124
  /**
84
125
  * Route definitions in this group.
85
126
  */
@@ -87,24 +128,45 @@ export type RouteGroup<
87
128
  };
88
129
 
89
130
  type RouteInput<Ctx> =
90
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
91
- | RouteDef<Ctx, any>
92
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
93
- | RouteGroup<Ctx, readonly RouteDef<Ctx, any>[]>;
131
+ | AnyContractRouteDef<Ctx>
132
+ | AnyRouteDef
133
+ | RouteGroup<Ctx, readonly AnyRouteDef[]>;
134
+
135
+ type ContextualRouteInput<Ctx> =
136
+ | PlainRouteDef<Ctx>
137
+ | RouteGroup<Ctx, readonly AnyRouteDef[]>;
138
+
139
+ type RouteGroupBuilder<Ctx> = {
140
+ <
141
+ const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
142
+ const R extends readonly PlainRouteDef<
143
+ Ctx & AddedCtxFromHooks<GroupHooks>
144
+ >[] = readonly PlainRouteDef<Ctx & AddedCtxFromHooks<GroupHooks>>[],
145
+ >(group: {
146
+ name: string;
147
+ hooks?: GroupHooks;
148
+ routes: R;
149
+ }): RouteGroup<Ctx, R>;
150
+ <
151
+ const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
152
+ const R extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
153
+ >(group: {
154
+ name: string;
155
+ hooks?: GroupHooks;
156
+ routes: R;
157
+ }): RouteGroup<Ctx, R>;
158
+ };
94
159
 
95
160
  type RoutesFromInput<Input> =
96
- // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
97
- Input extends RouteGroup<any, infer Routes>
161
+ Input extends RouteGroup<infer _Ctx, infer Routes>
98
162
  ? Routes
99
- : // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
100
- Input extends RouteDef<any, any>
163
+ : Input extends AnyRouteDef
101
164
  ? readonly [Input]
102
165
  : readonly [];
103
166
 
104
167
  type FlattenRouteInputs<Inputs extends readonly unknown[]> =
105
168
  number extends Inputs["length"]
106
- ? // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
107
- readonly RouteDef<any, any>[]
169
+ ? readonly AnyRouteDef[]
108
170
  : Inputs extends readonly [infer First, ...infer Rest]
109
171
  ? readonly [...RoutesFromInput<First>, ...FlattenRouteInputs<Rest>]
110
172
  : readonly [];
@@ -122,6 +184,22 @@ type ContractsFromRouteList<
122
184
  : never;
123
185
  };
124
186
 
187
+ /**
188
+ * Define one route registration with hook-aware handler typing.
189
+ *
190
+ * Direct route objects are still supported. Use this helper when route-scoped
191
+ * hooks enrich `ctx` for a single handler and you want TypeScript to infer the
192
+ * added fields.
193
+ */
194
+ export function defineRoute<Ctx>() {
195
+ return <
196
+ CLike extends ContractLike,
197
+ const Hooks extends readonly RouteHook<Ctx, object>[] = readonly [],
198
+ >(
199
+ route: RouteDef<Ctx, CLike, Hooks>,
200
+ ): RouteDef<Ctx, CLike, Hooks> => route;
201
+ }
202
+
125
203
  /**
126
204
  * Options for creating a Beignet server instance.
127
205
  */
@@ -129,7 +207,7 @@ export type CreateServerOptions<
129
207
  Ctx,
130
208
  Ports extends AnyPorts,
131
209
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
132
- Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
210
+ Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
133
211
  Providers extends readonly ServiceProvider<
134
212
  unknown,
135
213
  // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
@@ -637,6 +715,7 @@ function buildHandler<
637
715
  contract: C,
638
716
  userHandler: Handler<Ctx, C>,
639
717
  hooks: ServerHook<Ctx, FinalPorts>[],
718
+ routeHooks: readonly RouteHook<unknown, object>[] = [],
640
719
  optionsOverrides?: {
641
720
  skipRoutePreparation?: boolean;
642
721
  },
@@ -1208,6 +1287,39 @@ function buildHandler<
1208
1287
  }
1209
1288
  }
1210
1289
 
1290
+ if (!result) {
1291
+ for (const hook of routeHooks) {
1292
+ try {
1293
+ const additions = await hook.resolve({
1294
+ req,
1295
+ ctx: currentCtx,
1296
+ contract,
1297
+ path,
1298
+ query,
1299
+ headers,
1300
+ body,
1301
+ });
1302
+ if (additions && typeof additions === "object") {
1303
+ currentCtx = {
1304
+ ...(currentCtx as object),
1305
+ ...additions,
1306
+ } as Ctx;
1307
+ }
1308
+ } catch (error) {
1309
+ result = await resolveErrorResult(
1310
+ error,
1311
+ currentCtx,
1312
+ pathValue,
1313
+ queryValue,
1314
+ headersValue,
1315
+ bodyValue,
1316
+ { owner: "framework" },
1317
+ );
1318
+ break;
1319
+ }
1320
+ }
1321
+ }
1322
+
1211
1323
  if (!result) {
1212
1324
  try {
1213
1325
  result = {
@@ -1328,7 +1440,7 @@ export async function createServer<
1328
1440
  Ctx,
1329
1441
  Ports extends AnyPorts,
1330
1442
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1331
- Routes extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
1443
+ Routes extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
1332
1444
  Providers extends readonly ServiceProvider<
1333
1445
  unknown,
1334
1446
  // biome-ignore lint/suspicious/noExplicitAny: provider config types are erased at this level
@@ -1382,6 +1494,7 @@ export async function createServer<
1382
1494
  const registerRoute = <C extends HttpContractConfig>(
1383
1495
  contract: C,
1384
1496
  handler: Handler<Ctx, C>,
1497
+ routeHooks: readonly RouteHook<unknown, object>[] = [],
1385
1498
  ): void => {
1386
1499
  if (contract.body && !methodSupportsRequestBody(contract.method)) {
1387
1500
  throw new Error(
@@ -1412,6 +1525,7 @@ export async function createServer<
1412
1525
  contract,
1413
1526
  handler,
1414
1527
  hooks,
1528
+ routeHooks,
1415
1529
  );
1416
1530
  registry.push({
1417
1531
  contract,
@@ -1444,7 +1558,11 @@ export async function createServer<
1444
1558
  try {
1445
1559
  for (const route of options.routes) {
1446
1560
  const contract = resolveContract(route.contract);
1447
- registerRoute(contract, route.handle);
1561
+ registerRoute(
1562
+ contract,
1563
+ route.handle as Handler<Ctx, typeof contract>,
1564
+ route.hooks as readonly RouteHook<unknown, object>[] | undefined,
1565
+ );
1448
1566
  }
1449
1567
  } catch (error) {
1450
1568
  try {
@@ -1518,6 +1636,7 @@ export async function createServer<
1518
1636
  notFoundContract,
1519
1637
  async () => errorResponse(404, "NOT_FOUND", "Not found"),
1520
1638
  hooks,
1639
+ [],
1521
1640
  { skipRoutePreparation: true },
1522
1641
  );
1523
1642
 
@@ -1549,6 +1668,15 @@ export async function createServer<
1549
1668
  * ]);
1550
1669
  * ```
1551
1670
  */
1671
+ export function defineRoutes<
1672
+ Ctx,
1673
+ const R extends
1674
+ readonly ContextualRouteInput<Ctx>[] = readonly ContextualRouteInput<Ctx>[],
1675
+ >(routes: R): FlattenRouteInputs<R>;
1676
+ export function defineRoutes<
1677
+ Ctx,
1678
+ const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
1679
+ >(routes: R): FlattenRouteInputs<R>;
1552
1680
  export function defineRoutes<
1553
1681
  Ctx,
1554
1682
  const R extends readonly RouteInput<Ctx>[] = readonly RouteInput<Ctx>[],
@@ -1557,7 +1685,12 @@ export function defineRoutes<
1557
1685
 
1558
1686
  for (const route of routes) {
1559
1687
  if (isRouteGroup(route)) {
1560
- flattened.push(...route.routes);
1688
+ for (const groupRoute of route.routes) {
1689
+ flattened.push({
1690
+ ...groupRoute,
1691
+ hooks: [...(route.hooks ?? []), ...(groupRoute.hooks ?? [])],
1692
+ });
1693
+ }
1561
1694
  } else {
1562
1695
  flattened.push(route);
1563
1696
  }
@@ -1585,26 +1718,61 @@ export function contractsFromRoutes<
1585
1718
  * Define a named group of related route registrations.
1586
1719
  *
1587
1720
  * Route groups are flattened by defineRoutes, so createServer still receives
1588
- * a regular route list while app code can keep feature route wiring colocated.
1721
+ * a regular route list while app code can keep feature route wiring and scoped
1722
+ * hooks colocated.
1589
1723
  *
1590
1724
  * @example
1591
1725
  * ```ts
1592
- * const todoRoutes = defineRouteGroup<AppContext>({
1726
+ * const todoRoutes = defineRouteGroup<AppContext>()({
1593
1727
  * name: "todos",
1728
+ * hooks: [auth.optional()],
1594
1729
  * routes: [
1595
1730
  * { contract: listTodos, handle: async ({ ctx }) => ctx.todos.list() },
1596
1731
  * ]
1597
1732
  * });
1598
1733
  * ```
1599
1734
  */
1735
+ export function defineRouteGroup<Ctx>(): RouteGroupBuilder<Ctx>;
1600
1736
  export function defineRouteGroup<
1601
1737
  Ctx,
1602
1738
  // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1603
1739
  const R extends readonly RouteDef<Ctx, any>[] = readonly RouteDef<Ctx, any>[],
1604
- >(group: { name: string; routes: R }): RouteGroup<Ctx, R> {
1740
+ >(group: {
1741
+ name: string;
1742
+ hooks?: readonly RouteHook<Ctx, object>[];
1743
+ routes: R;
1744
+ }): RouteGroup<Ctx, R>;
1745
+ export function defineRouteGroup<
1746
+ Ctx,
1747
+ // biome-ignore lint/suspicious/noExplicitAny: route contract types are erased at this level
1748
+ const R extends readonly RouteDef<any, any>[] = readonly RouteDef<any, any>[],
1749
+ >(group?: {
1750
+ name: string;
1751
+ hooks?: readonly RouteHook<Ctx, object>[];
1752
+ routes: R;
1753
+ }): RouteGroup<Ctx, R> | RouteGroupBuilder<Ctx> {
1754
+ const createGroup = <
1755
+ const GroupHooks extends readonly RouteHook<Ctx, object>[] = readonly [],
1756
+ const GroupRoutes extends readonly AnyRouteDef[] = readonly AnyRouteDef[],
1757
+ >(input: {
1758
+ name: string;
1759
+ hooks?: GroupHooks;
1760
+ routes: GroupRoutes;
1761
+ }): RouteGroup<Ctx, GroupRoutes> => ({
1762
+ kind: ROUTE_GROUP_KIND,
1763
+ name: input.name,
1764
+ hooks: input.hooks,
1765
+ routes: input.routes,
1766
+ });
1767
+
1768
+ if (!group) {
1769
+ return createGroup;
1770
+ }
1771
+
1605
1772
  return {
1606
1773
  kind: ROUTE_GROUP_KIND,
1607
1774
  name: group.name,
1775
+ hooks: group.hooks,
1608
1776
  routes: group.routes,
1609
1777
  };
1610
1778
  }
@@ -196,6 +196,17 @@ export class SeedRunError extends Error {
196
196
  }
197
197
  }
198
198
 
199
+ /**
200
+ * Reset one or more factory sequences to their configured starting values.
201
+ */
202
+ export function resetFactories(
203
+ ...factories: readonly Pick<FactoryDef, "resetSequence">[]
204
+ ): void {
205
+ for (const factory of factories) {
206
+ factory.resetSequence();
207
+ }
208
+ }
209
+
199
210
  function errorMessage(error: unknown): string {
200
211
  return error instanceof Error ? error.message : String(error);
201
212
  }