@edium/halifax 1.0.0 → 2.0.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 (150) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +72 -50
  3. package/README_AUTOCRUD.md +61 -19
  4. package/README_QUERYBUILDER.md +1 -1
  5. package/README_REPO_ADAPTERS.md +80 -11
  6. package/dist/adapters/http/ExpressAdapter.d.ts +34 -5
  7. package/dist/adapters/http/ExpressAdapter.js +20 -12
  8. package/dist/adapters/http/FastifyAdapter.d.ts +93 -0
  9. package/dist/adapters/http/FastifyAdapter.js +125 -0
  10. package/dist/adapters/http/HyperExpressAdapter.d.ts +82 -0
  11. package/dist/adapters/http/HyperExpressAdapter.js +128 -0
  12. package/dist/adapters/http/UltimateExpressAdapter.d.ts +84 -0
  13. package/dist/adapters/http/UltimateExpressAdapter.js +108 -0
  14. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +89 -40
  15. package/dist/adapters/orm/prisma/PrismaAdapter.js +233 -71
  16. package/dist/adapters/orm/prisma/astToPrisma.d.ts +26 -0
  17. package/dist/adapters/orm/prisma/astToPrisma.js +140 -0
  18. package/dist/adapters/orm/prisma/createPrismaResources.d.ts +1 -2
  19. package/dist/adapters/orm/prisma/createPrismaResources.js +10 -6
  20. package/dist/adapters/orm/prisma/helpers.d.ts +0 -1
  21. package/dist/adapters/orm/prisma/helpers.js +0 -1
  22. package/dist/adapters/orm/prisma/index.d.ts +1 -2
  23. package/dist/adapters/orm/prisma/index.js +0 -1
  24. package/dist/adapters/orm/prisma/types.d.ts +14 -9
  25. package/dist/adapters/orm/prisma/types.js +0 -1
  26. package/dist/auth/AuthStrategy.d.ts +0 -9
  27. package/dist/auth/AuthStrategy.js +0 -7
  28. package/dist/core/cache/CacheStore.d.ts +25 -0
  29. package/dist/core/cache/CacheStore.js +1 -0
  30. package/dist/core/cache/createCachingRepository.d.ts +39 -0
  31. package/dist/core/cache/createCachingRepository.js +116 -0
  32. package/dist/core/cache/in-memory/InMemoryCacheStore.d.ts +19 -0
  33. package/dist/core/cache/in-memory/InMemoryCacheStore.js +34 -0
  34. package/dist/core/cache/index.d.ts +5 -0
  35. package/dist/core/cache/index.js +5 -0
  36. package/dist/core/cache/redis/RedisCacheStore.d.ts +28 -0
  37. package/dist/core/cache/redis/RedisCacheStore.js +42 -0
  38. package/dist/core/cache/redis/RedisLikeClient.d.ts +12 -0
  39. package/dist/core/cache/redis/RedisLikeClient.js +1 -0
  40. package/dist/core/crudRouter.d.ts +65 -8
  41. package/dist/core/crudRouter.js +231 -95
  42. package/dist/core/queryString.d.ts +3 -3
  43. package/dist/core/queryString.js +16 -7
  44. package/dist/core/types.d.ts +141 -31
  45. package/dist/core/types.js +13 -1
  46. package/dist/core/validation.d.ts +12 -4
  47. package/dist/core/validation.js +33 -13
  48. package/dist/enums/SqlComparison.d.ts +13 -3
  49. package/dist/enums/SqlComparison.js +12 -2
  50. package/dist/enums/SqlOperator.d.ts +0 -1
  51. package/dist/enums/SqlOperator.js +0 -1
  52. package/dist/enums/SqlOrder.d.ts +0 -1
  53. package/dist/enums/SqlOrder.js +0 -1
  54. package/dist/errors/AuthenticationError.d.ts +0 -1
  55. package/dist/errors/AuthenticationError.js +0 -1
  56. package/dist/errors/AuthorizationError.d.ts +0 -1
  57. package/dist/errors/AuthorizationError.js +0 -1
  58. package/dist/errors/BadRequestError.d.ts +0 -1
  59. package/dist/errors/BadRequestError.js +0 -1
  60. package/dist/errors/HttpError.d.ts +0 -1
  61. package/dist/errors/HttpError.js +0 -1
  62. package/dist/errors/MethodNotAllowedError.d.ts +0 -1
  63. package/dist/errors/MethodNotAllowedError.js +0 -1
  64. package/dist/errors/NotAcceptableError.d.ts +0 -1
  65. package/dist/errors/NotAcceptableError.js +0 -1
  66. package/dist/errors/NotFoundError.d.ts +0 -1
  67. package/dist/errors/NotFoundError.js +0 -1
  68. package/dist/errors/NotImplementedError.d.ts +0 -1
  69. package/dist/errors/NotImplementedError.js +0 -1
  70. package/dist/errors/ServerError.d.ts +0 -1
  71. package/dist/errors/ServerError.js +0 -1
  72. package/dist/errors/UnprocessableEntityError.d.ts +0 -1
  73. package/dist/errors/UnprocessableEntityError.js +0 -1
  74. package/dist/errors/UnsupportedMediaTypeError.d.ts +0 -1
  75. package/dist/errors/UnsupportedMediaTypeError.js +0 -1
  76. package/dist/index.d.ts +1 -3
  77. package/dist/index.js +1 -3
  78. package/dist/interfaces/IQueryFilter.d.ts +1 -2
  79. package/dist/interfaces/IQueryFilter.js +0 -1
  80. package/dist/interfaces/IQueryOptions.d.ts +9 -9
  81. package/dist/interfaces/IQueryOptions.js +0 -1
  82. package/dist/interfaces/ISort.d.ts +0 -1
  83. package/dist/interfaces/ISort.js +0 -1
  84. package/package.json +10 -8
  85. package/dist/adapters/http/ExpressAdapter.d.ts.map +0 -1
  86. package/dist/adapters/http/ExpressAdapter.js.map +0 -1
  87. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts.map +0 -1
  88. package/dist/adapters/orm/prisma/PrismaAdapter.js.map +0 -1
  89. package/dist/adapters/orm/prisma/createPrismaResources.d.ts.map +0 -1
  90. package/dist/adapters/orm/prisma/createPrismaResources.js.map +0 -1
  91. package/dist/adapters/orm/prisma/helpers.d.ts.map +0 -1
  92. package/dist/adapters/orm/prisma/helpers.js.map +0 -1
  93. package/dist/adapters/orm/prisma/index.d.ts.map +0 -1
  94. package/dist/adapters/orm/prisma/index.js.map +0 -1
  95. package/dist/adapters/orm/prisma/types.d.ts.map +0 -1
  96. package/dist/adapters/orm/prisma/types.js.map +0 -1
  97. package/dist/auth/AuthStrategy.d.ts.map +0 -1
  98. package/dist/auth/AuthStrategy.js.map +0 -1
  99. package/dist/classes/QueryBuilder.d.ts +0 -33
  100. package/dist/classes/QueryBuilder.d.ts.map +0 -1
  101. package/dist/classes/QueryBuilder.js +0 -262
  102. package/dist/classes/QueryBuilder.js.map +0 -1
  103. package/dist/core/crudRouter.d.ts.map +0 -1
  104. package/dist/core/crudRouter.js.map +0 -1
  105. package/dist/core/queryString.d.ts.map +0 -1
  106. package/dist/core/queryString.js.map +0 -1
  107. package/dist/core/types.d.ts.map +0 -1
  108. package/dist/core/types.js.map +0 -1
  109. package/dist/core/validation.d.ts.map +0 -1
  110. package/dist/core/validation.js.map +0 -1
  111. package/dist/enums/SqlComparison.d.ts.map +0 -1
  112. package/dist/enums/SqlComparison.js.map +0 -1
  113. package/dist/enums/SqlOperator.d.ts.map +0 -1
  114. package/dist/enums/SqlOperator.js.map +0 -1
  115. package/dist/enums/SqlOrder.d.ts.map +0 -1
  116. package/dist/enums/SqlOrder.js.map +0 -1
  117. package/dist/errors/AuthenticationError.d.ts.map +0 -1
  118. package/dist/errors/AuthenticationError.js.map +0 -1
  119. package/dist/errors/AuthorizationError.d.ts.map +0 -1
  120. package/dist/errors/AuthorizationError.js.map +0 -1
  121. package/dist/errors/BadRequestError.d.ts.map +0 -1
  122. package/dist/errors/BadRequestError.js.map +0 -1
  123. package/dist/errors/HttpError.d.ts.map +0 -1
  124. package/dist/errors/HttpError.js.map +0 -1
  125. package/dist/errors/MethodNotAllowedError.d.ts.map +0 -1
  126. package/dist/errors/MethodNotAllowedError.js.map +0 -1
  127. package/dist/errors/NotAcceptableError.d.ts.map +0 -1
  128. package/dist/errors/NotAcceptableError.js.map +0 -1
  129. package/dist/errors/NotFoundError.d.ts.map +0 -1
  130. package/dist/errors/NotFoundError.js.map +0 -1
  131. package/dist/errors/NotImplementedError.d.ts.map +0 -1
  132. package/dist/errors/NotImplementedError.js.map +0 -1
  133. package/dist/errors/ServerError.d.ts.map +0 -1
  134. package/dist/errors/ServerError.js.map +0 -1
  135. package/dist/errors/UnprocessableEntityError.d.ts.map +0 -1
  136. package/dist/errors/UnprocessableEntityError.js.map +0 -1
  137. package/dist/errors/UnsupportedMediaTypeError.d.ts.map +0 -1
  138. package/dist/errors/UnsupportedMediaTypeError.js.map +0 -1
  139. package/dist/index.d.ts.map +0 -1
  140. package/dist/index.js.map +0 -1
  141. package/dist/interfaces/IParamQuery.d.ts +0 -8
  142. package/dist/interfaces/IParamQuery.d.ts.map +0 -1
  143. package/dist/interfaces/IParamQuery.js +0 -2
  144. package/dist/interfaces/IParamQuery.js.map +0 -1
  145. package/dist/interfaces/IQueryFilter.d.ts.map +0 -1
  146. package/dist/interfaces/IQueryFilter.js.map +0 -1
  147. package/dist/interfaces/IQueryOptions.d.ts.map +0 -1
  148. package/dist/interfaces/IQueryOptions.js.map +0 -1
  149. package/dist/interfaces/ISort.d.ts.map +0 -1
  150. package/dist/interfaces/ISort.js.map +0 -1
