@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
@@ -58,16 +58,13 @@ export interface HttpServer {
58
58
  }
59
59
  /** Flags indicating which optional repository operations the adapter supports. */
60
60
  export interface RepositoryCapabilities {
61
- /** True when the adapter can execute raw SQL via the query builder. */
62
- supportsNativeSql: boolean;
63
- /** True when the adapter supports `?include=` for eager-loading relations. */
61
+ /**
62
+ * True when the adapter can eager-load relations via `?include=`. When `false`, the router
63
+ * rejects any `?include=` request with 422 instead of silently ignoring it.
64
+ */
64
65
  supportsIncludes: boolean;
65
- /** True when the adapter can participate in database transactions. */
66
- supportsTransactions: boolean;
67
66
  /** True when `createMany` returns the created records rather than an empty array. */
68
67
  supportsCreateManyReturn: boolean;
69
- /** True when the adapter accepts a NoSQL-style query AST (non-SQL ORMs). */
70
- supportsNoSqlQueryAst: boolean;
71
68
  }
72
69
  /** Options for paginated, filtered, and sorted list queries. */
73
70
  export interface ListOptions {
@@ -106,8 +103,8 @@ export interface UpdateManyResult<TRecord> {
106
103
  /** Updated records, when the adapter supports returning them. */
107
104
  results?: TRecord[];
108
105
  }
109
- /** Result envelope returned by `executeQueryBuilder`. */
110
- export interface NativeQueryResult<TRecord> {
106
+ /** Result envelope returned by `executeQuery`. */
107
+ export interface QueryResult<TRecord> {
111
108
  /** Total number of matching records (before pagination). */
112
109
  count?: number;
113
110
  /** Records for the current page. */
@@ -118,10 +115,32 @@ export interface CreateOptions {
118
115
  /** An idempotency key — the adapter may de-duplicate requests with the same key. */
119
116
  idempotencyKey?: string;
120
117
  }
118
+ /**
119
+ * A resolved tenant constraint for a single request: the column to scope on and
120
+ * the value the current caller is allowed to see. Produced by the router from
121
+ * {@link TenantResourceConfig} (the field) and the tenant resolver (the value),
122
+ * then handed to {@link Repository.withScope}.
123
+ */
124
+ export interface TenantScope {
125
+ /** Column / property on the model that stores the tenant key (e.g. `'companyId'`). */
126
+ field: string;
127
+ /** The tenant key the caller is bound to (e.g. their company id). */
128
+ value: unknown;
129
+ }
121
130
  /** Core data-access contract that every Halifax repository adapter must satisfy. */
122
131
  export interface Repository<TRecord = unknown, TCreate = Partial<TRecord>, TUpdate = Partial<TRecord>> {
123
132
  /** Optional capability flags used by Halifax to decide which routes to activate. */
124
133
  readonly capabilities?: Partial<RepositoryCapabilities>;
134
+ /**
135
+ * Field schema the adapter knows about (e.g. derived from a Prisma model). When present,
136
+ * a {@link ResourceDefinition} may omit `fields` entirely (these are used as the base) or
137
+ * supply a sparse subset that is merged over these as per-field overrides.
138
+ */
139
+ readonly fields?: FieldDefinition[];
140
+ /** Relation schema the adapter knows about — used as the base for `?include=` access. */
141
+ readonly relations?: RelationDefinition[];
142
+ /** Primary-key field name (default `'id'`). The router protects this field from write bodies. */
143
+ readonly idField?: string;
125
144
  /**
126
145
  * Fetch a single record by its primary key.
127
146
  * @param id - Primary key value (integer or UUID string).
@@ -187,7 +206,21 @@ export interface Repository<TRecord = unknown, TCreate = Partial<TRecord>, TUpda
187
206
  * @param query - Full query AST including table name, filters, pagination, and sort.
188
207
  * @returns A count-and-results envelope for the matching rows.
189
208
  */
190
- executeQueryBuilder?(query: IQueryOptions): Promise<NativeQueryResult<TRecord>>;
209
+ executeQuery?(query: IQueryOptions): Promise<QueryResult<TRecord>>;
210
+ /**
211
+ * Return a request-scoped clone of this repository that transparently constrains
212
+ * **every** operation to the given {@link TenantScope}. Reads are filtered by the
213
+ * scope, writes are stamped with it, and bulk SQL operations have the scope AND-ed
214
+ * into their WHERE clause so callers can never reach another tenant's rows.
215
+ *
216
+ * Adapters that cannot enforce scoping safely should leave this undefined — the
217
+ * router treats a tenant-scoped resource whose repository lacks `withScope` as a
218
+ * fatal misconfiguration (fail-closed) rather than serving it unscoped.
219
+ *
220
+ * @param scope - The resolved tenant constraint for the current request.
221
+ * @returns A new repository instance bound to `scope` (the original is unchanged).
222
+ */
223
+ withScope?(scope: TenantScope): Repository<TRecord, TCreate, TUpdate>;
191
224
  }
192
225
  /** All CRUD action identifiers used for permissions and audit. */
193
226
  export type CrudAction = 'create' | 'readOne' | 'readMany' | 'readManyWithQueryBuilder' | 'updateOne' | 'updateMany' | 'upsertOne' | 'deleteOne' | 'deleteMany';
@@ -228,10 +261,14 @@ export interface ModelSchema {
228
261
  export interface ModelResourceOptions {
229
262
  /** When true, this model is skipped entirely. */
230
263
  exclude?: boolean;
264
+ /**
265
+ * Tenant isolation for this model. Set `{ field }` to scope on a specific column,
266
+ * or `false` to opt this model out of an otherwise tenant-scoped API. When omitted,
267
+ * the model is auto-scoped if the API's default tenant field exists on it.
268
+ */
269
+ tenant?: TenantResourceConfig | false;
231
270
  /** Override the URL prefix (default: auto-derived kebab-plural of the model name). */
232
271
  routePrefix?: string;
233
- /** Override the database table name. */
234
- tableName?: string;
235
272
  /** Override the default CRUD permissions for this model. */
236
273
  permissions?: CrudPermissions;
237
274
  /** Required permission strings per action for fine-grained access control. */
@@ -243,19 +280,35 @@ export interface ModelResourceOptions {
243
280
  /** Maximum nesting depth for WHERE clause children (default: 3). */
244
281
  maxFilterDepth?: number;
245
282
  }
246
- /** Describes a single column exposed through the Halifax API. */
283
+ /**
284
+ * Describes a single column exposed through the Halifax API.
285
+ *
286
+ * Every flag is **permissive by default** — only set one to `false` to restrict a field.
287
+ * A field with just `{ name }` is filterable, sortable, selectable, and writable. The lone
288
+ * exception is the primary key, which is non-writable by default (it comes from the URL / DB);
289
+ * set `writable: true` on it explicitly if you really want clients to supply it.
290
+ */
247
291
  export interface FieldDefinition {
248
292
  /** Column / property name. */
249
293
  name: string;
250
- /** When `false`, the field cannot be used in `?field=` filters. */
294
+ /** When `false`, the field cannot be used in `?field=` filters. Defaults to `true`. */
251
295
  filterable?: boolean;
252
- /** When `false`, the field cannot be used in `?order=` sorts. */
296
+ /** When `false`, the field cannot be used in `?order=` sorts. Defaults to `true`. */
253
297
  sortable?: boolean;
254
- /** When `false`, the field is excluded from `?fields=` projections. */
298
+ /** When `false`, the field is excluded from `?fields=` projections. Defaults to `true`. */
255
299
  selectable?: boolean;
256
- /** When `false`, the field is stripped from POST/PATCH request bodies. */
300
+ /** When `false`, the field is stripped from POST/PATCH/PUT bodies. Defaults to `true` (except the primary key). */
257
301
  writable?: boolean;
258
302
  }
303
+ /**
304
+ * Declares that a resource is tenant-scoped: every request is confined to rows whose
305
+ * {@link TenantResourceConfig.field} equals the tenant value resolved for the caller.
306
+ * Omit `tenant` (or set it to `false`) to expose a resource globally / unscoped.
307
+ */
308
+ export interface TenantResourceConfig {
309
+ /** Column / property on this model that stores the tenant key (e.g. `'companyId'`). */
310
+ field: string;
311
+ }
259
312
  /** Describes a relation that callers may eagerly load via `?include=`. */
260
313
  export interface RelationDefinition {
261
314
  /** Relation name as defined on the Prisma model. */
@@ -265,29 +318,86 @@ export interface RelationDefinition {
265
318
  }
266
319
  /** Full definition of a Halifax resource: its repository, field schema, routing, and permissions. */
267
320
  export interface ResourceDefinition<TRecord = unknown, TCreate = Partial<TRecord>, TUpdate = Partial<TRecord>> {
268
- /** Human-readable resource name (usually the Prisma model name). */
269
- name: string;
270
- /** URL path segment (e.g. `'users'`, `'blog-posts'`). */
321
+ /**
322
+ * URL path segment (e.g. `'users'`, `'blog-posts'`). The only required field — it defines
323
+ * the resource's public route, which has no safe default.
324
+ */
271
325
  routePrefix: string;
272
- /** Database table name used by the query builder. */
273
- tableName?: string;
274
- /** Scalar field definitions — controls filtering, sorting, selection, and write access. */
275
- fields: FieldDefinition[];
276
- /** Relation definitions — controls `?include=` access. */
277
- relations?: RelationDefinition[];
278
- /** CRUD operation toggles. Defaults to {@link defaultCrudPermissions}. */
279
- permissions?: CrudPermissions;
280
326
  /** The data adapter that handles reads and writes for this resource. */
281
327
  repository: Repository<TRecord, TCreate, TUpdate>;
328
+ /**
329
+ * Human-readable resource name (used in error messages and the cache-key namespace).
330
+ * Optional — defaults to a title-cased form of {@link routePrefix} (`'blog-posts'` → `'Blog Posts'`).
331
+ */
332
+ name?: string;
333
+ /**
334
+ * Scalar field definitions — control filtering, sorting, selection, and write access.
335
+ *
336
+ * Optional when the {@link repository} exposes its own field schema (e.g. a `PrismaAdapter`
337
+ * built with a `model`, or anything from `createPrismaResources`): in that case the
338
+ * repository's fields are the base, and any entries here are merged over them **by name** as
339
+ * sparse overrides — so you list only the fields you want to change. When the repository
340
+ * exposes no schema, this is the authoritative allow-list and is required.
341
+ */
342
+ fields?: FieldDefinition[];
343
+ /**
344
+ * Relation definitions — control `?include=` access. Merged over the repository's relation
345
+ * schema by name when the repository exposes one; otherwise the authoritative list.
346
+ */
347
+ relations?: RelationDefinition[];
348
+ /**
349
+ * Tenant isolation for this resource. When set (and a tenant resolver is configured
350
+ * on the API), every read/write/bulk operation is constrained to the caller's tenant.
351
+ * Set to `false` to explicitly opt a resource out of an otherwise tenant-scoped API.
352
+ * When omitted, the resource is scoped only if the API's default tenant field exists
353
+ * on this model (auto-detection); otherwise it is treated as global.
354
+ */
355
+ tenant?: TenantResourceConfig | false;
356
+ /**
357
+ * CRUD operation toggles. Merged over {@link defaultCrudPermissions}, which enables every
358
+ * action — so you only list the actions you want to **disable** (e.g. `{ allowDeleteMany: false }`).
359
+ */
360
+ permissions?: CrudPermissions;
282
361
  /** Required permission strings per action (checked by the auth strategy). */
283
362
  requiredPermissions?: Partial<Record<CrudAction, string[]>>;
284
- /** Default page size when the caller omits `?limit=`. No limit applied when `undefined`. */
363
+ /**
364
+ * Default page size when the caller omits `?limit=`. Defaults to {@link DEFAULT_PAGE_LIMIT}
365
+ * (5000). Set to `0` to apply no default limit (return all rows when `?limit=` is omitted).
366
+ */
285
367
  defaultLimit?: number;
286
- /** Hard cap on page size. Requests over this are silently capped. No cap when `undefined`. */
368
+ /**
369
+ * Hard cap on page size; larger requests are silently capped (the response `count` still
370
+ * reflects the true total). Defaults to {@link MAX_PAGE_LIMIT} (5000). Set to `0` to remove
371
+ * the cap entirely — combine `defaultLimit: 0` and `maxLimit: 0` to disable pagination.
372
+ */
287
373
  maxLimit?: number;
288
374
  /** Maximum nesting depth for WHERE clause children. Defaults to 3. */
289
375
  maxFilterDepth?: number;
376
+ /**
377
+ * Read-through caching for this resource. When set, the router caches
378
+ * `getOne`/`getMany`/query reads and invalidates them on any write to this resource.
379
+ * Overrides the API-wide default. Omit to inherit the API default (if any); set to
380
+ * `false` to explicitly disable caching for this resource even when a default is configured.
381
+ */
382
+ cache?: ResourceCacheConfig | false;
383
+ }
384
+ /** Per-resource read-through cache configuration. */
385
+ export interface ResourceCacheConfig {
386
+ /** Time-to-live for cached reads, in seconds. `0` means **never expire** (cache forever). */
387
+ ttlSeconds: number;
290
388
  }
389
+ /**
390
+ * Default page size applied when a resource sets no `defaultLimit` and the caller omits
391
+ * `?limit=`. Chosen as a generous safety ceiling — large enough for typical "show everything"
392
+ * UIs, small enough to prevent a runaway full-table scan. `getMany` always returns the true
393
+ * total `count`, so a page is never a silent drop; a resource can set `defaultLimit: 0` to
394
+ * return all rows by default.
395
+ */
396
+ export declare const DEFAULT_PAGE_LIMIT = 5000;
397
+ /**
398
+ * Default hard cap on page size applied when a resource sets no `maxLimit`. A resource can set
399
+ * `maxLimit: 0` to remove the cap entirely (no pagination).
400
+ */
401
+ export declare const MAX_PAGE_LIMIT = 5000;
291
402
  /** Default permissions applied to every resource — all CRUD operations enabled. */
292
403
  export declare const defaultCrudPermissions: Required<CrudPermissions>;
293
- //# sourceMappingURL=types.d.ts.map
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Default page size applied when a resource sets no `defaultLimit` and the caller omits
3
+ * `?limit=`. Chosen as a generous safety ceiling — large enough for typical "show everything"
4
+ * UIs, small enough to prevent a runaway full-table scan. `getMany` always returns the true
5
+ * total `count`, so a page is never a silent drop; a resource can set `defaultLimit: 0` to
6
+ * return all rows by default.
7
+ */
8
+ export const DEFAULT_PAGE_LIMIT = 5000;
9
+ /**
10
+ * Default hard cap on page size applied when a resource sets no `maxLimit`. A resource can set
11
+ * `maxLimit: 0` to remove the cap entirely (no pagination).
12
+ */
13
+ export const MAX_PAGE_LIMIT = 5000;
1
14
  /** Default permissions applied to every resource — all CRUD operations enabled. */
2
15
  export const defaultCrudPermissions = {
3
16
  allowCreate: true,
@@ -10,4 +23,3 @@ export const defaultCrudPermissions = {
10
23
  allowDeleteOne: true,
11
24
  allowDeleteMany: true
12
25
  };
13
- //# sourceMappingURL=types.js.map
@@ -15,7 +15,15 @@ export declare function isValidInt32(value: string | number | null, min?: number
15
15
  */
16
16
  export declare function isValidUuid(value: string): boolean;
17
17
  /**
18
- * Asserts that `value` is a valid integer ID or UUID string.
18
+ * Returns `true` when `value` is a valid MongoDB ObjectId (24 hex chars).
19
+ * Lets MongoDB-backed resources accept their `_id` values through the same `:id` route
20
+ * validation that integer and UUID keys use.
21
+ * @param value - String to test.
22
+ * @returns `true` when `value` is a 24-character hex string.
23
+ */
24
+ export declare function isValidObjectId(value: string): boolean;
25
+ /**
26
+ * Asserts that `value` is a valid integer ID, UUID, or MongoDB ObjectId string.
19
27
  * Throws {@link BadRequestError} when validation fails.
20
28
  * @param value - Raw `:id` route parameter to validate.
21
29
  */
@@ -45,8 +53,9 @@ export declare function validateSelectableFields(resource: ResourceDefinition, f
45
53
  */
46
54
  export declare function validateSortableFields(resource: ResourceDefinition, fields: string[]): void;
47
55
  /**
48
- * Throws {@link UnprocessableEntityError} when any of `includes` reference non-includable relations.
49
- * @param resource - The resource definition whose relations are checked.
56
+ * Throws {@link UnprocessableEntityError} when any of `includes` reference non-includable
57
+ * relations, or when the resource's repository reports it cannot eager-load relations at all.
58
+ * @param resource - The resource definition whose relations and repository are checked.
50
59
  * @param includes - Relation names to validate.
51
60
  */
52
61
  export declare function validateIncludes(resource: ResourceDefinition, includes?: string[]): void;
@@ -72,4 +81,3 @@ export declare function validateWhere(resource: ResourceDefinition, where?: IQue
72
81
  * @param query - The query AST to validate.
73
82
  */
74
83
  export declare function validateAdvancedQuery(resource: ResourceDefinition, query: IQueryOptions): void;
75
- //# sourceMappingURL=validation.d.ts.map
@@ -26,19 +26,32 @@ export function isValidInt32(value, min = 1) {
26
26
  export function isValidUuid(value) {
27
27
  return uuidValidate(value);
28
28
  }
29
+ /** Matches a MongoDB ObjectId: exactly 24 hexadecimal characters. */
30
+ const objectIdPattern = /^[a-f\d]{24}$/i;
29
31
  /**
30
- * Asserts that `value` is a valid integer ID or UUID string.
32
+ * Returns `true` when `value` is a valid MongoDB ObjectId (24 hex chars).
33
+ * Lets MongoDB-backed resources accept their `_id` values through the same `:id` route
34
+ * validation that integer and UUID keys use.
35
+ * @param value - String to test.
36
+ * @returns `true` when `value` is a 24-character hex string.
37
+ */
38
+ export function isValidObjectId(value) {
39
+ return objectIdPattern.test(value);
40
+ }
41
+ /**
42
+ * Asserts that `value` is a valid integer ID, UUID, or MongoDB ObjectId string.
31
43
  * Throws {@link BadRequestError} when validation fails.
32
44
  * @param value - Raw `:id` route parameter to validate.
33
45
  */
34
46
  export function validateId(value) {
47
+ const message = 'Id parameter must be an integer (1–2147483647), a valid UUID, or a 24-character ObjectId.';
35
48
  if (value === undefined) {
36
- throw new BadRequestError('Id parameter must be an integer (1–2147483647) or a valid UUID.');
49
+ throw new BadRequestError(message);
37
50
  }
38
51
  const isInt = isValidInt32(value);
39
- const isUuid = typeof value === 'string' && isValidUuid(value);
40
- if (!isInt && !isUuid) {
41
- throw new BadRequestError('Id parameter must be an integer (1–2147483647) or a valid UUID.');
52
+ const isStringId = typeof value === 'string' && (isValidUuid(value) || isValidObjectId(value));
53
+ if (!isInt && !isStringId) {
54
+ throw new BadRequestError(message);
42
55
  }
43
56
  }
44
57
  /**
@@ -47,7 +60,7 @@ export function validateId(value) {
47
60
  * @returns Array of field name strings.
48
61
  */
49
62
  export function getFieldNames(resource) {
50
- return resource.fields.map((field) => {
63
+ return (resource.fields ?? []).map((field) => {
51
64
  return field.name;
52
65
  });
53
66
  }
@@ -72,7 +85,7 @@ export function validateFields(resource, fields = []) {
72
85
  */
73
86
  export function validateSelectableFields(resource, fields) {
74
87
  const nonSelectable = fields.filter((name) => {
75
- const field = resource.fields.find((f) => f.name === name);
88
+ const field = resource.fields?.find((f) => f.name === name);
76
89
  return field?.selectable === false;
77
90
  });
78
91
  if (nonSelectable.length) {
@@ -86,7 +99,7 @@ export function validateSelectableFields(resource, fields) {
86
99
  */
87
100
  export function validateSortableFields(resource, fields) {
88
101
  const nonSortable = fields.filter((name) => {
89
- const field = resource.fields.find((f) => f.name === name);
102
+ const field = resource.fields?.find((f) => f.name === name);
90
103
  return field?.sortable === false;
91
104
  });
92
105
  if (nonSortable.length) {
@@ -94,11 +107,15 @@ export function validateSortableFields(resource, fields) {
94
107
  }
95
108
  }
96
109
  /**
97
- * Throws {@link UnprocessableEntityError} when any of `includes` reference non-includable relations.
98
- * @param resource - The resource definition whose relations are checked.
110
+ * Throws {@link UnprocessableEntityError} when any of `includes` reference non-includable
111
+ * relations, or when the resource's repository reports it cannot eager-load relations at all.
112
+ * @param resource - The resource definition whose relations and repository are checked.
99
113
  * @param includes - Relation names to validate.
100
114
  */
101
115
  export function validateIncludes(resource, includes = []) {
116
+ if (includes.length && resource.repository?.capabilities?.supportsIncludes === false) {
117
+ throw new UnprocessableEntityError('This resource does not support relation includes.');
118
+ }
102
119
  const validIncludes = new Set((resource.relations ?? [])
103
120
  .filter((relation) => {
104
121
  return relation.includable !== false;
@@ -120,7 +137,7 @@ export function validateIncludes(resource, includes = []) {
120
137
  * @param query - The raw query-string object from the HTTP request.
121
138
  */
122
139
  export function validateQueryString(resource, query) {
123
- const filterableFieldNames = resource.fields
140
+ const filterableFieldNames = (resource.fields ?? [])
124
141
  .filter((f) => f.filterable !== false)
125
142
  .map((f) => f.name);
126
143
  const allFieldNames = new Set(getFieldNames(resource));
@@ -152,7 +169,7 @@ export function validateQueryString(resource, query) {
152
169
  * @param depth - Current recursion depth (used for nesting-limit enforcement; start at `0`).
153
170
  */
154
171
  export function validateWhere(resource, where = [], depth = 0) {
155
- const maxDepth = resource.maxFilterDepth ?? 3;
172
+ const maxDepth = resource.maxFilterDepth ?? 4;
156
173
  if (depth > maxDepth) {
157
174
  throw new UnprocessableEntityError(`Filter nesting exceeds the maximum allowed depth of ${maxDepth}.`);
158
175
  }
@@ -190,6 +207,10 @@ export function validateAdvancedQuery(resource, query) {
190
207
  validateFields(resource, query.fields);
191
208
  validateSelectableFields(resource, query.fields);
192
209
  }
210
+ if (query.distinct) {
211
+ validateFields(resource, query.distinct);
212
+ validateSelectableFields(resource, query.distinct);
213
+ }
193
214
  if (query.orderBy) {
194
215
  const validOrders = new Set(Object.values(SqlOrder));
195
216
  const sortFields = query.orderBy.map((s) => s.field);
@@ -203,4 +224,3 @@ export function validateAdvancedQuery(resource, query) {
203
224
  }
204
225
  validateWhere(resource, query.where);
205
226
  }
206
- //# sourceMappingURL=validation.js.map
@@ -1,6 +1,16 @@
1
- /** SQL comparison operators used in query-builder WHERE clauses. */
1
+ /**
2
+ * Comparison operators accepted in query-builder WHERE clauses.
3
+ *
4
+ * The classic SQL operators (`=`, `LIKE`, `IN`, `BETWEEN`, …) are kept as-is. The string
5
+ * operators (`CONTAINS`, `STARTS WITH`, `ENDS WITH`) are portable extensions that both
6
+ * Prisma and Drizzle express natively on every dialect, so they behave identically
7
+ * regardless of the underlying database — unlike `LIKE`, whose case-sensitivity varies
8
+ * by engine collation.
9
+ */
2
10
  export declare enum SqlComparison {
3
11
  Between = "BETWEEN",
12
+ Contains = "CONTAINS",
13
+ EndsWith = "ENDS WITH",
4
14
  Equal = "=",
5
15
  GreaterThan = ">",
6
16
  GreaterThanOrEqual = ">=",
@@ -13,6 +23,6 @@ export declare enum SqlComparison {
13
23
  NotBetween = "NOT BETWEEN",
14
24
  NotEqual = "<>",
15
25
  NotIn = "NOT IN",
16
- NotLike = "NOT LIKE"
26
+ NotLike = "NOT LIKE",
27
+ StartsWith = "STARTS WITH"
17
28
  }
18
- //# sourceMappingURL=SqlComparison.d.ts.map
@@ -1,7 +1,17 @@
1
- /** SQL comparison operators used in query-builder WHERE clauses. */
1
+ /**
2
+ * Comparison operators accepted in query-builder WHERE clauses.
3
+ *
4
+ * The classic SQL operators (`=`, `LIKE`, `IN`, `BETWEEN`, …) are kept as-is. The string
5
+ * operators (`CONTAINS`, `STARTS WITH`, `ENDS WITH`) are portable extensions that both
6
+ * Prisma and Drizzle express natively on every dialect, so they behave identically
7
+ * regardless of the underlying database — unlike `LIKE`, whose case-sensitivity varies
8
+ * by engine collation.
9
+ */
2
10
  export var SqlComparison;
3
11
  (function (SqlComparison) {
4
12
  SqlComparison["Between"] = "BETWEEN";
13
+ SqlComparison["Contains"] = "CONTAINS";
14
+ SqlComparison["EndsWith"] = "ENDS WITH";
5
15
  SqlComparison["Equal"] = "=";
6
16
  SqlComparison["GreaterThan"] = ">";
7
17
  SqlComparison["GreaterThanOrEqual"] = ">=";
@@ -15,5 +25,5 @@ export var SqlComparison;
15
25
  SqlComparison["NotEqual"] = "<>";
16
26
  SqlComparison["NotIn"] = "NOT IN";
17
27
  SqlComparison["NotLike"] = "NOT LIKE";
28
+ SqlComparison["StartsWith"] = "STARTS WITH";
18
29
  })(SqlComparison || (SqlComparison = {}));
19
- //# sourceMappingURL=SqlComparison.js.map
@@ -3,4 +3,3 @@ export declare enum SqlOperator {
3
3
  And = "AND",
4
4
  Or = "OR"
5
5
  }
6
- //# sourceMappingURL=SqlOperator.d.ts.map
@@ -4,4 +4,3 @@ export var SqlOperator;
4
4
  SqlOperator["And"] = "AND";
5
5
  SqlOperator["Or"] = "OR";
6
6
  })(SqlOperator || (SqlOperator = {}));
7
- //# sourceMappingURL=SqlOperator.js.map
@@ -3,4 +3,3 @@ export declare enum SqlOrder {
3
3
  ASC = "ASC",
4
4
  DESC = "DESC"
5
5
  }
6
- //# sourceMappingURL=SqlOrder.d.ts.map
@@ -4,4 +4,3 @@ export var SqlOrder;
4
4
  SqlOrder["ASC"] = "ASC";
5
5
  SqlOrder["DESC"] = "DESC";
6
6
  })(SqlOrder || (SqlOrder = {}));
7
- //# sourceMappingURL=SqlOrder.js.map
@@ -7,4 +7,3 @@ export declare class AuthenticationError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=AuthenticationError.d.ts.map
@@ -10,4 +10,3 @@ export class AuthenticationError extends HttpError {
10
10
  this.name = 'AuthenticationError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=AuthenticationError.js.map
@@ -7,4 +7,3 @@ export declare class AuthorizationError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=AuthorizationError.d.ts.map
@@ -10,4 +10,3 @@ export class AuthorizationError extends HttpError {
10
10
  this.name = 'AuthorizationError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=AuthorizationError.js.map
@@ -7,4 +7,3 @@ export declare class BadRequestError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=BadRequestError.d.ts.map
@@ -10,4 +10,3 @@ export class BadRequestError extends HttpError {
10
10
  this.name = 'BadRequestError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=BadRequestError.js.map
@@ -9,4 +9,3 @@ export declare abstract class HttpError extends Error {
9
9
  */
10
10
  constructor(message: string, status: number, details?: unknown);
11
11
  }
12
- //# sourceMappingURL=HttpError.d.ts.map
@@ -14,4 +14,3 @@ export class HttpError extends Error {
14
14
  this.details = details;
15
15
  }
16
16
  }
17
- //# sourceMappingURL=HttpError.js.map
@@ -7,4 +7,3 @@ export declare class MethodNotAllowedError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=MethodNotAllowedError.d.ts.map
@@ -10,4 +10,3 @@ export class MethodNotAllowedError extends HttpError {
10
10
  this.name = 'MethodNotAllowedError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=MethodNotAllowedError.js.map
@@ -7,4 +7,3 @@ export declare class NotAcceptableError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=NotAcceptableError.d.ts.map
@@ -10,4 +10,3 @@ export class NotAcceptableError extends HttpError {
10
10
  this.name = 'NotAcceptableError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=NotAcceptableError.js.map
@@ -7,4 +7,3 @@ export declare class NotFoundError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=NotFoundError.d.ts.map
@@ -10,4 +10,3 @@ export class NotFoundError extends HttpError {
10
10
  this.name = 'NotFoundError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=NotFoundError.js.map
@@ -7,4 +7,3 @@ export declare class NotImplementedError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=NotImplementedError.d.ts.map
@@ -10,4 +10,3 @@ export class NotImplementedError extends HttpError {
10
10
  this.name = 'NotImplementedError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=NotImplementedError.js.map
@@ -7,4 +7,3 @@ export declare class ServerError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=ServerError.d.ts.map
@@ -10,4 +10,3 @@ export class ServerError extends HttpError {
10
10
  this.name = 'ServerError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=ServerError.js.map
@@ -7,4 +7,3 @@ export declare class UnprocessableEntityError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=UnprocessableEntityError.d.ts.map
@@ -10,4 +10,3 @@ export class UnprocessableEntityError extends HttpError {
10
10
  this.name = 'UnprocessableEntityError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=UnprocessableEntityError.js.map
@@ -7,4 +7,3 @@ export declare class UnsupportedMediaTypeError extends HttpError {
7
7
  */
8
8
  constructor(message?: string, details?: unknown);
9
9
  }
10
- //# sourceMappingURL=UnsupportedMediaTypeError.d.ts.map
@@ -10,4 +10,3 @@ export class UnsupportedMediaTypeError extends HttpError {
10
10
  this.name = 'UnsupportedMediaTypeError';
11
11
  }
12
12
  }
13
- //# sourceMappingURL=UnsupportedMediaTypeError.js.map