@edium/halifax 1.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 (135) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +148 -0
  4. package/README_AUTH.md +172 -0
  5. package/README_AUTOCRUD.md +253 -0
  6. package/README_CACHE.md +164 -0
  7. package/README_HTTP_ADAPTERS.md +309 -0
  8. package/README_MULTITENANCY.md +162 -0
  9. package/README_QUERYBUILDER.md +219 -0
  10. package/README_REPO_ADAPTERS.md +266 -0
  11. package/dist/adapters/http/ExpressAdapter.d.ts +40 -0
  12. package/dist/adapters/http/ExpressAdapter.d.ts.map +1 -0
  13. package/dist/adapters/http/ExpressAdapter.js +109 -0
  14. package/dist/adapters/http/ExpressAdapter.js.map +1 -0
  15. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +143 -0
  16. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts.map +1 -0
  17. package/dist/adapters/orm/prisma/PrismaAdapter.js +277 -0
  18. package/dist/adapters/orm/prisma/PrismaAdapter.js.map +1 -0
  19. package/dist/adapters/orm/prisma/createPrismaResources.d.ts +15 -0
  20. package/dist/adapters/orm/prisma/createPrismaResources.d.ts.map +1 -0
  21. package/dist/adapters/orm/prisma/createPrismaResources.js +51 -0
  22. package/dist/adapters/orm/prisma/createPrismaResources.js.map +1 -0
  23. package/dist/adapters/orm/prisma/helpers.d.ts +27 -0
  24. package/dist/adapters/orm/prisma/helpers.d.ts.map +1 -0
  25. package/dist/adapters/orm/prisma/helpers.js +45 -0
  26. package/dist/adapters/orm/prisma/helpers.js.map +1 -0
  27. package/dist/adapters/orm/prisma/index.d.ts +4 -0
  28. package/dist/adapters/orm/prisma/index.d.ts.map +1 -0
  29. package/dist/adapters/orm/prisma/index.js +3 -0
  30. package/dist/adapters/orm/prisma/index.js.map +1 -0
  31. package/dist/adapters/orm/prisma/types.d.ts +49 -0
  32. package/dist/adapters/orm/prisma/types.d.ts.map +1 -0
  33. package/dist/adapters/orm/prisma/types.js +2 -0
  34. package/dist/adapters/orm/prisma/types.js.map +1 -0
  35. package/dist/auth/AuthStrategy.d.ts +198 -0
  36. package/dist/auth/AuthStrategy.d.ts.map +1 -0
  37. package/dist/auth/AuthStrategy.js +227 -0
  38. package/dist/auth/AuthStrategy.js.map +1 -0
  39. package/dist/classes/QueryBuilder.d.ts +33 -0
  40. package/dist/classes/QueryBuilder.d.ts.map +1 -0
  41. package/dist/classes/QueryBuilder.js +262 -0
  42. package/dist/classes/QueryBuilder.js.map +1 -0
  43. package/dist/core/crudRouter.d.ts +36 -0
  44. package/dist/core/crudRouter.d.ts.map +1 -0
  45. package/dist/core/crudRouter.js +391 -0
  46. package/dist/core/crudRouter.js.map +1 -0
  47. package/dist/core/queryString.d.ts +13 -0
  48. package/dist/core/queryString.d.ts.map +1 -0
  49. package/dist/core/queryString.js +89 -0
  50. package/dist/core/queryString.js.map +1 -0
  51. package/dist/core/types.d.ts +293 -0
  52. package/dist/core/types.d.ts.map +1 -0
  53. package/dist/core/types.js +13 -0
  54. package/dist/core/types.js.map +1 -0
  55. package/dist/core/validation.d.ts +75 -0
  56. package/dist/core/validation.d.ts.map +1 -0
  57. package/dist/core/validation.js +206 -0
  58. package/dist/core/validation.js.map +1 -0
  59. package/dist/enums/SqlComparison.d.ts +18 -0
  60. package/dist/enums/SqlComparison.d.ts.map +1 -0
  61. package/dist/enums/SqlComparison.js +19 -0
  62. package/dist/enums/SqlComparison.js.map +1 -0
  63. package/dist/enums/SqlOperator.d.ts +6 -0
  64. package/dist/enums/SqlOperator.d.ts.map +1 -0
  65. package/dist/enums/SqlOperator.js +7 -0
  66. package/dist/enums/SqlOperator.js.map +1 -0
  67. package/dist/enums/SqlOrder.d.ts +6 -0
  68. package/dist/enums/SqlOrder.d.ts.map +1 -0
  69. package/dist/enums/SqlOrder.js +7 -0
  70. package/dist/enums/SqlOrder.js.map +1 -0
  71. package/dist/errors/AuthenticationError.d.ts +10 -0
  72. package/dist/errors/AuthenticationError.d.ts.map +1 -0
  73. package/dist/errors/AuthenticationError.js +13 -0
  74. package/dist/errors/AuthenticationError.js.map +1 -0
  75. package/dist/errors/AuthorizationError.d.ts +10 -0
  76. package/dist/errors/AuthorizationError.d.ts.map +1 -0
  77. package/dist/errors/AuthorizationError.js +13 -0
  78. package/dist/errors/AuthorizationError.js.map +1 -0
  79. package/dist/errors/BadRequestError.d.ts +10 -0
  80. package/dist/errors/BadRequestError.d.ts.map +1 -0
  81. package/dist/errors/BadRequestError.js +13 -0
  82. package/dist/errors/BadRequestError.js.map +1 -0
  83. package/dist/errors/HttpError.d.ts +12 -0
  84. package/dist/errors/HttpError.d.ts.map +1 -0
  85. package/dist/errors/HttpError.js +17 -0
  86. package/dist/errors/HttpError.js.map +1 -0
  87. package/dist/errors/MethodNotAllowedError.d.ts +10 -0
  88. package/dist/errors/MethodNotAllowedError.d.ts.map +1 -0
  89. package/dist/errors/MethodNotAllowedError.js +13 -0
  90. package/dist/errors/MethodNotAllowedError.js.map +1 -0
  91. package/dist/errors/NotAcceptableError.d.ts +10 -0
  92. package/dist/errors/NotAcceptableError.d.ts.map +1 -0
  93. package/dist/errors/NotAcceptableError.js +13 -0
  94. package/dist/errors/NotAcceptableError.js.map +1 -0
  95. package/dist/errors/NotFoundError.d.ts +10 -0
  96. package/dist/errors/NotFoundError.d.ts.map +1 -0
  97. package/dist/errors/NotFoundError.js +13 -0
  98. package/dist/errors/NotFoundError.js.map +1 -0
  99. package/dist/errors/NotImplementedError.d.ts +10 -0
  100. package/dist/errors/NotImplementedError.d.ts.map +1 -0
  101. package/dist/errors/NotImplementedError.js +13 -0
  102. package/dist/errors/NotImplementedError.js.map +1 -0
  103. package/dist/errors/ServerError.d.ts +10 -0
  104. package/dist/errors/ServerError.d.ts.map +1 -0
  105. package/dist/errors/ServerError.js +13 -0
  106. package/dist/errors/ServerError.js.map +1 -0
  107. package/dist/errors/UnprocessableEntityError.d.ts +10 -0
  108. package/dist/errors/UnprocessableEntityError.d.ts.map +1 -0
  109. package/dist/errors/UnprocessableEntityError.js +13 -0
  110. package/dist/errors/UnprocessableEntityError.js.map +1 -0
  111. package/dist/errors/UnsupportedMediaTypeError.d.ts +10 -0
  112. package/dist/errors/UnsupportedMediaTypeError.d.ts.map +1 -0
  113. package/dist/errors/UnsupportedMediaTypeError.js +13 -0
  114. package/dist/errors/UnsupportedMediaTypeError.js.map +1 -0
  115. package/dist/index.d.ts +27 -0
  116. package/dist/index.d.ts.map +1 -0
  117. package/dist/index.js +27 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/interfaces/IParamQuery.d.ts +8 -0
  120. package/dist/interfaces/IParamQuery.d.ts.map +1 -0
  121. package/dist/interfaces/IParamQuery.js +2 -0
  122. package/dist/interfaces/IParamQuery.js.map +1 -0
  123. package/dist/interfaces/IQueryFilter.d.ts +18 -0
  124. package/dist/interfaces/IQueryFilter.d.ts.map +1 -0
  125. package/dist/interfaces/IQueryFilter.js +2 -0
  126. package/dist/interfaces/IQueryFilter.js.map +1 -0
  127. package/dist/interfaces/IQueryOptions.d.ts +20 -0
  128. package/dist/interfaces/IQueryOptions.d.ts.map +1 -0
  129. package/dist/interfaces/IQueryOptions.js +2 -0
  130. package/dist/interfaces/IQueryOptions.js.map +1 -0
  131. package/dist/interfaces/ISort.d.ts +9 -0
  132. package/dist/interfaces/ISort.d.ts.map +1 -0
  133. package/dist/interfaces/ISort.js +2 -0
  134. package/dist/interfaces/ISort.js.map +1 -0
  135. package/package.json +169 -0
