@edium/halifax 1.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -0
- package/README.md +72 -50
- package/README_AUTOCRUD.md +94 -19
- package/README_QUERYBUILDER.md +1 -1
- package/README_REPO_ADAPTERS.md +80 -11
- package/dist/adapters/http/ExpressAdapter.d.ts +34 -5
- package/dist/adapters/http/ExpressAdapter.js +20 -12
- package/dist/adapters/http/FastifyAdapter.d.ts +93 -0
- package/dist/adapters/http/FastifyAdapter.js +125 -0
- package/dist/adapters/http/HyperExpressAdapter.d.ts +82 -0
- package/dist/adapters/http/HyperExpressAdapter.js +128 -0
- package/dist/adapters/http/UltimateExpressAdapter.d.ts +84 -0
- package/dist/adapters/http/UltimateExpressAdapter.js +108 -0
- package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +89 -40
- package/dist/adapters/orm/prisma/PrismaAdapter.js +233 -71
- package/dist/adapters/orm/prisma/astToPrisma.d.ts +26 -0
- package/dist/adapters/orm/prisma/astToPrisma.js +140 -0
- package/dist/adapters/orm/prisma/createPrismaResources.d.ts +1 -2
- package/dist/adapters/orm/prisma/createPrismaResources.js +10 -6
- package/dist/adapters/orm/prisma/helpers.d.ts +0 -1
- package/dist/adapters/orm/prisma/helpers.js +0 -1
- package/dist/adapters/orm/prisma/index.d.ts +1 -2
- package/dist/adapters/orm/prisma/index.js +0 -1
- package/dist/adapters/orm/prisma/types.d.ts +14 -9
- package/dist/adapters/orm/prisma/types.js +0 -1
- package/dist/auth/AuthStrategy.d.ts +0 -9
- package/dist/auth/AuthStrategy.js +0 -7
- package/dist/core/cache/CacheStore.d.ts +25 -0
- package/dist/core/cache/CacheStore.js +1 -0
- package/dist/core/cache/createCachingRepository.d.ts +39 -0
- package/dist/core/cache/createCachingRepository.js +116 -0
- package/dist/core/cache/in-memory/InMemoryCacheStore.d.ts +19 -0
- package/dist/core/cache/in-memory/InMemoryCacheStore.js +34 -0
- package/dist/core/cache/index.d.ts +5 -0
- package/dist/core/cache/index.js +5 -0
- package/dist/core/cache/redis/RedisCacheStore.d.ts +28 -0
- package/dist/core/cache/redis/RedisCacheStore.js +42 -0
- package/dist/core/cache/redis/RedisLikeClient.d.ts +12 -0
- package/dist/core/cache/redis/RedisLikeClient.js +1 -0
- package/dist/core/crudRouter.d.ts +72 -8
- package/dist/core/crudRouter.js +266 -105
- package/dist/core/queryString.d.ts +3 -3
- package/dist/core/queryString.js +16 -7
- package/dist/core/types.d.ts +151 -31
- package/dist/core/types.js +13 -1
- package/dist/core/validation.d.ts +12 -4
- package/dist/core/validation.js +33 -13
- package/dist/enums/SqlComparison.d.ts +13 -3
- package/dist/enums/SqlComparison.js +12 -2
- package/dist/enums/SqlOperator.d.ts +0 -1
- package/dist/enums/SqlOperator.js +0 -1
- package/dist/enums/SqlOrder.d.ts +0 -1
- package/dist/enums/SqlOrder.js +0 -1
- package/dist/errors/AuthenticationError.d.ts +0 -1
- package/dist/errors/AuthenticationError.js +0 -1
- package/dist/errors/AuthorizationError.d.ts +0 -1
- package/dist/errors/AuthorizationError.js +0 -1
- package/dist/errors/BadRequestError.d.ts +0 -1
- package/dist/errors/BadRequestError.js +0 -1
- package/dist/errors/HttpError.d.ts +0 -1
- package/dist/errors/HttpError.js +0 -1
- package/dist/errors/MethodNotAllowedError.d.ts +0 -1
- package/dist/errors/MethodNotAllowedError.js +0 -1
- package/dist/errors/NotAcceptableError.d.ts +0 -1
- package/dist/errors/NotAcceptableError.js +0 -1
- package/dist/errors/NotFoundError.d.ts +0 -1
- package/dist/errors/NotFoundError.js +0 -1
- package/dist/errors/NotImplementedError.d.ts +0 -1
- package/dist/errors/NotImplementedError.js +0 -1
- package/dist/errors/ServerError.d.ts +0 -1
- package/dist/errors/ServerError.js +0 -1
- package/dist/errors/UnprocessableEntityError.d.ts +0 -1
- package/dist/errors/UnprocessableEntityError.js +0 -1
- package/dist/errors/UnsupportedMediaTypeError.d.ts +0 -1
- package/dist/errors/UnsupportedMediaTypeError.js +0 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -3
- package/dist/interfaces/IQueryFilter.d.ts +1 -2
- package/dist/interfaces/IQueryFilter.js +0 -1
- package/dist/interfaces/IQueryOptions.d.ts +9 -9
- package/dist/interfaces/IQueryOptions.js +0 -1
- package/dist/interfaces/ISort.d.ts +0 -1
- package/dist/interfaces/ISort.js +0 -1
- package/package.json +10 -8
- package/dist/adapters/http/ExpressAdapter.d.ts.map +0 -1
- package/dist/adapters/http/ExpressAdapter.js.map +0 -1
- package/dist/adapters/orm/prisma/PrismaAdapter.d.ts.map +0 -1
- package/dist/adapters/orm/prisma/PrismaAdapter.js.map +0 -1
- package/dist/adapters/orm/prisma/createPrismaResources.d.ts.map +0 -1
- package/dist/adapters/orm/prisma/createPrismaResources.js.map +0 -1
- package/dist/adapters/orm/prisma/helpers.d.ts.map +0 -1
- package/dist/adapters/orm/prisma/helpers.js.map +0 -1
- package/dist/adapters/orm/prisma/index.d.ts.map +0 -1
- package/dist/adapters/orm/prisma/index.js.map +0 -1
- package/dist/adapters/orm/prisma/types.d.ts.map +0 -1
- package/dist/adapters/orm/prisma/types.js.map +0 -1
- package/dist/auth/AuthStrategy.d.ts.map +0 -1
- package/dist/auth/AuthStrategy.js.map +0 -1
- package/dist/classes/QueryBuilder.d.ts +0 -33
- package/dist/classes/QueryBuilder.d.ts.map +0 -1
- package/dist/classes/QueryBuilder.js +0 -262
- package/dist/classes/QueryBuilder.js.map +0 -1
- package/dist/core/crudRouter.d.ts.map +0 -1
- package/dist/core/crudRouter.js.map +0 -1
- package/dist/core/queryString.d.ts.map +0 -1
- package/dist/core/queryString.js.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/validation.js.map +0 -1
- package/dist/enums/SqlComparison.d.ts.map +0 -1
- package/dist/enums/SqlComparison.js.map +0 -1
- package/dist/enums/SqlOperator.d.ts.map +0 -1
- package/dist/enums/SqlOperator.js.map +0 -1
- package/dist/enums/SqlOrder.d.ts.map +0 -1
- package/dist/enums/SqlOrder.js.map +0 -1
- package/dist/errors/AuthenticationError.d.ts.map +0 -1
- package/dist/errors/AuthenticationError.js.map +0 -1
- package/dist/errors/AuthorizationError.d.ts.map +0 -1
- package/dist/errors/AuthorizationError.js.map +0 -1
- package/dist/errors/BadRequestError.d.ts.map +0 -1
- package/dist/errors/BadRequestError.js.map +0 -1
- package/dist/errors/HttpError.d.ts.map +0 -1
- package/dist/errors/HttpError.js.map +0 -1
- package/dist/errors/MethodNotAllowedError.d.ts.map +0 -1
- package/dist/errors/MethodNotAllowedError.js.map +0 -1
- package/dist/errors/NotAcceptableError.d.ts.map +0 -1
- package/dist/errors/NotAcceptableError.js.map +0 -1
- package/dist/errors/NotFoundError.d.ts.map +0 -1
- package/dist/errors/NotFoundError.js.map +0 -1
- package/dist/errors/NotImplementedError.d.ts.map +0 -1
- package/dist/errors/NotImplementedError.js.map +0 -1
- package/dist/errors/ServerError.d.ts.map +0 -1
- package/dist/errors/ServerError.js.map +0 -1
- package/dist/errors/UnprocessableEntityError.d.ts.map +0 -1
- package/dist/errors/UnprocessableEntityError.js.map +0 -1
- package/dist/errors/UnsupportedMediaTypeError.d.ts.map +0 -1
- package/dist/errors/UnsupportedMediaTypeError.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/interfaces/IParamQuery.d.ts +0 -8
- package/dist/interfaces/IParamQuery.d.ts.map +0 -1
- package/dist/interfaces/IParamQuery.js +0 -2
- package/dist/interfaces/IParamQuery.js.map +0 -1
- package/dist/interfaces/IQueryFilter.d.ts.map +0 -1
- package/dist/interfaces/IQueryFilter.js.map +0 -1
- package/dist/interfaces/IQueryOptions.d.ts.map +0 -1
- package/dist/interfaces/IQueryOptions.js.map +0 -1
- package/dist/interfaces/ISort.d.ts.map +0 -1
- package/dist/interfaces/ISort.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,102 @@
|
|
|
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.1.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Configurable response envelope.** A new `envelope` option wraps every success response body
|
|
11
|
+
under a single key (e.g. `envelope: 'data'` → `{ "data": <body> }`). Set it API-wide on
|
|
12
|
+
`createExpressCrudRouter`/`registerCrudApi` options, or per resource on `ResourceDefinition`
|
|
13
|
+
(the per-resource setting wins, including an explicit `null`/`''` to opt a single resource out
|
|
14
|
+
of an API-wide envelope). The wrap is uniform across list, single, create/update/upsert, and
|
|
15
|
+
the delete confirmation; **error responses are never enveloped**, keeping one stable error
|
|
16
|
+
contract. Applied at the response boundary (after the cache), so cached payloads are
|
|
17
|
+
envelope-agnostic. Eases adopting Halifax behind clients that expect a legacy `{ data: ... }`
|
|
18
|
+
shape. Defaults to off — fully backward compatible.
|
|
19
|
+
|
|
20
|
+
## [2.0.0]
|
|
21
|
+
|
|
22
|
+
A breaking release with two themes: **permissive, minimal-by-default resource definitions**
|
|
23
|
+
(declare the exceptions, not the boilerplate), and a **full real-database CI matrix** that
|
|
24
|
+
verifies every supported engine for real.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Full real-database CI matrix** — the integration suite now runs against **six** engines,
|
|
29
|
+
each in its own container: PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, and SQLite
|
|
30
|
+
(embedded). Previously only PostgreSQL, MySQL, and SQLite ran in CI.
|
|
31
|
+
- `docker-compose.test.yml` (one service per engine) and `scripts/integration-matrix.sh` /
|
|
32
|
+
`pnpm test:integration:all` to bring the databases up and run the suite against each — the
|
|
33
|
+
same path CI uses, so local and CI runs are identical.
|
|
34
|
+
- Prisma schemas for SQL Server (`schema.mssql.prisma`) and CockroachDB
|
|
35
|
+
(`schema.cockroachdb.prisma`, using `sequence()` ids to stay 32-bit-safe), and the
|
|
36
|
+
`@prisma/adapter-mssql` driver adapter.
|
|
37
|
+
- An ID-kind-aware integration suite: assertions adapt to the engine's key type (integer vs
|
|
38
|
+
MongoDB `ObjectId`), so one suite runs honestly across every engine.
|
|
39
|
+
|
|
40
|
+
### Changed (breaking)
|
|
41
|
+
|
|
42
|
+
- **`ResourceDefinition` is permissive and minimal.** Only `routePrefix`, `repository`, and
|
|
43
|
+
`fields` are required — and `fields` only when the repository exposes no schema of its own.
|
|
44
|
+
- `name` is now **optional** — it defaults to a title-cased form of `routePrefix`
|
|
45
|
+
(`'blog-posts'` → `'Blog Posts'`) and can still be overridden.
|
|
46
|
+
- **`fields` is now optional and override-aware.** When the repository exposes a field schema
|
|
47
|
+
(any `PrismaAdapter` built with a `model`, and everything from `createPrismaResources`), that
|
|
48
|
+
schema is the base and the resource's `fields` are merged over it **by name** as sparse
|
|
49
|
+
overrides — so you list a field only to _change_ it. With a bare adapter, `fields` remains
|
|
50
|
+
the authoritative allow-list.
|
|
51
|
+
- **Field flags are permissive by default.** `filterable`, `sortable`, `selectable`, and now
|
|
52
|
+
**`writable`** all default to `true`. Previously `writable` defaulted to `false` — bodies now
|
|
53
|
+
accept any defined field unless you set `writable: false`. The **primary key is protected**: it
|
|
54
|
+
is non-writable by default (set `writable: true` to opt in).
|
|
55
|
+
- **Page size is bounded by generous defaults — at most 5000 records per request.**
|
|
56
|
+
`defaultLimit: 5000` and `maxLimit: 5000` (exported as `DEFAULT_PAGE_LIMIT` / `MAX_PAGE_LIMIT`)
|
|
57
|
+
apply when a resource sets none — large enough for typical "show everything" UIs, a seatbelt
|
|
58
|
+
against an accidental unbounded scan of a large table. Previously an unset limit returned every
|
|
59
|
+
row, uncapped. The response `count` is always the true total, so a capped page is never a silent
|
|
60
|
+
drop. Set `defaultLimit: 0` to skip the default bound (return all rows when `?limit=` is omitted)
|
|
61
|
+
and `maxLimit: 0` to remove the cap — use both to disable pagination entirely.
|
|
62
|
+
- **`RepositoryCapabilities` trimmed to the two flags that carry their weight:** removed
|
|
63
|
+
`supportsTransactions` (no transaction feature existed) and `supportsQueryAst` (always true;
|
|
64
|
+
implied by the presence of `executeQuery`). `supportsIncludes` now has teeth — the router
|
|
65
|
+
rejects `?include=` with `422` when a repository reports `supportsIncludes: false`. The
|
|
66
|
+
`Repository` interface gained optional `fields` / `relations` / `idField` for schema exposure.
|
|
67
|
+
- **Widened the `@prisma/client` peer dependency to `>=6.0.0`** (was `>=7.0.0`). `PrismaAdapter`
|
|
68
|
+
imports nothing from `@prisma/client` and only calls stable model-delegate methods, so it runs
|
|
69
|
+
unchanged on Prisma 6 or 7. CI exercises Prisma 7 only, so Prisma 6 is best-effort; its main
|
|
70
|
+
draw is MongoDB, which Prisma 7 does not yet support. See README_REPO_ADAPTERS.md for the
|
|
71
|
+
schema/client differences a Prisma 6 project needs.
|
|
72
|
+
|
|
73
|
+
### Removed
|
|
74
|
+
|
|
75
|
+
- **MongoDB** from the advertised supported-database list and the CI matrix. Prisma ORM v7
|
|
76
|
+
dropped MongoDB support ("coming soon in v7"), and the matrix targets Prisma 7. MongoDB still
|
|
77
|
+
works on **Prisma 6** (now also supported — see above). The forward-ready `schema.mongodb.prisma`
|
|
78
|
+
and an `ObjectId`-aware integration suite remain in the repo so MongoDB rejoins the matrix
|
|
79
|
+
unchanged once Prisma 7 restores support.
|
|
80
|
+
- The "PostgreSQL, MySQL, and SQLite run in CI; the rest use the same adapter and test harness"
|
|
81
|
+
documentation caveat — every advertised relational engine is now verified in CI against a real
|
|
82
|
+
database.
|
|
83
|
+
- The deprecated auth aliases `AuthProvider`, `AllowAllAuthProvider`, `ApiKeyAuthProvider`, and
|
|
84
|
+
`PermissionAuthProvider`. Use `AuthStrategy`, `AllowAllAuthStrategy`, `ApiKeyAuthStrategy`, and
|
|
85
|
+
`JwtClaimsAuthStrategy` respectively.
|
|
86
|
+
|
|
87
|
+
### Migration
|
|
88
|
+
|
|
89
|
+
- Resource definitions can be slimmed dramatically: drop `name` (unless you want a specific one),
|
|
90
|
+
drop per-field `filterable`/`sortable`/`selectable`/`writable: true` flags, and drop
|
|
91
|
+
`defaultLimit`/`maxLimit` if 5000/5000 suit you.
|
|
92
|
+
- **If your app relied on list endpoints returning _every_ row** (no limit), set
|
|
93
|
+
`defaultLimit: 0` and `maxLimit: 0` on those resources (or globally via
|
|
94
|
+
`createPrismaResources({ defaultLimit: 0, maxLimit: 0 })`) — otherwise results are now bounded
|
|
95
|
+
at 5000 by default.
|
|
96
|
+
- If you relied on `writable` defaulting to `false`, audit your `fields`: any field that should
|
|
97
|
+
not be client-writable now needs an explicit `writable: false` (the primary key is already
|
|
98
|
+
protected automatically).
|
|
99
|
+
- If you read `capabilities.supportsTransactions` or `capabilities.supportsQueryAst`, remove those
|
|
100
|
+
references (use `typeof repo.executeQuery === 'function'` to detect query-AST support).
|
|
101
|
+
|
|
6
102
|
## [1.0.0]
|
|
7
103
|
|
|
8
104
|
First public release.
|
|
@@ -28,4 +124,5 @@ First public release.
|
|
|
28
124
|
- **Auth & field-level security** — API key, JWT/Bearer, and Passport strategies; per-action
|
|
29
125
|
required permissions; and `filterable`/`sortable`/`selectable`/`writable` field flags.
|
|
30
126
|
|
|
127
|
+
[2.0.0]: https://github.com/splayfee/halifax/releases/tag/v2.0.0
|
|
31
128
|
[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
|
-
- 🗄️ **
|
|
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
|
|
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,
|
|
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,
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
###
|
|
139
|
+
### Run against every database
|
|
111
140
|
|
|
112
141
|
```bash
|
|
113
|
-
docker
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
157
|
+
Or the classic single-Postgres flow with a `.env.test` file:
|
|
130
158
|
|
|
131
159
|
```bash
|
|
132
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
package/README_AUTOCRUD.md
CHANGED
|
@@ -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
|
-
|
|
15
|
-
maxLimit: 200, // requests above this are silently capped
|
|
15
|
+
repository: postRepository,
|
|
16
16
|
fields: [
|
|
17
|
-
{ name: 'id',
|
|
18
|
-
{ name: 'title'
|
|
19
|
-
{ name: 'content'
|
|
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',
|
|
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
|
-
|
|
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,61 @@ fields: [
|
|
|
88
118
|
|
|
89
119
|
## Pagination
|
|
90
120
|
|
|
91
|
-
|
|
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:
|
|
96
|
-
maxLimit:
|
|
130
|
+
defaultLimit: 25, // smaller default page for this resource
|
|
131
|
+
maxLimit: 5000, // allow larger pages here
|
|
97
132
|
}
|
|
98
133
|
```
|
|
99
134
|
|
|
100
|
-
|
|
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
|
+
```
|
|
143
|
+
|
|
144
|
+
## Response Envelope
|
|
145
|
+
|
|
146
|
+
By default every success response is sent **bare** — a list is `{ count, results }`, a single
|
|
147
|
+
record is the object itself, a delete is `{ deleted: true }`. To wrap every success body under a
|
|
148
|
+
single key (handy when adopting Halifax behind a client that expects a legacy `{ data: ... }`
|
|
149
|
+
shape), set `envelope` — API-wide or per resource:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// API-wide: every resource's success body is wrapped under "data"
|
|
153
|
+
createExpressCrudRouter(resources, { authStrategy, envelope: 'data' })
|
|
154
|
+
|
|
155
|
+
// Per resource (wins over the API-wide setting):
|
|
156
|
+
{ routePrefix: 'posts', repository, envelope: 'data' }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The wrap is **uniform** — it nests the entire body, it does not reshape it:
|
|
160
|
+
|
|
161
|
+
| Endpoint | Bare (default) | `envelope: 'data'` |
|
|
162
|
+
| --------------- | ------------------------- | ------------------------------------- |
|
|
163
|
+
| List / query | `{ count, results }` | `{ data: { count, results } }` |
|
|
164
|
+
| Read / create | `{ id, ... }` | `{ data: { id, ... } }` |
|
|
165
|
+
| Delete one | `{ deleted: true }` | `{ data: { deleted: true } }` |
|
|
166
|
+
|
|
167
|
+
Notes:
|
|
168
|
+
|
|
169
|
+
- **Errors are never enveloped** — they always use the `{ errors: [...] }` shape (see below), so
|
|
170
|
+
clients have one stable error contract regardless of the success envelope.
|
|
171
|
+
- **Precedence** — an explicit per-resource `envelope` always wins, including `null` or `''`,
|
|
172
|
+
which opts a single resource out of an API-wide envelope. Omit it to inherit the API default.
|
|
173
|
+
- `null`, `undefined`, and `''` all mean "no envelope" (an empty key is rejected as meaningless).
|
|
174
|
+
- The envelope is applied at the response boundary, after the read-through cache, so cached
|
|
175
|
+
payloads are envelope-agnostic.
|
|
101
176
|
|
|
102
177
|
## Query-String Filtering and Pagination
|
|
103
178
|
|
|
@@ -107,7 +182,7 @@ GET /api/v1/posts?limit=25&offset=0&order=-createdAt&published=true&fields=id,ti
|
|
|
107
182
|
|
|
108
183
|
| Parameter | Description |
|
|
109
184
|
| --------------- | ---------------------------------------------------------------------- |
|
|
110
|
-
| `limit` | Page size. Capped by `maxLimit`; defaults to `defaultLimit`
|
|
185
|
+
| `limit` | Page size. Capped by `maxLimit`; defaults to `defaultLimit` (5000). |
|
|
111
186
|
| `offset` | Number of rows to skip. |
|
|
112
187
|
| `order` | Comma-separated field names. Prefix `-` for descending. |
|
|
113
188
|
| `fields` | Comma-separated field names to include in the response. |
|
package/README_QUERYBUILDER.md
CHANGED
|
@@ -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,
|
|
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
|
|
package/README_REPO_ADAPTERS.md
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
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`)
|
|
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,
|