@@ -1,5 +1,13 @@
1
1
  import { Router } from 'express';
2
2
  import { registerCrudApi } from '../../core/crudRouter.js';
3
+ /** Maps a Halifax {@link HttpMethod} to the corresponding Express registration method name. */
4
+ const METHOD_TO_REGISTRAR = {
5
+ GET: 'get',
6
+ POST: 'post',
7
+ PUT: 'put',
8
+ PATCH: 'patch',
9
+ DELETE: 'delete'
10
+ };
3
11
  /**
4
12
  * Casts Express's header map to Halifax's `Record<string, string | string[] | undefined>` type.
5
13
  * Express guarantees header names are lowercase and values are strings or string arrays,
@@ -48,11 +56,18 @@ function adaptResponse(res) {
48
56
  }
49
57
  };
50
58
  }
51
- /** Adapts an Express `App` or `Router` to Halifax's {@link HttpServer} interface. */
59
+ /**
60
+ * Adapts an Express `App` or `Router` to Halifax's {@link HttpServer} interface.
61
+ *
62
+ * Works with both Express 4 and Express 5 — the adapter only uses the route methods,
63
+ * `listen`, and the request/response surface that are identical across both majors, and
64
+ * every route path it registers uses plain segments and `:id` named params (never `*`
65
+ * path wildcards), so it sidesteps the Express 5 `path-to-regexp` routing-syntax change.
66
+ */
52
67
  export class ExpressHttpServer {
53
68
  app;
54
69
  /**
55
- * @param app - Express application or router to register routes on.
70
+ * @param app - Express application or router to register routes on (Express 4 or 5).
56
71
  * When an `App` is provided, `start()` will call `listen()`.
57
72
  * When a `Router` is provided, `start()` is a no-op.
58
73
  */
@@ -69,13 +84,8 @@ export class ExpressHttpServer {
69
84
  const cb = (req, res) => {
70
85
  void Promise.resolve(handler(adaptRequest(req), adaptResponse(res)));
71
86
  };
72
- if (method === '*') {
73
- ;
74
- this.app.all(path, cb);
75
- return;
76
- }
77
- ;
78
- this.app[method.toLowerCase()](path, cb);
87
+ const register = method === '*' ? this.app.all : this.app[METHOD_TO_REGISTRAR[method]];
88
+ register.call(this.app, path, cb);
79
89
  }
80
90
  /**
81
91
  * Starts the Express server. No-op when the underlying app does not expose a `listen` method
@@ -85,8 +95,7 @@ export class ExpressHttpServer {
85
95
  */
86
96
  async start(port, host) {
87
97
  await new Promise((resolve) => {
88
- if ('listen' in this.app && typeof this.app.listen === 'function') {
89
- ;
98
+ if (typeof this.app.listen === 'function') {
90
99
  this.app.listen(port, host, () => resolve());
91
100
  return;
92
101
  }
@@ -106,4 +115,3 @@ export function createExpressCrudRouter(resources, options = {}) {
106
115
  registerCrudApi(new ExpressHttpServer(router), resources, options);
107
116
  return router;
108
117
  }
109
- //# sourceMappingURL=ExpressAdapter.js.map
@@ -0,0 +1,93 @@
1
+ import type { HttpMethod, HttpRouteHandler, HttpServer, ResourceDefinition } from '../../core/types.js';
2
+ import { type CrudApiOptions } from '../../core/crudRouter.js';
3
+ /** The minimal slice of a Fastify request Halifax reads. Fastify pre-parses the JSON body. */
4
+ interface FastifyRequestLike {
5
+ method: string;
6
+ params: unknown;
7
+ query: unknown;
8
+ body: unknown;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ }
11
+ /** The minimal slice of a Fastify reply Halifax writes. */
12
+ interface FastifyReplyLike {
13
+ code(statusCode: number): FastifyReplyLike;
14
+ send(payload?: unknown): unknown;
15
+ header(name: string, value: string): unknown;
16
+ }
17
+ /** A Fastify route handler bound to the structural request/reply shapes above. */
18
+ type FastifyHandlerLike = (req: FastifyRequestLike, reply: FastifyReplyLike) => unknown;
19
+ /**
20
+ * The minimal slice of a Fastify instance that Halifax drives.
21
+ *
22
+ * Typing against this structural interface keeps the published `.d.ts` from pinning
23
+ * consumers to a specific `fastify` version, mirroring the approach used for Express.
24
+ */
25
+ export interface FastifyAppLike {
26
+ route(opts: {
27
+ method: string | string[];
28
+ url: string;
29
+ handler: FastifyHandlerLike;
30
+ }): unknown;
31
+ addContentTypeParser(contentType: string | RegExp | string[], opts: {
32
+ parseAs: 'string' | 'buffer';
33
+ }, parser: (req: unknown, body: string | Buffer, done: (err: Error | null, body?: unknown) => void) => void): unknown;
34
+ listen(opts: {
35
+ port: number;
36
+ host?: string;
37
+ }): Promise<unknown>;
38
+ }
39
+ /**
40
+ * Adapts a Fastify instance to Halifax's {@link HttpServer} interface.
41
+ *
42
+ * Two Fastify-specific concerns are handled here so the resulting API behaves like the
43
+ * Express adapter:
44
+ *
45
+ * - **415 parity.** A catch-all content-type parser is installed so non-JSON bodies are
46
+ * accepted (as raw strings) and reach the router, which then emits the structured 415
47
+ * — instead of Fastify's own non-Halifax 415 error page.
48
+ * - **405 parity.** The `'*'` catch-all is registered for every CRUD verb *not* already
49
+ * bound on the path, so unsupported methods return 405 (with the `Allow` header the
50
+ * router sets) rather than Fastify's default 404.
51
+ */
52
+ export declare class FastifyHttpServer implements HttpServer {
53
+ private readonly app;
54
+ /** Tracks which HTTP methods have been bound per URL, to compute the `'*'` complement. */
55
+ private readonly registered;
56
+ /**
57
+ * @param app - The Fastify instance (or encapsulated plugin instance) to register routes on.
58
+ */
59
+ constructor(app: FastifyAppLike);
60
+ /**
61
+ * Registers a route on the Fastify instance for the given method and path.
62
+ * @param method - HTTP method (or `'*'` for a catch-all spanning every unbound CRUD verb).
63
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
64
+ * @param handler - Halifax route handler to invoke on matching requests.
65
+ */
66
+ registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void;
67
+ /**
68
+ * Starts the Fastify server listening on the given port and host.
69
+ * @param port - TCP port number to bind to.
70
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
71
+ */
72
+ start(port: number, host?: string): Promise<void>;
73
+ }
74
+ /** Options for {@link createFastifyCrudPlugin}. Alias of {@link CrudApiOptions}. */
75
+ export type FastifyCrudPluginOptions = CrudApiOptions;
76
+ /** A Fastify plugin function: registered with `app.register(plugin, { prefix: '/api' })`. */
77
+ export type FastifyCrudPlugin = (instance: FastifyAppLike) => Promise<void>;
78
+ /**
79
+ * Creates a Fastify plugin that registers CRUD routes for every resource.
80
+ *
81
+ * Fastify's mounting mechanism is plugins-with-prefixes rather than mountable routers, so
82
+ * this is the idiomatic equivalent of {@link createExpressCrudRouter}:
83
+ *
84
+ * ```ts
85
+ * app.register(createFastifyCrudPlugin([postResource], { authStrategy }), { prefix: '/api/v1' })
86
+ * ```
87
+ *
88
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
89
+ * @param options - Auth strategy, query-builder path overrides, etc.
90
+ * @returns A Fastify plugin function ready to pass to `app.register`.
91
+ */
92
+ export declare function createFastifyCrudPlugin(resources: ResourceDefinition[], options?: FastifyCrudPluginOptions): FastifyCrudPlugin;
93
+ export {};
@@ -0,0 +1,125 @@
1
+ import { registerCrudApi } from '../../core/crudRouter.js';
2
+ /**
3
+ * The CRUD HTTP verbs Halifax registers. Used to compute the complement set for the
4
+ * `'*'` catch-all so unsupported methods produce a 405 (as Express's `app.all` does)
5
+ * rather than Fastify's default 404. `HEAD` is intentionally excluded because Fastify
6
+ * auto-exposes a `HEAD` route for every `GET`; including it would collide.
7
+ */
8
+ const ALL_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
9
+ /**
10
+ * Wraps a Fastify request in Halifax's framework-agnostic {@link HttpRequest}.
11
+ * @param req - The Fastify request to adapt.
12
+ * @returns A Halifax-compatible {@link HttpRequest} with the original request in `raw`.
13
+ */
14
+ function adaptRequest(req) {
15
+ return {
16
+ method: req.method,
17
+ params: (req.params ?? {}),
18
+ query: (req.query ?? {}),
19
+ body: req.body,
20
+ headers: req.headers,
21
+ raw: req
22
+ };
23
+ }
24
+ /**
25
+ * Wraps a Fastify reply in Halifax's framework-agnostic {@link HttpResponse}.
26
+ * @param reply - The Fastify reply to adapt.
27
+ * @returns A Halifax-compatible {@link HttpResponse} with the original reply in `raw`.
28
+ */
29
+ function adaptResponse(reply) {
30
+ return {
31
+ raw: reply,
32
+ status(code) {
33
+ reply.code(code);
34
+ return this;
35
+ },
36
+ json(payload) {
37
+ reply.send(payload);
38
+ },
39
+ send(payload) {
40
+ reply.send(payload);
41
+ },
42
+ setHeader(name, value) {
43
+ reply.header(name, value);
44
+ }
45
+ };
46
+ }
47
+ /**
48
+ * Adapts a Fastify instance to Halifax's {@link HttpServer} interface.
49
+ *
50
+ * Two Fastify-specific concerns are handled here so the resulting API behaves like the
51
+ * Express adapter:
52
+ *
53
+ * - **415 parity.** A catch-all content-type parser is installed so non-JSON bodies are
54
+ * accepted (as raw strings) and reach the router, which then emits the structured 415
55
+ * — instead of Fastify's own non-Halifax 415 error page.
56
+ * - **405 parity.** The `'*'` catch-all is registered for every CRUD verb *not* already
57
+ * bound on the path, so unsupported methods return 405 (with the `Allow` header the
58
+ * router sets) rather than Fastify's default 404.
59
+ */
60
+ export class FastifyHttpServer {
61
+ app;
62
+ /** Tracks which HTTP methods have been bound per URL, to compute the `'*'` complement. */
63
+ registered = new Map();
64
+ /**
65
+ * @param app - The Fastify instance (or encapsulated plugin instance) to register routes on.
66
+ */
67
+ constructor(app) {
68
+ this.app = app;
69
+ // Accept any Content-Type so the router — not Fastify — owns the 415 decision. The
70
+ // built-in `application/json` parser still takes precedence for JSON bodies.
71
+ try {
72
+ this.app.addContentTypeParser('*', { parseAs: 'string' }, (_req, body, done) => done(null, body));
73
+ }
74
+ catch {
75
+ // A parser for '*' is already registered on this instance — nothing to do.
76
+ }
77
+ }
78
+ /**
79
+ * Registers a route on the Fastify instance for the given method and path.
80
+ * @param method - HTTP method (or `'*'` for a catch-all spanning every unbound CRUD verb).
81
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
82
+ * @param handler - Halifax route handler to invoke on matching requests.
83
+ */
84
+ registerRoute(method, path, handler) {
85
+ const run = (req, reply) => Promise.resolve(handler(adaptRequest(req), adaptResponse(reply)));
86
+ if (method === '*') {
87
+ const bound = this.registered.get(path) ?? new Set();
88
+ const complement = ALL_METHODS.filter((m) => !bound.has(m));
89
+ if (complement.length)
90
+ this.app.route({ method: complement, url: path, handler: run });
91
+ return;
92
+ }
93
+ const bound = this.registered.get(path) ?? new Set();
94
+ bound.add(method);
95
+ this.registered.set(path, bound);
96
+ this.app.route({ method, url: path, handler: run });
97
+ }
98
+ /**
99
+ * Starts the Fastify server listening on the given port and host.
100
+ * @param port - TCP port number to bind to.
101
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
102
+ */
103
+ async start(port, host) {
104
+ await this.app.listen(host ? { port, host } : { port });
105
+ }
106
+ }
107
+ /**
108
+ * Creates a Fastify plugin that registers CRUD routes for every resource.
109
+ *
110
+ * Fastify's mounting mechanism is plugins-with-prefixes rather than mountable routers, so
111
+ * this is the idiomatic equivalent of {@link createExpressCrudRouter}:
112
+ *
113
+ * ```ts
114
+ * app.register(createFastifyCrudPlugin([postResource], { authStrategy }), { prefix: '/api/v1' })
115
+ * ```
116
+ *
117
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
118
+ * @param options - Auth strategy, query-builder path overrides, etc.
119
+ * @returns A Fastify plugin function ready to pass to `app.register`.
120
+ */
121
+ export function createFastifyCrudPlugin(resources, options = {}) {
122
+ return async function halifaxCrudPlugin(instance) {
123
+ registerCrudApi(new FastifyHttpServer(instance), resources, options);
124
+ };
125
+ }
@@ -0,0 +1,82 @@
1
+ import HyperExpress from 'hyper-express';
2
+ import type { HttpMethod, HttpRouteHandler, HttpServer, ResourceDefinition } from '../../core/types.js';
3
+ import { type CrudApiOptions } from '../../core/crudRouter.js';
4
+ /**
5
+ * The minimal slice of a HyperExpress request Halifax reads. HyperExpress exposes an
6
+ * Express-flavoured API on top of uWebSockets, but — unlike Express — the request body
7
+ * is not pre-parsed by middleware; it is downloaded on demand via {@link json}.
8
+ */
9
+ interface HyperExpressRequest {
10
+ method: string;
11
+ path_parameters: Record<string, string>;
12
+ query_parameters: Record<string, string>;
13
+ headers: Record<string, string | string[] | undefined>;
14
+ json<T = unknown, D = undefined>(default_value?: D): Promise<T | D>;
15
+ }
16
+ /** The minimal slice of a HyperExpress response Halifax writes. */
17
+ interface HyperExpressResponse {
18
+ status(code: number): HyperExpressResponse;
19
+ json(payload: unknown): unknown;
20
+ send(payload?: unknown): unknown;
21
+ header(name: string, value: string): unknown;
22
+ }
23
+ /** A HyperExpress route-registration method (e.g. `app.get`). */
24
+ type HyperExpressRouteRegistrar = (path: string, handler: (req: HyperExpressRequest, res: HyperExpressResponse) => unknown) => unknown;
25
+ /**
26
+ * The minimal slice of a HyperExpress `Server` or `Router` that Halifax drives.
27
+ *
28
+ * Typing against this structural interface keeps the published `.d.ts` from pinning
29
+ * consumers to a specific `hyper-express` version, mirroring the approach used for Express.
30
+ */
31
+ export interface HyperExpressAppLike {
32
+ get: HyperExpressRouteRegistrar;
33
+ post: HyperExpressRouteRegistrar;
34
+ put: HyperExpressRouteRegistrar;
35
+ patch: HyperExpressRouteRegistrar;
36
+ delete: HyperExpressRouteRegistrar;
37
+ /** Registers a handler for any HTTP method — HyperExpress's catch-all, used for 405 fallbacks. */
38
+ any: HyperExpressRouteRegistrar;
39
+ /** Present on a `Server` but not a `Router`; when absent, {@link HyperExpressHttpServer.start} is a no-op. */
40
+ listen?: (port: number, host?: string) => Promise<unknown>;
41
+ }
42
+ /**
43
+ * Adapts a HyperExpress `Server` or `Router` to Halifax's {@link HttpServer} interface.
44
+ *
45
+ * Like the Express adapter, every route uses plain segments and `:id` named params. The
46
+ * `'*'` catch-all maps to HyperExpress's `any()`, which matches methods not otherwise
47
+ * registered on the path — giving the same 405 behaviour as Express's `app.all`.
48
+ */
49
+ export declare class HyperExpressHttpServer implements HttpServer {
50
+ private readonly app;
51
+ /**
52
+ * @param app - HyperExpress server or router to register routes on.
53
+ * When a `Server` is provided, `start()` will call `listen()`.
54
+ * When a `Router` is provided, `start()` is a no-op.
55
+ */
56
+ constructor(app: HyperExpressAppLike);
57
+ /**
58
+ * Registers a route on the app for the given method and path.
59
+ * @param method - HTTP method (or `'*'` to register a catch-all via `app.any`).
60
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
61
+ * @param handler - Halifax route handler to invoke on matching requests.
62
+ */
63
+ registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void;
64
+ /**
65
+ * Starts the server. No-op when the underlying app does not expose a `listen` method
66
+ * (e.g. when `app` is a `Router` mounted on an existing server).
67
+ * @param port - TCP port number to bind to.
68
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
69
+ */
70
+ start(port: number, host?: string): Promise<void>;
71
+ }
72
+ /** Options for {@link createHyperExpressCrudRouter}. Alias of {@link CrudApiOptions}. */
73
+ export type HyperExpressCrudRouterOptions = CrudApiOptions;
74
+ /**
75
+ * Creates a fully-wired HyperExpress `Router` with CRUD routes for every resource.
76
+ *
77
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
78
+ * @param options - Auth strategy, query-builder path overrides, etc.
79
+ * @returns A HyperExpress `Router` ready to mount with `app.use('/api', router)`.
80
+ */
81
+ export declare function createHyperExpressCrudRouter(resources: ResourceDefinition[], options?: HyperExpressCrudRouterOptions): InstanceType<typeof HyperExpress.Router>;
82
+ export {};
@@ -0,0 +1,128 @@
1
+ import HyperExpress from 'hyper-express';
2
+ import { registerCrudApi } from '../../core/crudRouter.js';
3
+ /** Maps a Halifax {@link HttpMethod} to the corresponding HyperExpress registration method name. */
4
+ const METHOD_TO_REGISTRAR = {
5
+ GET: 'get',
6
+ POST: 'post',
7
+ PUT: 'put',
8
+ PATCH: 'patch',
9
+ DELETE: 'delete'
10
+ };
11
+ /** HTTP methods that may carry a request body Halifax needs to parse. */
12
+ const BODY_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
13
+ /**
14
+ * Reads the first value of a (possibly array) header.
15
+ * @param value - The raw header value.
16
+ * @returns The header as a lowercase-safe string, or `''` when absent.
17
+ */
18
+ function headerValue(value) {
19
+ const first = Array.isArray(value) ? value[0] : value;
20
+ return typeof first === 'string' ? first : '';
21
+ }
22
+ /**
23
+ * Wraps a HyperExpress request in Halifax's framework-agnostic {@link HttpRequest}.
24
+ *
25
+ * The JSON body is downloaded here (HyperExpress does not pre-parse it). Only requests
26
+ * that both carry a body-bearing method and declare a JSON `Content-Type` are parsed;
27
+ * everything else is left as `undefined`, which lets the CRUD router emit a 415 for
28
+ * non-JSON payloads exactly as the Express adapter (with `express.json()`) does.
29
+ * @param req - The HyperExpress request to adapt.
30
+ * @returns A Halifax-compatible {@link HttpRequest} with the original request in `raw`.
31
+ */
32
+ async function adaptRequest(req) {
33
+ let body;
34
+ const contentType = headerValue(req.headers['content-type']);
35
+ if (BODY_METHODS.has(req.method.toUpperCase()) && contentType.includes('application/json')) {
36
+ try {
37
+ body = await req.json(undefined);
38
+ }
39
+ catch {
40
+ body = undefined;
41
+ }
42
+ }
43
+ return {
44
+ method: req.method,
45
+ params: req.path_parameters,
46
+ query: req.query_parameters,
47
+ body,
48
+ headers: req.headers,
49
+ raw: req
50
+ };
51
+ }
52
+ /**
53
+ * Wraps a HyperExpress response in Halifax's framework-agnostic {@link HttpResponse}.
54
+ * @param res - The HyperExpress response to adapt.
55
+ * @returns A Halifax-compatible {@link HttpResponse} with the original response in `raw`.
56
+ */
57
+ function adaptResponse(res) {
58
+ return {
59
+ raw: res,
60
+ status(code) {
61
+ res.status(code);
62
+ return this;
63
+ },
64
+ json(payload) {
65
+ res.json(payload);
66
+ },
67
+ send(payload) {
68
+ res.send(payload);
69
+ },
70
+ setHeader(name, value) {
71
+ res.header(name, value);
72
+ }
73
+ };
74
+ }
75
+ /**
76
+ * Adapts a HyperExpress `Server` or `Router` to Halifax's {@link HttpServer} interface.
77
+ *
78
+ * Like the Express adapter, every route uses plain segments and `:id` named params. The
79
+ * `'*'` catch-all maps to HyperExpress's `any()`, which matches methods not otherwise
80
+ * registered on the path — giving the same 405 behaviour as Express's `app.all`.
81
+ */
82
+ export class HyperExpressHttpServer {
83
+ app;
84
+ /**
85
+ * @param app - HyperExpress server or router to register routes on.
86
+ * When a `Server` is provided, `start()` will call `listen()`.
87
+ * When a `Router` is provided, `start()` is a no-op.
88
+ */
89
+ constructor(app) {
90
+ this.app = app;
91
+ }
92
+ /**
93
+ * Registers a route on the app for the given method and path.
94
+ * @param method - HTTP method (or `'*'` to register a catch-all via `app.any`).
95
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
96
+ * @param handler - Halifax route handler to invoke on matching requests.
97
+ */
98
+ registerRoute(method, path, handler) {
99
+ const cb = async (req, res) => {
100
+ await Promise.resolve(handler(await adaptRequest(req), adaptResponse(res)));
101
+ };
102
+ const register = method === '*' ? this.app.any : this.app[METHOD_TO_REGISTRAR[method]];
103
+ register.call(this.app, path, cb);
104
+ }
105
+ /**
106
+ * Starts the server. No-op when the underlying app does not expose a `listen` method
107
+ * (e.g. when `app` is a `Router` mounted on an existing server).
108
+ * @param port - TCP port number to bind to.
109
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
110
+ */
111
+ async start(port, host) {
112
+ if (typeof this.app.listen !== 'function')
113
+ return;
114
+ await (host ? this.app.listen(port, host) : this.app.listen(port));
115
+ }
116
+ }
117
+ /**
118
+ * Creates a fully-wired HyperExpress `Router` with CRUD routes for every resource.
119
+ *
120
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
121
+ * @param options - Auth strategy, query-builder path overrides, etc.
122
+ * @returns A HyperExpress `Router` ready to mount with `app.use('/api', router)`.
123
+ */
124
+ export function createHyperExpressCrudRouter(resources, options = {}) {
125
+ const router = new HyperExpress.Router();
126
+ registerCrudApi(new HyperExpressHttpServer(router), resources, options);
127
+ return router;
128
+ }
@@ -0,0 +1,84 @@
1
+ import ultimateExpress from 'ultimate-express';
2
+ import type { HttpMethod, HttpRouteHandler, HttpServer, ResourceDefinition } from '../../core/types.js';
3
+ import { type CrudApiOptions } from '../../core/crudRouter.js';
4
+ declare const Router: typeof ultimateExpress.Router;
5
+ /** A route-registration method as exposed by an Ultimate Express app or router (e.g. `app.get`). */
6
+ type UltimateExpressRouteRegistrar = (path: string, handler: (req: UltimateExpressRequest, res: UltimateExpressResponse) => void) => void;
7
+ /**
8
+ * The minimal slice of an Ultimate Express request Halifax reads. Ultimate Express is a
9
+ * drop-in, uWebSockets-backed reimplementation of the Express API, so this mirrors the
10
+ * Express request surface exactly.
11
+ */
12
+ interface UltimateExpressRequest {
13
+ method: string;
14
+ params: Record<string, string>;
15
+ query: Record<string, unknown>;
16
+ body: unknown;
17
+ headers: Record<string, string | string[] | undefined>;
18
+ }
19
+ /** The minimal slice of an Ultimate Express response Halifax writes. */
20
+ interface UltimateExpressResponse {
21
+ status(code: number): UltimateExpressResponse;
22
+ json(payload: unknown): unknown;
23
+ send(payload?: unknown): unknown;
24
+ setHeader(name: string, value: string): unknown;
25
+ }
26
+ /**
27
+ * The minimal slice of an Ultimate Express application or router that Halifax drives.
28
+ *
29
+ * Typing against this structural interface — rather than importing concrete types from
30
+ * `ultimate-express` — keeps the published `.d.ts` from pinning consumers to a specific
31
+ * version, exactly as {@link ExpressHttpServer} does for Express.
32
+ */
33
+ export interface UltimateExpressAppLike {
34
+ get: UltimateExpressRouteRegistrar;
35
+ post: UltimateExpressRouteRegistrar;
36
+ put: UltimateExpressRouteRegistrar;
37
+ patch: UltimateExpressRouteRegistrar;
38
+ delete: UltimateExpressRouteRegistrar;
39
+ all: UltimateExpressRouteRegistrar;
40
+ /** Present on an `App` but not a `Router`; when absent, {@link UltimateExpressHttpServer.start} is a no-op. */
41
+ listen?: (port: number, host: string | undefined, callback: () => void) => unknown;
42
+ }
43
+ /**
44
+ * Adapts an Ultimate Express `App` or `Router` to Halifax's {@link HttpServer} interface.
45
+ *
46
+ * Because Ultimate Express implements the Express API, this adapter is a near-identical
47
+ * twin of {@link ExpressHttpServer}: it uses the same route methods, `:id` named params
48
+ * (never `*` path wildcards), and request/response surface. The difference is purely the
49
+ * underlying transport (uWebSockets), which is faster but otherwise transparent.
50
+ */
51
+ export declare class UltimateExpressHttpServer implements HttpServer {
52
+ private readonly app;
53
+ /**
54
+ * @param app - Ultimate Express application or router to register routes on.
55
+ * When an `App` is provided, `start()` will call `listen()`.
56
+ * When a `Router` is provided, `start()` is a no-op.
57
+ */
58
+ constructor(app: UltimateExpressAppLike);
59
+ /**
60
+ * Registers a route on the app for the given method and path.
61
+ * @param method - HTTP method (or `'*'` to register a catch-all via `app.all`).
62
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
63
+ * @param handler - Halifax route handler to invoke on matching requests.
64
+ */
65
+ registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void;
66
+ /**
67
+ * Starts the server. No-op when the underlying app does not expose a `listen` method
68
+ * (e.g. when `app` is a `Router` mounted on an existing app).
69
+ * @param port - TCP port number to bind to.
70
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
71
+ */
72
+ start(port: number, host?: string): Promise<void>;
73
+ }
74
+ /** Options for {@link createUltimateExpressCrudRouter}. Alias of {@link CrudApiOptions}. */
75
+ export type UltimateExpressCrudRouterOptions = CrudApiOptions;
76
+ /**
77
+ * Creates a fully-wired Ultimate Express `Router` with CRUD routes for every resource.
78
+ *
79
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
80
+ * @param options - Auth strategy, query-builder path overrides, etc.
81
+ * @returns An Ultimate Express `Router` ready to mount with `app.use('/api', router)`.
82
+ */
83
+ export declare function createUltimateExpressCrudRouter(resources: ResourceDefinition[], options?: UltimateExpressCrudRouterOptions): ReturnType<typeof Router>;
84
+ export {};