@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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,88 @@
3
3
  All notable changes to this project are documented here. This project adheres to
4
4
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## [2.0.0]
7
+
8
+ A breaking release with two themes: **permissive, minimal-by-default resource definitions**
9
+ (declare the exceptions, not the boilerplate), and a **full real-database CI matrix** that
10
+ verifies every supported engine for real.
11
+
12
+ ### Added
13
+
14
+ - **Full real-database CI matrix** — the integration suite now runs against **six** engines,
15
+ each in its own container: PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite
16
+ (embedded). Previously only PostgreSQL, MySQL, and SQLite ran in CI.
17
+ - `docker-compose.test.yml` (one service per engine) and `scripts/integration-matrix.sh` /
18
+ `pnpm test:integration:all` to bring the databases up and run the suite against each — the
19
+ same path CI uses, so local and CI runs are identical.
20
+ - Prisma schemas for SQL Server (`schema.mssql.prisma`) and CockroachDB
21
+ (`schema.cockroachdb.prisma`, using `sequence()` ids to stay 32-bit-safe), and the
22
+ `@prisma/adapter-mssql` driver adapter.
23
+ - An ID-kind-aware integration suite: assertions adapt to the engine's key type (integer vs
24
+ MongoDB `ObjectId`), so one suite runs honestly across every engine.
25
+
26
+ ### Changed (breaking)
27
+
28
+ - **`ResourceDefinition` is permissive and minimal.** Only `routePrefix`, `repository`, and
29
+ `fields` are required — and `fields` only when the repository exposes no schema of its own.
30
+ - `name` is now **optional** — it defaults to a title-cased form of `routePrefix`
31
+ (`'blog-posts'` → `'Blog Posts'`) and can still be overridden.
32
+ - **`fields` is now optional and override-aware.** When the repository exposes a field schema
33
+ (any `PrismaAdapter` built with a `model`, and everything from `createPrismaResources`), that
34
+ schema is the base and the resource's `fields` are merged over it **by name** as sparse
35
+ overrides — so you list a field only to _change_ it. With a bare adapter, `fields` remains
36
+ the authoritative allow-list.
37
+ - **Field flags are permissive by default.** `filterable`, `sortable`, `selectable`, and now
38
+ **`writable`** all default to `true`. Previously `writable` defaulted to `false` — bodies now
39
+ accept any defined field unless you set `writable: false`. The **primary key is protected**: it
40
+ is non-writable by default (set `writable: true` to opt in).
41
+ - **Page size is bounded by generous defaults — at most 5000 records per request.**
42
+ `defaultLimit: 5000` and `maxLimit: 5000` (exported as `DEFAULT_PAGE_LIMIT` / `MAX_PAGE_LIMIT`)
43
+ apply when a resource sets none — large enough for typical "show everything" UIs, a seatbelt
44
+ against an accidental unbounded scan of a large table. Previously an unset limit returned every
45
+ row, uncapped. The response `count` is always the true total, so a capped page is never a silent
46
+ drop. Set `defaultLimit: 0` to skip the default bound (return all rows when `?limit=` is omitted)
47
+ and `maxLimit: 0` to remove the cap — use both to disable pagination entirely.
48
+ - **`RepositoryCapabilities` trimmed to the two flags that carry their weight:** removed
49
+ `supportsTransactions` (no transaction feature existed) and `supportsQueryAst` (always true;
50
+ implied by the presence of `executeQuery`). `supportsIncludes` now has teeth — the router
51
+ rejects `?include=` with `422` when a repository reports `supportsIncludes: false`. The
52
+ `Repository` interface gained optional `fields` / `relations` / `idField` for schema exposure.
53
+ - **Widened the `@prisma/client` peer dependency to `>=6.0.0`** (was `>=7.0.0`). `PrismaAdapter`
54
+ imports nothing from `@prisma/client` and only calls stable model-delegate methods, so it runs
55
+ unchanged on Prisma 6 or 7. CI exercises Prisma 7 only, so Prisma 6 is best-effort; its main
56
+ draw is MongoDB, which Prisma 7 does not yet support. See README_REPO_ADAPTERS.md for the
57
+ schema/client differences a Prisma 6 project needs.
58
+
59
+ ### Removed
60
+
61
+ - **MongoDB** from the advertised supported-database list and the CI matrix. Prisma ORM v7
62
+ dropped MongoDB support ("coming soon in v7"), and the matrix targets Prisma 7. MongoDB still
63
+ works on **Prisma 6** (now also supported — see above). The forward-ready `schema.mongodb.prisma`
64
+ and an `ObjectId`-aware integration suite remain in the repo so MongoDB rejoins the matrix
65
+ unchanged once Prisma 7 restores support.
66
+ - The "PostgreSQL, MySQL, and SQLite run in CI; the rest use the same adapter and test harness"
67
+ documentation caveat — every advertised relational engine is now verified in CI against a real
68
+ database.
69
+ - The deprecated auth aliases `AuthProvider`, `AllowAllAuthProvider`, `ApiKeyAuthProvider`, and
70
+ `PermissionAuthProvider`. Use `AuthStrategy`, `AllowAllAuthStrategy`, `ApiKeyAuthStrategy`, and
71
+ `JwtClaimsAuthStrategy` respectively.
72
+
73
+ ### Migration
74
+
75
+ - Resource definitions can be slimmed dramatically: drop `name` (unless you want a specific one),
76
+ drop per-field `filterable`/`sortable`/`selectable`/`writable: true` flags, and drop
77
+ `defaultLimit`/`maxLimit` if 5000/5000 suit you.
78
+ - **If your app relied on list endpoints returning _every_ row** (no limit), set
79
+ `defaultLimit: 0` and `maxLimit: 0` on those resources (or globally via
80
+ `createPrismaResources({ defaultLimit: 0, maxLimit: 0 })`) — otherwise results are now bounded
81
+ at 5000 by default.
82
+ - If you relied on `writable` defaulting to `false`, audit your `fields`: any field that should
83
+ not be client-writable now needs an explicit `writable: false` (the primary key is already
84
+ protected automatically).
85
+ - If you read `capabilities.supportsTransactions` or `capabilities.supportsQueryAst`, remove those
86
+ references (use `typeof repo.executeQuery === 'function'` to detect query-AST support).
87
+
6
88
  ## [1.0.0]