@@ -0,0 +1,219 @@
1
+ # Query Builder
2
+
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
+
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.
6
+
7
+ It is enabled by default; disable it per-resource with the `allowReadManyWithQueryBuilder` permission:
8
+
9
+ ```ts
10
+ permissions: {
11
+ allowReadManyWithQueryBuilder: false,
12
+ }
13
+ ```
14
+
15
+ ## Full Payload Reference
16
+
17
+ ```ts
18
+ interface QueryPayload {
19
+ fields?: string[] // columns to return (default: all)
20
+ where?: QueryFilter[] // filter conditions
21
+ orderBy?: SortEntry[] // sort order
22
+ limit?: number // page size
23
+ offset?: number // rows to skip
24
+ distinct?: string[] // return rows distinct on these columns
25
+ }
26
+ ```
27
+
28
+ > The query targets the resource's own model — there is no table name in the payload, so a caller can never point the query at another table.
29
+
30
+ ### `fields`
31
+
32
+ An optional array of column names to include in the response. Omit it to return all columns (`SELECT *`).
33
+
34
+ Fields are validated against the resource's field definitions. A field with `selectable: false` returns 400.
35
+
36
+ ```json
37
+ { "fields": ["id", "title", "published"] }
38
+ ```
39
+
40
+ ### `where`
41
+
42
+ An array of filter conditions. Each entry is a `QueryFilter`:
43
+
44
+ ```ts
45
+ interface QueryFilter {
46
+ field: string // column name
47
+ comparison: string // see comparisons table
48
+ value1?: scalar | scalar[] // primary value (omit for IS NULL / IS NOT NULL)
49
+ value2?: scalar // second value for BETWEEN / NOT BETWEEN
50
+ operator?: 'AND' | 'OR' // required for all entries except the last
51
+ children?: QueryFilter[] // nested group (produces parenthesised sub-clause)
52
+ }
53
+
54
+ type scalar = string | number | boolean | null
55
+ ```
56
+
57
+ Fields are validated against the resource's field definitions. A field with `filterable: false` returns 400.
58
+
59
+ #### Comparisons
60
+
61
+ | `comparison` | Prisma mapping | `value1` | `value2` |
62
+ | ------------- | --------------------------------------- | -------------------------- | -------- |
63
+ | `=` | `{ equals }` | scalar | — |
64
+ | `<>` | `{ not }` | scalar | — |
65
+ | `>` | `{ gt }` | scalar | — |
66
+ | `>=` | `{ gte }` | scalar | — |
67
+ | `<` | `{ lt }` | scalar | — |
68
+ | `<=` | `{ lte }` | scalar | — |
69
+ | `LIKE` | `{ contains / startsWith / endsWith }`† | string (use `%` wildcards) | — |
70
+ | `NOT LIKE` | `{ NOT: … }` | string | — |
71
+ | `CONTAINS` | `{ contains }` | string | — |
72
+ | `STARTS WITH` | `{ startsWith }` | string | — |
73
+ | `ENDS WITH` | `{ endsWith }` | string | — |
74
+ | `IN` | `{ in }` | array of scalars | — |
75
+ | `NOT IN` | `{ notIn }` | array of scalars | — |
76
+ | `BETWEEN` | `{ gte, lte }` | scalar | scalar |
77
+ | `NOT BETWEEN` | `{ OR: [{ lt }, { gt }] }` | scalar | scalar |
78
+ | `IS NULL` | `field: null` | — | — |
79
+ | `IS NOT NULL` | `{ not: null }` | — | — |
80
+
81
+ † `LIKE` parses `%` wildcards: `%x%` → `contains`, `x%` → `startsWith`, `%x` → `endsWith`, and a wildcard-free value collapses to `equals`. The portable `CONTAINS` / `STARTS WITH` / `ENDS WITH` operators (supported by both Prisma and Drizzle) are the recommended, dialect-independent way to do substring matching — unlike SQL `LIKE`, whose case-sensitivity varies by engine collation.
82
+
83
+ #### Examples
84
+
85
+ Simple equality:
86
+
87
+ ```json
88
+ { "field": "published", "comparison": "=", "value1": true }
89
+ ```
90
+
91
+ Multi-condition with AND:
92
+
93
+ ```json
94
+ [
95
+ { "field": "published", "comparison": "=", "value1": true, "operator": "AND" },
96
+ { "field": "title", "comparison": "LIKE", "value1": "%typescript%" }
97
+ ]
98
+ ```
99
+
100
+ IN list:
101
+
102
+ ```json
103
+ { "field": "authorId", "comparison": "IN", "value1": [1, 2, 3] }
104
+ ```
105
+
106
+ NULL check:
107
+
108
+ ```json
109
+ { "field": "deletedAt", "comparison": "IS NULL" }
110
+ ```
111
+
112
+ Range:
113
+
114
+ ```json
115
+ { "field": "score", "comparison": "BETWEEN", "value1": 10, "value2": 100 }
116
+ ```
117
+
118
+ #### Nested filters (parenthesised groups)
119
+
120
+ Use `children` to wrap a group of conditions in parentheses:
121
+
122
+ ```json
123
+ {
124
+ "where": [
125
+ { "field": "published", "comparison": "=", "value1": true, "operator": "AND" },
126
+ {
127
+ "operator": "OR",
128
+ "children": [
129
+ { "field": "authorId", "comparison": "=", "value1": 1, "operator": "OR" },
130
+ { "field": "authorId", "comparison": "=", "value1": 2 }
131
+ ]
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ Produces: `WHERE published = $1 AND (authorId = $2 OR authorId = $3)`
138
+
139
+ ### `orderBy`
140
+
141
+ An array of sort directives. Each entry:
142
+
143
+ ```ts
144
+ interface SortEntry {
145
+ field: string // column name — must be a defined, sortable resource field
146
+ order: 'ASC' | 'DESC'
147
+ }
148
+ ```
149
+
150
+ Fields with `sortable: false` return 400. When `orderBy` is omitted the query defaults to `ORDER BY id ASC`.
151
+
152
+ ```json
153
+ {
154
+ "orderBy": [
155
+ { "field": "createdAt", "order": "DESC" },
156
+ { "field": "id", "order": "ASC" }
157
+ ]
158
+ }
159
+ ```
160
+
161
+ ### `limit`
162
+
163
+ Maximum number of rows to return. If the resource has a `maxLimit` configured, requests above that value are silently capped.
164
+
165
+ ```json
166
+ { "limit": 25 }
167
+ ```
168
+
169
+ ### `offset`
170
+
171
+ Number of rows to skip before returning results. Use with `limit` for pagination.
172
+
173
+ ```json
174
+ { "limit": 25, "offset": 50 }
175
+ ```
176
+
177
+ Pagination maps to Prisma `take`/`skip`, so it works identically on every supported database.
178
+
179
+ ### `distinct`
180
+
181
+ Return only rows distinct on the given columns (maps to Prisma `distinct`; supported by both Prisma and Drizzle). Columns are validated against the resource like any other field.
182
+
183
+ ```json
184
+ { "distinct": ["authorId"], "fields": ["authorId"] }
185
+ ```
186
+
187
+ ## Full Example
188
+
189
+ ```json
190
+ POST /api/v1/posts/query
191
+ {
192
+ "fields": ["id", "title", "authorId", "createdAt"],
193
+ "where": [
194
+ { "field": "published", "comparison": "=", "value1": true, "operator": "AND" },
195
+ { "field": "title", "comparison": "CONTAINS", "value1": "api" }
196
+ ],
197
+ "orderBy": [{ "field": "createdAt", "order": "DESC" }],
198
+ "limit": 25,
199
+ "offset": 0
200
+ }
201
+ ```
202
+
203
+ ## Response
204
+
205
+ ```json
206
+ {
207
+ "count": 142,
208
+ "results": [
209
+ {
210
+ "id": 7,
211
+ "title": "Building APIs with Halifax",
212
+ "authorId": 2,
213
+ "createdAt": "2025-01-15T10:00:00.000Z"
214
+ }
215
+ ]
216
+ }
217
+ ```
218
+
219
+ `count` is the total number of matching rows (before pagination), not the length of `results`.
@@ -0,0 +1,266 @@
1
+ # Repositories
2
+
3
+ A repository is the data-access layer Halifax talks to. All adapters implement the same `Repository` interface, so you can swap ORM/database without touching your routes or auth.
4
+
5
+ ## The Repository Interface
6
+
7
+ ```ts
8
+ interface Repository<TRecord, TCreate, TUpdate> {
9
+ readonly capabilities?: Partial<RepositoryCapabilities>
10
+
11
+ getOne(
12
+ id: string | number,
13
+ options?: { fields?: string[]; include?: string[] }
14
+ ): Promise<TRecord | null>
15
+ getMany(options?: ListOptions): Promise<ListResult<TRecord>>
16
+ createOne(data: TCreate): Promise<TRecord>
17
+ createMany(data: TCreate[]): Promise<TRecord[]>
18
+ updateOne(id: string | number, data: TUpdate): Promise<TRecord | null>
19
+ deleteOne(id: string | number): Promise<boolean>
20
+
21
+ // Optional bulk / query-builder operations (PrismaAdapter implements all three)
22
+ updateMany?(query: IQueryOptions, data: TUpdate): Promise<UpdateManyResult<TRecord>>
23
+ deleteMany?(query: IQueryOptions): Promise<DeleteManyResult>
24
+ executeQuery?(query: IQueryOptions): Promise<QueryResult<TRecord>>
25
+ }
26
+ ```
27
+
28
+ ## Repository Capabilities
29
+
30
+ Repositories declare what they support through a `capabilities` property. Read it to make runtime decisions without guessing:
31
+
32
+ ```ts
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
38
+ }
39
+ ```
40
+
41
+ `PrismaAdapter` implements `updateMany` / `deleteMany` / `executeQuery` for every database (they compile to portable Prisma Client calls) and reports `supportsQueryAst: true`.
42
+
43
+ ## Prisma 7 Repository Adapter
44
+
45
+ ### 1. Define your Prisma schema
46
+
47
+ With Prisma 7, the datasource block no longer accepts a `url` property. The URL goes in `prisma.config.ts` for CLI tools, and in a driver adapter for the runtime client.
48
+
49
+ ```prisma
50
+ // prisma/schema.prisma
51
+ datasource db {
52
+ provider = "postgresql"
53
+ }
54
+
55
+ generator client {
56
+ provider = "prisma-client-js"
57
+ }
58
+
59
+ model Post {
60
+ id Int @id @default(autoincrement())
61
+ title String
62
+ content String?
63
+ published Boolean @default(false)
64
+ authorId Int?
65
+ author Author? @relation(fields: [authorId], references: [id])
66
+ createdAt DateTime @default(now())
67
+ updatedAt DateTime @updatedAt
68
+
69
+ @@map("posts")
70
+ }
71
+
72
+ model Author {
73
+ id Int @id @default(autoincrement())
74
+ name String
75
+ email String @unique
76
+ posts Post[]
77
+
78
+ @@map("authors")
79
+ }
80
+ ```
81
+
82
+ Create `prisma.config.ts` at the project root so `prisma generate`, `prisma migrate`, and `prisma db push` can find the database URL:
83
+
84
+ ```ts
85
+ // prisma.config.ts
86
+ import { defineConfig } from 'prisma/config'
87
+
88
+ export default defineConfig({
89
+ datasource: { url: process.env.DATABASE_URL }
90
+ })
91
+ ```
92
+
93
+ ```bash
94
+ pnpm exec prisma generate
95
+ pnpm exec prisma migrate dev --name init
96
+ ```
97
+
98
+ ### 2. Create the Prisma client
99
+
100
+ Prisma 7 requires a driver adapter at runtime. Install `@prisma/adapter-pg` for PostgreSQL:
101
+
102
+ ```bash
103
+ pnpm add @prisma/adapter-pg pg
104
+ pnpm add -D @types/pg
105
+ ```
106
+
107
+ ```ts
108
+ // src/db.ts
109
+ import { PrismaClient } from '@prisma/client'
110
+ import { PrismaPg } from '@prisma/adapter-pg'
111
+
112
+ const adapter = new PrismaPg(process.env.DATABASE_URL!)
113
+ export const prisma = new PrismaClient({ adapter })
114
+ ```
115
+
116
+ ### 3. Create a `PrismaAdapter`
117
+
118
+ ```ts
119
+ import { PrismaAdapter } from '@edium/halifax'
120
+ import type { Post, Prisma } from '@prisma/client'
121
+ import { prisma } from './db.js'
122
+
123
+ export const postRepository = new PrismaAdapter<
124
+ Post,
125
+ Prisma.PostCreateInput,
126
+ Prisma.PostUpdateInput
127
+ >({
128
+ delegate: prisma.post // no cast needed
129
+ })
130
+ ```
131
+
132
+ Just the model delegate — CRUD, bulk operations, and the query builder all run through it.
133
+
134
+ #### Options
135
+
136
+ | Option | Type | Required | Description |
137
+ | --------------- | --------- | -------- | -------------------------------------------------------------------- |
138
+ | `delegate` | `any` | yes | The Prisma model delegate (`prisma.post`, `prisma.user`, …) |
139
+ | `idField` | `string` | no | Primary key field name (default: `"id"`) |
140
+ | `returnCreated` | `boolean` | no | When `true`, `createMany` returns created records (default: `false`) |
141
+
142
+ #### `createMany` and returned records
143
+
144
+ By default, `createMany` uses Prisma's bulk insert for efficiency but returns an empty array because Prisma's `createMany` does not return the created rows. Set `returnCreated: true` to fall back to serial `createOne` calls and receive the full records:
145
+
146
+ ```ts
147
+ new PrismaAdapter({
148
+ delegate: prisma.post,
149
+ returnCreated: true // slower, but returns created records
150
+ })
151
+ ```
152
+
153
+ `capabilities.supportsCreateManyReturn` reflects this setting.
154
+
155
+ ### `select` vs `include`
156
+
157
+ `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
+
159
+ ## Supported Databases
160
+
161
+ 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:
162
+
163
+ | Database | Prisma `provider` | Driver adapter |
164
+ | --------------- | ----------------- | -------------------------------- |
165
+ | PostgreSQL | `postgresql` | `@prisma/adapter-pg` |
166
+ | CockroachDB | `cockroachdb` | `@prisma/adapter-pg` |
167
+ | MySQL / MariaDB | `mysql` | `@prisma/adapter-mariadb` |
168
+ | SQL Server | `sqlserver` | `@prisma/adapter-mssql` |
169
+ | SQLite | `sqlite` | `@prisma/adapter-better-sqlite3` |
170
+ | MongoDB | `mongodb` | _(built-in connector)_ |
171
+
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>`).
173
+
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.
175
+
176
+ ## Targeting database Views
177
+
178
+ A database **view is just a model** to Halifax. Prisma exposes a `view` block as a delegate with the same read API as a model (`prisma.activeUsers.findMany()`), so you point a resource's repository at it and disable writes:
179
+
180
+ ```ts
181
+ const activeUsersResource: ResourceDefinition = {
182
+ name: 'ActiveUser',
183
+ routePrefix: 'active-users',
184
+ fields: [{ name: 'id' }, { name: 'email', filterable: true }],
185
+ permissions: {
186
+ allowReadOne: true,
187
+ allowReadMany: true,
188
+ allowReadManyWithQueryBuilder: true,
189
+ allowCreate: false,
190
+ allowUpdateOne: false,
191
+ allowUpdateMany: false,
192
+ allowUpsertOne: false,
193
+ allowDeleteOne: false,
194
+ allowDeleteMany: false
195
+ },
196
+ repository: new PrismaAdapter({ delegate: prisma.activeUsers })
197
+ }
198
+ ```
199
+
200
+ No adapter changes are needed — reads, filtering, sorting, pagination, and the query builder all work against the view. (Drizzle views behave the same way.)
201
+
202
+ ## Caching
203
+
204
+ Any resource can be served through a pluggable read-through cache (in-memory or Redis), with
205
+ per-resource TTLs, a never-expire mode, automatic write-invalidation, tenant-safe keys, and a
206
+ cache-bust header:
207
+
208
+ ```ts
209
+ const postResource: ResourceDefinition = {
210
+ /* … */
211
+ cache: { ttlSeconds: 60 } // cache reads for 60s; writes invalidate automatically
212
+ }
213
+ ```
214
+
215
+ See **[README_CACHE.md](./README_CACHE.md)** for in-memory and Redis examples, the
216
+ never-expire (`ttlSeconds: 0`) and `cache: false` options, and the `Cache-Control: no-cache`
217
+ bust header.
218
+
219
+ ## Implementing a Custom Repository
220
+
221
+ Any class or object that satisfies the `Repository` interface works:
222
+
223
+ ```ts
224
+ import type { Repository, ListResult } from '@edium/halifax'
225
+
226
+ export class InMemoryRepository<T extends { id: number }> implements Repository<
227
+ T,
228
+ Omit<T, 'id'>,
229
+ Partial<T>
230
+ > {
231
+ private records: T[] = []
232
+ private nextId = 1
233
+
234
+ async getOne(id: string | number) {
235
+ return this.records.find((r) => r.id === Number(id)) ?? null
236
+ }
237
+
238
+ async getMany(): Promise<ListResult<T>> {
239
+ return { count: this.records.length, results: [...this.records] }
240
+ }
241
+
242
+ async createOne(data: Omit<T, 'id'>) {
243
+ const record = { id: this.nextId++, ...data } as T
244
+ this.records.push(record)
245
+ return record
246
+ }
247
+
248
+ async createMany(data: Omit<T, 'id'>[]) {
249
+ return Promise.all(data.map((d) => this.createOne(d)))
250
+ }
251
+
252
+ async updateOne(id: string | number, data: Partial<T>) {
253
+ const record = this.records.find((r) => r.id === Number(id))
254
+ if (!record) return null
255
+ Object.assign(record, data)
256
+ return record
257
+ }
258
+
259
+ async deleteOne(id: string | number) {
260
+ const idx = this.records.findIndex((r) => r.id === Number(id))
261
+ if (idx === -1) return false
262
+ this.records.splice(idx, 1)
263
+ return true
264
+ }
265
+ }
266
+ ```
@@ -0,0 +1,40 @@
1
+ import type { Express } from 'express';
2
+ import { Router } from 'express';
3
+ import type { HttpMethod, HttpRouteHandler, HttpServer } from '../../core/types.js';
4
+ import { type CrudApiOptions } from '../../core/crudRouter.js';
5
+ import type { ResourceDefinition } from '../../core/types.js';
6
+ /** Adapts an Express `App` or `Router` to Halifax's {@link HttpServer} interface. */
7
+ export declare class ExpressHttpServer implements HttpServer {
8
+ private readonly app;
9
+ /**
10
+ * @param app - Express application or router to register routes on.
11
+ * When an `App` is provided, `start()` will call `listen()`.
12
+ * When a `Router` is provided, `start()` is a no-op.
13
+ */
14
+ constructor(app: Express | Router);
15
+ /**
16
+ * Registers a route on the Express app for the given method and path.
17
+ * @param method - HTTP method (or `'*'` to register a catch-all via `app.all`).
18
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
19
+ * @param handler - Halifax route handler to invoke on matching requests.
20
+ */
21
+ registerRoute(method: HttpMethod, path: string, handler: HttpRouteHandler): void;
22
+ /**
23
+ * Starts the Express server. No-op when the underlying app does not expose a `listen` method
24
+ * (e.g. when `app` is a `Router` mounted on an existing Express app).
25
+ * @param port - TCP port number to bind to.
26
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
27
+ */
28
+ start(port: number, host?: string): Promise<void>;
29
+ }
30
+ /** Options for {@link createExpressCrudRouter}. Alias of {@link CrudApiOptions}. */
31
+ export type ExpressCrudRouterOptions = CrudApiOptions;
32
+ /**
33
+ * Creates a fully-wired Express `Router` with CRUD routes for every resource.
34
+ *
35
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
36
+ * @param options - Auth strategy, query-builder path overrides, etc.
37
+ * @returns An Express `Router` ready to mount with `app.use('/api', router)`.
38
+ */
39
+ export declare function createExpressCrudRouter(resources: ResourceDefinition[], options?: ExpressCrudRouterOptions): Router;
40
+ //# sourceMappingURL=ExpressAdapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpressAdapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/http/ExpressAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAA;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,KAAK,EACV,UAAU,EAGV,gBAAgB,EAChB,UAAU,EACX,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAuDzD,qFAAqF;AACrF,qBAAa,iBAAkB,YAAW,UAAU;IAM/B,OAAO,CAAC,QAAQ,CAAC,GAAG;IALvC;;;;OAIG;gBACiC,GAAG,EAAE,OAAO,GAAG,MAAM;IAEzD;;;;;OAKG;IACI,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAWvF;;;;;OAKG;IACU,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAS/D;AAED,oFAAoF;AACpF,MAAM,MAAM,wBAAwB,GAAG,cAAc,CAAA;AAErD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,kBAAkB,EAAE,EAC/B,OAAO,GAAE,wBAA6B,GACrC,MAAM,CAIR"}
@@ -0,0 +1,109 @@
1
+ import { Router } from 'express';
2
+ import { registerCrudApi } from '../../core/crudRouter.js';
3
+ /**
4
+ * Casts Express's header map to Halifax's `Record<string, string | string[] | undefined>` type.
5
+ * Express guarantees header names are lowercase and values are strings or string arrays,
6
+ * so this cast is safe.
7
+ * @param headers - The raw headers object from an Express `Request`.
8
+ * @returns The same object typed as Halifax's header record.
9
+ */
10
+ function normalizeHeaders(headers) {
11
+ return headers;
12
+ }
13
+ /**
14
+ * Wraps an Express `Request` in Halifax's framework-agnostic {@link HttpRequest}.
15
+ * @param req - The Express request to adapt.
16
+ * @returns A Halifax-compatible {@link HttpRequest} with the original request in `raw`.
17
+ */
18
+ function adaptRequest(req) {
19
+ return {
20
+ method: req.method,
21
+ params: req.params,
22
+ query: req.query,
23
+ body: req.body,
24
+ headers: normalizeHeaders(req.headers),
25
+ raw: req
26
+ };
27
+ }
28
+ /**
29
+ * Wraps an Express `Response` in Halifax's framework-agnostic {@link HttpResponse}.
30
+ * @param res - The Express response to adapt.
31
+ * @returns A Halifax-compatible {@link HttpResponse} with the original response in `raw`.
32
+ */
33
+ function adaptResponse(res) {
34
+ return {
35
+ raw: res,
36
+ status(code) {
37
+ res.status(code);
38
+ return this;
39
+ },
40
+ json(payload) {
41
+ res.json(payload);
42
+ },
43
+ send(payload) {
44
+ res.send(payload);
45
+ },
46
+ setHeader(name, value) {
47
+ res.setHeader(name, value);
48
+ }
49
+ };
50
+ }
51
+ /** Adapts an Express `App` or `Router` to Halifax's {@link HttpServer} interface. */
52
+ export class ExpressHttpServer {
53
+ app;
54
+ /**
55
+ * @param app - Express application or router to register routes on.
56
+ * When an `App` is provided, `start()` will call `listen()`.
57
+ * When a `Router` is provided, `start()` is a no-op.
58
+ */
59
+ constructor(app) {
60
+ this.app = app;
61
+ }
62
+ /**
63
+ * Registers a route on the Express app for the given method and path.
64
+ * @param method - HTTP method (or `'*'` to register a catch-all via `app.all`).
65
+ * @param path - Route path pattern (e.g. `'/users/:id'`).
66
+ * @param handler - Halifax route handler to invoke on matching requests.
67
+ */
68
+ registerRoute(method, path, handler) {
69
+ const cb = (req, res) => {
70
+ void Promise.resolve(handler(adaptRequest(req), adaptResponse(res)));
71
+ };
72
+ if (method === '*') {
73
+ ;
74
+ this.app.all(path, cb);
75
+ return;
76
+ }
77
+ ;
78
+ this.app[method.toLowerCase()](path, cb);
79
+ }
80
+ /**
81
+ * Starts the Express server. No-op when the underlying app does not expose a `listen` method
82
+ * (e.g. when `app` is a `Router` mounted on an existing Express app).
83
+ * @param port - TCP port number to bind to.
84
+ * @param host - Hostname or IP address to bind to (defaults to all interfaces when omitted).
85
+ */
86
+ async start(port, host) {
87
+ await new Promise((resolve) => {
88
+ if ('listen' in this.app && typeof this.app.listen === 'function') {
89
+ ;
90
+ this.app.listen(port, host, () => resolve());
91
+ return;
92
+ }
93
+ resolve();
94
+ });
95
+ }
96
+ }
97
+ /**
98
+ * Creates a fully-wired Express `Router` with CRUD routes for every resource.
99
+ *
100
+ * @param resources - Resource definitions to register (use {@link createPrismaResources} to generate these).
101
+ * @param options - Auth strategy, query-builder path overrides, etc.
102
+ * @returns An Express `Router` ready to mount with `app.use('/api', router)`.
103
+ */
104
+ export function createExpressCrudRouter(resources, options = {}) {
105
+ const router = Router();
106
+ registerCrudApi(new ExpressHttpServer(router), resources, options);
107
+ return router;
108
+ }
109
+ //# sourceMappingURL=ExpressAdapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpressAdapter.js","sourceRoot":"","sources":["../../../src/adapters/http/ExpressAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAQhC,OAAO,EAAE,eAAe,EAAuB,MAAM,sBAAsB,CAAA;AAG3E;;;;;;GAMG;AACH,SAAS,gBAAgB,CACvB,OAA2B;IAE3B,OAAO,OAAwD,CAAA;AACjE,CAAC;AAED;;;;GAIG;AACH,SAAS,YAAY,CAAC,GAAY;IAChC,OAAO;QACL,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,GAAG,CAAC,MAAgC;QAC5C,KAAK,EAAE,GAAG,CAAC,KAAgC;QAC3C,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC;QACtC,GAAG,EAAE,GAAG;KACT,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,GAAa;IAClC,OAAO;QACL,GAAG,EAAE,GAAG;QACR,MAAM,CAAC,IAAY;YACjB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YAChB,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,CAAC,OAAgB;YACnB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnB,CAAC;QACD,IAAI,CAAC,OAAiB;YACpB,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnB,CAAC;QACD,SAAS,CAAC,IAAY,EAAE,KAAa;YACnC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC5B,CAAC;KACF,CAAA;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,OAAO,iBAAiB;IAMQ;IALpC;;;;OAIG;IACH,YAAoC,GAAqB;QAArB,QAAG,GAAH,GAAG,CAAkB;IAAG,CAAC;IAE7D;;;;;OAKG;IACI,aAAa,CAAC,MAAkB,EAAE,IAAY,EAAE,OAAyB;QAC9E,MAAM,EAAE,GAAG,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;YACzC,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACtE,CAAC,CAAA;QACD,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,CAAC;YAAC,IAAI,CAAC,GAAW,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;YAChC,OAAM;QACR,CAAC;QACD,CAAC;QAAC,IAAI,CAAC,GAAW,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IACpD,CAAC;IAED;;;;;OAKG;IACI,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,IAAa;QAC5C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,IAAI,QAAQ,IAAI,IAAI,CAAC,GAAG,IAAI,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBAClE,CAAC;gBAAC,IAAI,CAAC,GAAW,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAA;gBACtD,OAAM;YACR,CAAC;YACD,OAAO,EAAE,CAAA;QACX,CAAC,CAAC,CAAA;IACJ,CAAC;CACF;AAKD;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,SAA+B,EAC/B,UAAoC,EAAE;IAEtC,MAAM,MAAM,GAAG,MAAM,EAAE,CAAA;IACvB,eAAe,CAAC,IAAI,iBAAiB,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;IAClE,OAAO,MAAM,CAAA;AACf,CAAC"}