7
89
 
8
90
  First public release.
@@ -28,4 +110,5 @@ First public release.
28
110
  - **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action
29
111
  required permissions; and `filterable`/`sortable`/`selectable`/`writable` field flags.
30
112
 
113
+ [2.0.0]: https://github.com/splayfee/halifax/releases/tag/v2.0.0
31
114
  [1.0.0]: https://github.com/splayfee/halifax/releases/tag/v1.0.0
package/README.md CHANGED
@@ -14,21 +14,21 @@ The package is split into small, replaceable layers — nothing is imported into
14
14
  - 🚀 **Zero-boilerplate CRUD** — define a resource once and get standards-compliant REST endpoints (list, read, create, update, upsert, delete, bulk) with correct status codes and a consistent error shape.
15
15
  - 🧩 **Adapter-driven & swappable** — your HTTP framework, ORM/database, and auth provider are injected, not baked in. Switch any layer without touching your resource definitions.
16
16
  - 🌐 **4 HTTP frameworks, identical behavior** — Express 4/5, Fastify, HyperExpress, and Ultimate Express, all verified against one shared conformance suite.
17
- - 🗄️ **Many databases, one adapter** — PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, CockroachDB, and MongoDB via [Prisma](https://www.prisma.io/). The query builder compiles to portable Prisma calls (never raw SQL), so the **same client request behaves identically on every database** — switch engines by changing one line. (PostgreSQL, MySQL, and SQLite run in CI; the rest use the same adapter and test harness.)
17
+ - 🗄️ **Six databases, one adapter** — PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite via [Prisma](https://www.prisma.io/). The query builder compiles to portable Prisma calls (never raw SQL), so the **same client request behaves identically on every database** — switch engines by changing one line. Every engine is verified in CI against a real database — one matrix leg each, the same suite on all.
18
18
  - 🔎 **Dynamic query-builder endpoint** — let the front-end compose rich filtered/sorted/paginated queries "for free" (`AND`/`OR`/nesting, `IN`, `BETWEEN`, `CONTAINS`, …) without hand-writing endpoints. Fully validated — bad fields/operators return structured `4xx` errors, never leaked DB internals.
19
19
  - 🏢 **Multi-tenancy built in** — per-resource tenant scoping with fail-closed guarantees; one tenant can never read or write another's rows.
20
20
  - ⚡ **Pluggable read-through caching** — in-memory or Redis, per-resource TTLs, never-expire mode, automatic write-invalidation, tenant-safe keys, and a `Cache-Control` bust header.
21
21
  - 🔐 **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action permissions; and `filterable`/`sortable`/`selectable`/`writable` flags enforced on every request.
22
- - 🧪 **Type-safe & battle-tested** — strict TypeScript, ESM, ships full `.d.ts`; hundreds of unit tests plus real-database + Redis integration tests in CI.
22
+ - 🧪 **Type-safe & battle-tested** — strict TypeScript, ESM, ships full `.d.ts`; hundreds of unit tests plus the full integration suite run against six real databases + Redis in CI.
23
23
 
24
24
  ## Current Support
25
25
 
26
- | Layer | Supported |
27
- | -------------- | ----------------------------------------------------------------------------- |
28
- | HTTP server | Express 4/5, Fastify, HyperExpress, Ultimate Express |
29
- | ORM / database | Prisma 7 + Postgres, MySQL, MariaDB, SQL Server, SQLite, MongoDB, CockroachDB |
30
- | Auth | API key, JWT/Bearer, Passport + JWT |
31
- | Caching | Pluggable read-through cache (in-memory default; bring Redis, etc.) |
26
+ | Layer | Supported |
27
+ | -------------- | ------------------------------------------------------------------------- |
28
+ | HTTP server | Express 4/5, Fastify, HyperExpress, Ultimate Express |
29
+ | ORM / database | Prisma 6 or 7 + Postgres, MySQL, MariaDB, SQL Server, CockroachDB, SQLite |
30
+ | Auth | API key, JWT/Bearer, Passport + JWT |
31
+ | Caching | Pluggable read-through cache (in-memory default; bring Redis, etc.) |
32
32
 
33
33
  Every HTTP adapter is interchangeable and behaves identically — same routes, status codes,
34
34
  error-body shape, and content negotiation — so you can switch frameworks without touching
@@ -38,9 +38,19 @@ your resource definitions, auth, or query logic. See
38
38
  The same is true across databases: the dynamic query-builder endpoint and all CRUD compile
39
39
  to portable Prisma Client calls (never raw SQL), so the **same client request behaves
40
40
  identically on every database** — switch engines by changing only the Prisma `provider`. The
41
- integration suite runs unchanged against Postgres, MySQL, and SQLite in CI to keep that honest.
42
-
43
- > **Roadmap** — This project welcomes community-written adapters for Drizzle, Sequelize, etc.
41
+ integration suite runs unchanged against all six engines — Postgres, MySQL, MariaDB, SQL Server,
42
+ CockroachDB, and SQLite — in CI (one matrix leg per engine) to keep that honest.
43
+
44
+ > **MongoDB — coming back.** MongoDB is absent from the list above for one reason only:
45
+ > **Prisma ORM v7 dropped its MongoDB connector** (Prisma's own guidance is to stay on v6 for
46
+ > Mongo; v7 support is "coming soon"). This is a Prisma limitation, not a Halifax one —
47
+ > `PrismaAdapter` is database-agnostic and never touches the connection. The
48
+ > `schema.mongodb.prisma` and an `ObjectId`-aware integration suite are already in the repo, so
49
+ > **the moment Prisma restores MongoDB support in v7, Halifax will add it back** — it rejoins
50
+ > the CI matrix unchanged, with no API changes for you. Need MongoDB today? It still works on
51
+ > **Prisma 6**, which Halifax also supports — see [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md).
52
+ >
53
+ > **Roadmap** — community-written adapters for Drizzle, Sequelize, etc. are also welcome.
44
54
 
45
55
  ## Install
46
56
 
@@ -64,22 +74,18 @@ import {
64
74
 
65
75
  const prisma = new PrismaClient({ adapter: new PrismaPg(process.env.DATABASE_URL!) })
66
76
 
77
+ // Permissive + minimal by default: only `routePrefix`, `repository`, and `fields` are required.
78
+ // Every field is filterable / sortable / selectable / writable unless you turn it off, and the
79
+ // primary key is never writable. Every CRUD action is enabled unless you disable it.
67
80
  const posts: ResourceDefinition = {
68
- name: 'Post',
69
81
  routePrefix: 'posts',
70
- defaultLimit: 50,
71
- maxLimit: 200,
72
- fields: [
73
- { name: 'id', filterable: true, sortable: true },
74
- { name: 'title', filterable: true, sortable: true, writable: true },
75
- { name: 'content', writable: true },
76
- { name: 'published', filterable: true, writable: true }
77
- ],
78
- permissions: {
79
- allowUpdateMany: false,
80
- allowDeleteMany: false
81
- },
82
- repository: new PrismaAdapter({ delegate: prisma.post })
82
+ repository: new PrismaAdapter({ delegate: prisma.post }),
83
+ fields: [{ name: 'id' }, { name: 'title' }, { name: 'content' }, { name: 'published' }]
84
+
85
+ // Everything below is OPTIONAL shown here just to illustrate the exceptions you *can* set:
86
+ // name: 'Post', // defaults to a title-cased routePrefix ('posts' → 'Posts')
87
+ // permissions: { allowDeleteMany: false }, // all actions on by default; list only what to disable
88
+ // defaultLimit: 5000, maxLimit: 5000, // these are the defaults already (0 / 0 = no pagination)
83
89
  }
84
90
 
85
91
  const app = express()
@@ -91,58 +97,74 @@ app.use(
91
97
  app.listen(3000)
92
98
  ```
93
99
 
100
+ > **Even less boilerplate.** For a Prisma-backed API, `createPrismaResources(prisma, Prisma.dmmf.datamodel.models)`
101
+ > derives a fully-configured resource for **every** model — no `fields`, no `routePrefix`, nothing
102
+ > to hand-write — and you override only the exceptions per model. The hand-built resource above is
103
+ > the path for custom, non-Prisma repositories. See
104
+ > [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md) for both, and
105
+ > [README_AUTOCRUD.md](./README_AUTOCRUD.md) for a "verbose mode" resource with every option
106
+ > annotated.
107
+
94
108
  ## Documentation
95
109
 
96
110
  | Guide | Contents |
97
111
  | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
98
112
  | [README_AUTOCRUD.md](./README_AUTOCRUD.md) | Resource definitions, field flags, ID types, pagination, query-string filtering, error shapes |
99
- | [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md) | Prisma 7 setup, `PrismaAdapter` options, capabilities, custom repositories |
113
+ | [README_REPO_ADAPTERS.md](./README_REPO_ADAPTERS.md) | Prisma 7 (and 6) setup, `PrismaAdapter` options, capabilities, custom repositories |
100
114
  | [README_HTTP_ADAPTERS.md](./README_HTTP_ADAPTERS.md) | Express, Fastify, HyperExpress & Ultimate Express adapters, and custom HTTP adapters |
101
115
  | [README_AUTH.md](./README_AUTH.md) | Auth strategies (`ApiKey`, `JWT`, `Passport`), `requiredPermissions`, custom `authorize` |
102
116
  | [README_MULTITENANCY.md](./README_MULTITENANCY.md) | Tenant isolation: `tenant` options, auto-detection, scoping guarantees, fail-closed behaviour |
103
117
  | [README_QUERYBUILDER.md](./README_QUERYBUILDER.md) | Query-builder payload, comparisons, nested filters, portable Prisma execution |
104
118
  | [README_CACHE.md](./README_CACHE.md) | Read-through caching: in-memory & Redis stores, never-expire, cache-bust header |
105
119
 
120
+ ## Examples
121
+
122
+ Runnable, self-contained examples live in [`examples/`](./examples) — each is a complete server
123
+ you can start with `pnpm tsx examples/<file>.ts`:
124
+
125
+ - **One per HTTP adapter** — `http-express.ts` (the canonical one, with an annotated "verbose
126
+ mode"), `http-fastify.ts`, `http-hyper-express.ts`, `http-ultimate-express.ts`. Same resources,
127
+ same behaviour; only the framework wiring differs.
128
+ - **One per database** — `db-postgres.ts`, `db-mysql.ts`, `db-mariadb.ts`, `db-mssql.ts`,
129
+ `db-cockroachdb.ts`, `db-sqlite.ts`. Express + Prisma against each engine; only the driver
130
+ adapter and connection string change.
131
+
106
132
  ## Running Integration Tests
107
133
 
108
- The integration suite tests the full stack against a real PostgreSQL database.
134
+ The integration suite runs the full stack against a **real database**, and the _same_ suite runs
135
+ unchanged against every supported engine. `docker-compose.test.yml` brings up one container per
136
+ engine (SQLite is embedded, so it needs none); `HALIFAX_DB` + `DATABASE_URL` select which one a
137
+ run targets, and `globalSetup` runs `prisma generate` + `prisma db push` automatically.
109
138
 
110
- ### 1. Start a Postgres container
139
+ ### Run against every database
111
140
 
112
141
  ```bash
113
- docker run -d \
114
- --name halifax-test-db \
115
- --restart unless-stopped \
116
- -e POSTGRES_USER=postgres \
117
- -e POSTGRES_PASSWORD=postgres \
118
- -e POSTGRES_DB=halifax_test \
119
- -p 5432:5432 \
120
- postgres:17
142
+ docker compose -f docker-compose.test.yml up -d --wait # postgres, mysql, mariadb, mssql, cockroachdb, redis
143
+ pnpm test:integration:all # runs the suite against all six engines
144
+ docker compose -f docker-compose.test.yml down -v # tear everything down
121
145
  ```
122
146
 
123
- ### 2. Create `.env.test`
147
+ ### Run against a single database
148
+
149
+ `pnpm test:integration` targets PostgreSQL by default (via `.env.test`). To target one specific
150
+ engine, use the matrix script — it sets the right `DATABASE_URL` for you:
124
151
 
125
152
  ```bash
126
- DATABASE_URL="postgresql://postgres:postgres@localhost:5432/halifax_test"
153
+ docker compose -f docker-compose.test.yml up -d --wait cockroachdb redis
154
+ bash scripts/integration-matrix.sh cockroachdb # or: postgres | mysql | mariadb | mssql | sqlite
127
155
  ```
128
156
 
129
- ### 3. Run
157
+ Or the classic single-Postgres flow with a `.env.test` file:
130
158
 
131
159
  ```bash
132
- pnpm test:integration
160
+ # .env.test
161
+ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/halifax_test"
133
162
  ```
134
163
 
135
- `globalSetup` runs `prisma generate` and `prisma db push` automatically before any test executes.
136
-
137
- ### Subsequent runs
138
-
139
164
  ```bash
140
- docker start halifax-test-db
141
165
  pnpm test:integration
142
166
  ```
143
167
 
144
- ### Tear down
145
-
146
- ```bash
147
- docker stop halifax-test-db && docker rm halifax-test-db
148
- ```
168
+ > MongoDB is not in the matrix yet — Prisma 7 dropped MongoDB support (coming soon in v7). The
169
+ > schema and ObjectId-aware suite are already in place and rejoin the matrix unchanged once
170
+ > Prisma supports it.
@@ -8,31 +8,61 @@ Halifax generates REST endpoints automatically from a `ResourceDefinition`. Defi
8
8
  import type { ResourceDefinition } from '@edium/halifax'
9
9
  import { postRepository } from './repositories/post.js'
10
10
 
11
+ // Permissive + minimal by default. Only `routePrefix`, `repository`, and `fields` are
12
+ // required — and `fields` only when the repository can't supply its own schema (see below).
11
13
  export const postResource: ResourceDefinition = {
12
- name: 'Post',
13
14
  routePrefix: 'posts',
14
- defaultLimit: 50, // applied when the caller omits ?limit=
15
- maxLimit: 200, // requests above this are silently capped
15
+ repository: postRepository,
16
16
  fields: [
17
- { name: 'id', filterable: true, sortable: true },
18
- { name: 'title', filterable: true, sortable: true, writable: true },
19
- { name: 'content', writable: true },
17
+ { name: 'id' }, // primary key non-writable automatically
18
+ { name: 'title' },
19
+ { name: 'content' },
20
+ { name: 'published' },
21
+ { name: 'authorId', writable: false }, // the only "exceptions" you need to spell out:
22
+ { name: 'createdAt', writable: false } // FK + server-managed timestamp are read-only
23
+ ]
24
+ }
25
+ ```
26
+
27
+ > **Why so few fields?** When the repository already knows the model schema — any
28
+ > `PrismaAdapter` built with a `model`, and everything from `createPrismaResources` — you can
29
+ > omit `fields` entirely; the repository's schema becomes the base and anything you list is
30
+ > merged over it **by name** as a sparse override. So you declare a field only to _change_ it
31
+ > (e.g. `{ name: 'content', writable: false }`). The bare adapter above (no model) is the path
32
+ > for **custom, non-Prisma repositories**, where `fields` is how you declare the API surface.
33
+
34
+ ### Verbose mode — every option, defaults made explicit
35
+
36
+ Nothing here is required; this is the same resource with every knob turned and annotated, so
37
+ you can see exactly what the minimal form above is defaulting to.
38
+
39
+ ```ts
40
+ export const postResource: ResourceDefinition = {
41
+ routePrefix: 'posts',
42
+ repository: postRepository,
43
+ name: 'Post', // default: title-cased routePrefix ('posts' → 'Posts')
44
+ fields: [
45
+ // Every flag defaults to `true`; the primary key is non-writable unless opted in.
46
+ { name: 'id', filterable: true, sortable: true, selectable: true, writable: false },
47
+ { name: 'title', filterable: true, sortable: true, selectable: true, writable: true },
48
+ { name: 'content', selectable: true, writable: true },
20
49
  { name: 'published', filterable: true, writable: true },
21
- { name: 'authorId', filterable: true },
22
- { name: 'createdAt', filterable: false, sortable: true, selectable: true }
50
+ { name: 'authorId', filterable: true, writable: false },
51
+ { name: 'createdAt', sortable: true, writable: false }
23
52
  ],
24
- relations: [{ name: 'author', includable: true }],
53
+ relations: [{ name: 'author', includable: true }], // default: includable when listed
25
54
  permissions: {
55
+ // All nine actions default to `true` — list only the ones you DISABLE.
26
56
  allowDeleteMany: false
27
57
  },
28
58
  requiredPermissions: {
29
59
  readMany: ['posts.read'],
30
- readOne: ['posts.read'],
31
60
  create: ['posts.create'],
32
- updateOne: ['posts.update'],
33
61
  deleteOne: ['posts.delete']
34
62
  },
35
- repository: postRepository
63
+ defaultLimit: 5000, // default: 5000 (0 = return all rows when ?limit= is omitted)
64
+ maxLimit: 5000, // default: 5000 (0 = no cap)
65
+ cache: { ttlSeconds: 30 } // default: caching off
36
66
  }
37
67
  ```
38
68
 
@@ -66,14 +96,14 @@ GET /api/v1/posts/507f1f77bcf86cd799439011
66
96
 
67
97
  ## Field Flags
68
98
 
69
- Each entry in `fields` accepts four optional boolean flags. All default to `true` — only set them explicitly to `false` to restrict access.
99
+ Each entry in `fields` accepts four optional boolean flags. All default to `true` — only set them explicitly to `false` to restrict access. The lone exception: the **primary key** is non-writable by default (it comes from the URL / database); set `writable: true` on it if you really want clients to supply it.
70
100
 
71
101
  | Flag | Effect when `false` |
72
102
  | ------------ | --------------------------------------------------------------------- |
73
103
  | `filterable` | Rejects the field as a query-string filter (`?fieldName=value`) → 400 |
74
104
  | `sortable` | Rejects the field in `?order=` and query-builder `orderBy` → 400 |
75
105
  | `selectable` | Rejects the field in `?fields=` and query-builder `fields` → 400 |
76
- | `writable` | Silently strips the field from POST / PATCH request bodies |
106
+ | `writable` | Silently strips the field from POST / PATCH / PUT request bodies |
77
107
 
78
108
  Example — `role` is fully locked down; `createdAt` can be read and sorted but never written or filtered:
79
109
 
@@ -88,16 +118,28 @@ fields: [
88
118
 
89
119
  ## Pagination
90
120
 
91
- Set `defaultLimit` and `maxLimit` on the resource:
121
+ **By default a list endpoint returns at most 5000 records.** Page size is bounded by generous
122
+ defaults — **`defaultLimit: 5000`** (used when the caller omits `?limit=`) and **`maxLimit: 5000`**
123
+ (the hard ceiling) — a seatbelt against an accidental unbounded scan of a large table, nothing
124
+ more. The response `count` is always the true total matching the query, so a capped page is never
125
+ a _silent_ drop — a client can see when `count` exceeds the number of returned rows. Override
126
+ either per resource:
92
127
 
93
128
  ```ts
94
129
  {
95
- defaultLimit: 50, // used when the caller omits ?limit=
96
- maxLimit: 200, // requests above this are silently capped to 200
130
+ defaultLimit: 25, // smaller default page for this resource
131
+ maxLimit: 5000, // allow larger pages here
97
132
  }
98
133
  ```
99
134
 
100
- Neither is required. Without them, page size is unlimited and fully caller-controlled.
135
+ **Disabling pagination.** Set a limit to `0` to remove that bound. `defaultLimit: 0` returns all
136
+ rows when `?limit=` is omitted; `maxLimit: 0` removes the cap. Use both to turn pagination off
137
+ entirely (every request returns the full result set) — handy when migrating an app that has
138
+ always pulled all rows:
139
+
140
+ ```ts
141
+ { defaultLimit: 0, maxLimit: 0 } // no pagination — return everything
142
+ ```
101
143
 
102
144
  ## Query-String Filtering and Pagination
103
145
 
@@ -107,7 +149,7 @@ GET /api/v1/posts?limit=25&offset=0&order=-createdAt&published=true&fields=id,ti
107
149
 
108
150
  | Parameter | Description |
109
151
  | --------------- | ---------------------------------------------------------------------- |
110
- | `limit` | Page size. Capped by `maxLimit`; defaults to `defaultLimit` if set. |
152
+ | `limit` | Page size. Capped by `maxLimit`; defaults to `defaultLimit` (5000). |
111
153
  | `offset` | Number of rows to skip. |
112
154
  | `order` | Comma-separated field names. Prefix `-` for descending. |
113
155
  | `fields` | Comma-separated field names to include in the response. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  The query builder exposes an advanced `POST /:resource/query` endpoint that accepts a structured JSON payload (an AST — abstract syntax tree) describing filters, sorting, pagination, and projection. It lets the front-end compose rich list queries "for free", without the back-end adding custom endpoints. (The path segment defaults to `query`; override it with the `queryBuilderPath` option.)
4
4
 
5
- **Database-agnostic by design.** The payload is fully validated against the resource — every field name, comparison, sort, and nesting depth — and invalid input returns a structured `400`/`422` _before_ any database call. The validated AST is then compiled to portable **Prisma Client** calls (never raw SQL), so the exact same request behaves identically on PostgreSQL, MySQL/MariaDB, SQLite, SQL Server, CockroachDB, and MongoDB. Switch databases by changing only the Prisma `provider` — your client code never changes.
5
+ **Database-agnostic by design.** The payload is fully validated against the resource — every field name, comparison, sort, and nesting depth — and invalid input returns a structured `400`/`422` _before_ any database call. The validated AST is then compiled to portable **Prisma Client** calls (never raw SQL), so the exact same request behaves identically on PostgreSQL, MySQL/MariaDB, SQL Server, CockroachDB, and SQLite. Switch databases by changing only the Prisma `provider` — your client code never changes.
6
6
 
7
7
  It is enabled by default; disable it per-resource with the `allowReadManyWithQueryBuilder` permission:
8
8
 
@@ -31,14 +31,12 @@ Repositories declare what they support through a `capabilities` property. Read i
31
31
 
32
32
  ```ts
33
33
  interface RepositoryCapabilities {
34
- supportsIncludes: boolean // ORM relation loading
35
- supportsTransactions: boolean // transaction wrapping
36
- supportsCreateManyReturn: boolean // createMany returns the created records
37
- supportsQueryAst: boolean // executes the query-builder AST
34
+ supportsIncludes: boolean // ORM relation loading; when false the router rejects ?include= with 422
35
+ supportsCreateManyReturn: boolean // createMany returns the created records (vs. an empty array)
38
36
  }
39
37
  ```
40
38
 
41
- `PrismaAdapter` implements `updateMany` / `deleteMany` / `executeQuery` for every database (they compile to portable Prisma Client calls) and reports `supportsQueryAst: true`.
39
+ `PrismaAdapter` reports `supportsIncludes: true` and `supportsCreateManyReturn: <returnCreated>`. It implements `updateMany` / `deleteMany` / `executeQuery` for every database (they compile to portable Prisma Client calls).
42
40
 
43
41
  ## Prisma 7 Repository Adapter
44
42
 
@@ -156,6 +154,81 @@ new PrismaAdapter({
156
154
 
157
155
  `select` (field projection) and `include` (relation loading) are mutually exclusive in Prisma. The adapter enforces this automatically: when `fields` is specified, it builds a `select` and ignores `include`; when only `include` is specified, it builds an `include`.
158
156
 
157
+ ## Where does the field schema come from?
158
+
159
+ A resource always needs a field schema — it's the allow-list that powers filtering, sorting, projection, and field-level write security. There is no schemaless resource; the router throws at registration if it can't find one. The schema can come from **two places**:
160
+
161
+ 1. **Derived from the model (preferred for Prisma).** When the `PrismaAdapter` knows the model, it exposes `fields`/`relations`/`idField`, and the resource can omit `fields` entirely — or list only the ones it wants to **change** (merged by name as sparse overrides). The zero-config way to get there is `createPrismaResources`, which builds a ready-to-serve resource for every model from Prisma's DMMF:
162
+
163
+ ```ts
164
+ import { Prisma, PrismaClient } from '@prisma/client'
165
+ import { PrismaPg } from '@prisma/adapter-pg'
166
+ import { createPrismaResources, createExpressCrudRouter } from '@edium/halifax'
167
+
168
+ const prisma = new PrismaClient({ adapter: new PrismaPg(process.env.DATABASE_URL!) })
169
+
170
+ // One line → a resource per model, fields and relations derived. No `fields` arrays anywhere.
171
+ const resources = createPrismaResources(prisma, Prisma.dmmf.datamodel.models, {
172
+ // Optional per-model tweaks — the only place you write anything:
173
+ models: {
174
+ AuditLog: { permissions: { allowDeleteOne: false, allowDeleteMany: false } }
175
+ }
176
+ })
177
+
178
+ app.use('/api/v1', createExpressCrudRouter(resources, { authStrategy }))
179
+ ```
180
+
181
+ 2. **Declared on the resource (for custom repositories).** A "bare" adapter — `new PrismaAdapter({ delegate })` with no model, or any **non-Prisma** `Repository` (in-memory, an external API, a different ORM) — has no schema to introspect. There, the resource's `fields` array **is** the schema, and it is required. This is the only situation where you hand-write `fields`, and it's the price of Halifax not having a model to read.
182
+
183
+ **Rule of thumb:** for Prisma, prefer `createPrismaResources` (or pass `model` to the adapter) and declare a field only to override it; reach for a hand-written `fields` array only when the repository genuinely has no schema to offer.
184
+
185
+ ## Prisma 6 (also supported)
186
+
187
+ Halifax's `peerDependencies` allow `@prisma/client >=6.0.0`, so it runs on **Prisma 6 or Prisma 7**. `PrismaAdapter` is database- and version-agnostic: it imports nothing from `@prisma/client` and only calls standard model-delegate methods (`findMany`, `findUnique`, `findFirst`, `create`, `createMany`, `update`, `updateMany`, `delete`, `deleteMany`, `upsert`, `count`) that behave identically across both majors. **You** construct the client and pass `prisma.<model>` as the `delegate` — Halifax never touches the parts that differ between the versions.
188
+
189
+ > **Caveats.** Halifax's CI matrix exercises **Prisma 7 only** — Prisma 6 is supported on the strength of that stable delegate surface, not a dedicated CI leg, so treat it as best-effort and pin/test your own app against it. Prisma 7 is the recommended path; the main reason to stay on (or drop to) Prisma 6 today is **MongoDB**, which Prisma 7 does not yet support. When Prisma 7 restores MongoDB, prefer upgrading over remaining on 6.
190
+
191
+ What you implement differently on Prisma 6 (everything below is your project's Prisma setup — no Halifax code changes):
192
+
193
+ | Concern | Prisma 7 (shown above) | Prisma 6 |
194
+ | ------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
195
+ | Datasource `url` | Forbidden in `schema.prisma`; lives in `prisma.config.ts` | `url = env("DATABASE_URL")` goes **back in the `datasource` block** |
196
+ | `prisma.config.ts` | Required (CLI reads the url from it) | Not used — the CLI reads the url from the schema |
197
+ | Runtime client | **Must** pass a driver adapter (`new PrismaClient({ adapter })`) | Plain `new PrismaClient()` works (built-in engine); driver adapters are opt-in (see below) |
198
+ | Driver adapters | Default, no flag | Behind `previewFeatures = ["driverAdapters"]` in the `generator` block, if you want them |
199
+ | MongoDB | Not supported | **Supported** via the built-in connector — `new PrismaClient()`, `provider = "mongodb"` |
200
+
201
+ A minimal Prisma 6 setup (engine-based client, no adapter):
202
+
203
+ ```prisma
204
+ // prisma/schema.prisma (Prisma 6)
205
+ datasource db {
206
+ provider = "postgresql"
207
+ url = env("DATABASE_URL")
208
+ }
209
+
210
+ generator client {
211
+ provider = "prisma-client-js"
212
+ }
213
+ ```
214
+
215
+ ```ts
216
+ // src/db.ts (Prisma 6 — no driver adapter needed)
217
+ import { PrismaClient } from '@prisma/client'
218
+
219
+ export const prisma = new PrismaClient()
220
+ ```
221
+
222
+ ```ts
223
+ // MongoDB on Prisma 6 — ObjectId keys; Halifax's :id validation already accepts them
224
+ model Post {
225
+ id String @id @default(auto()) @map("_id") @db.ObjectId
226
+ title String
227
+ }
228
+ ```
229
+
230
+ From there, the `PrismaAdapter` usage (`new PrismaAdapter({ delegate: prisma.post })`) and everything else in this guide is identical.
231
+
159
232
  ## Supported Databases
160
233
 
161
234
  The **same `PrismaAdapter`** works with every database Prisma supports — there is no adapter-per-database. All CRUD and the query builder compile to portable Prisma Client calls, so behaviour is identical across engines. To switch databases you change only the Prisma `provider` and driver adapter:
@@ -167,11 +240,10 @@ The **same `PrismaAdapter`** works with every database Prisma supports — there
167
240
  | MySQL / MariaDB | `mysql` | `@prisma/adapter-mariadb` |
168
241
  | SQL Server | `sqlserver` | `@prisma/adapter-mssql` |
169
242
  | SQLite | `sqlite` | `@prisma/adapter-better-sqlite3` |
170
- | MongoDB | `mongodb` | _(built-in connector)_ |
171
243
 
172
- The integration suite runs unchanged against PostgreSQL, MySQL, and SQLite in CI to keep this honest; the others use the same harness (`HALIFAX_DB=<db>`).
244
+ The integration suite runs unchanged against **all six** engines in CI PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite, one matrix leg each (`HALIFAX_DB=<db>`) — so the "behaviour is identical across engines" claim is enforced, not asserted.
173
245
 
174
- **MongoDB note.** Mongo keys are 24-character `ObjectId` strings (`@id @default(auto()) @map("_id") @db.ObjectId`). Halifax's `:id` route validation accepts integers, UUIDs, **and** ObjectIds, so id-based routes work on Mongo out of the box.
246
+ **MongoDB note.** MongoDB is absent from the table above because **Prisma 7 dropped its MongoDB connector** (it's "coming soon" in v7) — and the table/CI matrix target Prisma 7. MongoDB still works **on Prisma 6**, which Halifax also supports (see the Prisma 6 section above) — Mongo keys are 24-character `ObjectId` strings (`@id @default(auto()) @map("_id") @db.ObjectId`), and Halifax's `:id` route validation already accepts integers, UUIDs, **and** ObjectIds, so id-based routes work on Mongo out of the box. The forward-ready `schema.mongodb.prisma` and an ObjectId-aware integration suite rejoin the CI matrix unchanged the moment Prisma 7 supports MongoDB again.
175
247
 
176
248
  ## Targeting database Views
177
249
 
@@ -183,9 +255,6 @@ const activeUsersResource: ResourceDefinition = {
183
255
  routePrefix: 'active-users',
184
256
  fields: [{ name: 'id' }, { name: 'email', filterable: true }],
185
257
  permissions: {
186
- allowReadOne: true,
187
- allowReadMany: true,
188
- allowReadManyWithQueryBuilder: true,
189
258
  allowCreate: false,
190
259
  allowUpdateOne: false,
191
260
  allowUpdateMany: false,
@@ -1,17 +1,46 @@
1
- import type { Express } from 'express';
1
+ import type { Request, Response } from 'express';
2
2
  import { Router } from 'express';
3
3
  import type { HttpMethod, HttpRouteHandler, HttpServer } from '../../core/types.js';
4
4
  import { type CrudApiOptions } from '../../core/crudRouter.js';
5
5
  import type { ResourceDefinition } from '../../core/types.js';
6
- /** Adapts an Express `App` or `Router` to Halifax's {@link HttpServer} interface. */
6
+ /** A route-registration method as exposed by an Express app or router (e.g. `app.get`). */
7
+ type ExpressRouteRegistrar = (path: string, handler: (req: Request, res: Response) => void) => void;
8
+ /**
9
+ * The minimal slice of an Express application or router that Halifax actually drives.
10
+ *
11
+ * Typing against this structural interface — rather than importing the concrete `Express`
12
+ * type from a specific `@types/express` major — is what lets {@link ExpressHttpServer}
13
+ * accept an Express **4** or **5** app/router interchangeably: both versions structurally
14
+ * satisfy it, and the published `.d.ts` no longer pins consumers to one `@types/express`
15
+ * version. The route methods (`get`/`post`/.../`all`) and `listen` are present and
16
+ * signature-compatible across both majors.
17
+ */
18
+ export interface ExpressAppLike {
19
+ get: ExpressRouteRegistrar;
20
+ post: ExpressRouteRegistrar;
21
+ put: ExpressRouteRegistrar;
22
+ patch: ExpressRouteRegistrar;
23
+ delete: ExpressRouteRegistrar;
24
+ all: ExpressRouteRegistrar;
25
+ /** Present on an `App` but not a `Router`; when absent, {@link ExpressHttpServer.start} is a no-op. */
26
+ listen?: (port: number, host: string | undefined, callback: () => void) => unknown;
27
+ }
28
+ /**
29
+ * Adapts an Express `App` or `Router` to Halifax's {@link HttpServer} interface.
30
+ *
31
+ * Works with both Express 4 and Express 5 — the adapter only uses the route methods,
32
+ * `listen`, and the request/response surface that are identical across both majors, and
33
+ * every route path it registers uses plain segments and `:id` named params (never `*`
34
+ * path wildcards), so it sidesteps the Express 5 `path-to-regexp` routing-syntax change.
35
+ */
7
36
  export declare class ExpressHttpServer implements HttpServer {
8
37
  private readonly app;
9
38
  /**
10
- * @param app - Express application or router to register routes on.
39
+ * @param app - Express application or router to register routes on (Express 4 or 5).
11
40
  * When an `App` is provided, `start()` will call `listen()`.
12
41
  * When a `Router` is provided, `start()` is a no-op.
13
42
  */
14
- constructor(app: Express | Router);
43
+ constructor(app: ExpressAppLike);
15
44
  /**
16
45
  * Registers a route on the Express app for the given method and path.
17
46
  * @param method - HTTP method (or `'*'` to register a catch-all via `app.all`).
@@ -37,4 +66,4 @@ export type ExpressCrudRouterOptions = CrudApiOptions;
37
66
  * @returns An Express `Router` ready to mount with `app.use('/api', router)`.
38
67
  */
39
68
  export declare function createExpressCrudRouter(resources: ResourceDefinition[], options?: ExpressCrudRouterOptions): Router;
40
- //# sourceMappingURL=ExpressAdapter.d.ts.map
69
+ export {};