@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/dist/core/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
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 `
|
|
110
|
-
export interface
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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,96 @@ 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
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
* Wrap every success response body for this resource under a single key
|
|
385
|
+
* (e.g. `'data'` →
|
|
386
|
+
* `{ "data": <body> }`). Applies uniformly to all success payloads — list
|
|
387
|
+
* (`{ data: { count, results } }`), single object, create/update/upsert, and the
|
|
388
|
+
* `{ deleted: true }` confirmation. Error responses are never enveloped.
|
|
389
|
+
* Overrides the API-wide {@link CrudApiOptions.envelope}. Omit (or set `null`/`''`) for
|
|
390
|
+
* a bare body — the default, and backward compatible.
|
|
391
|
+
*/
|
|
392
|
+
envelope?: string | null;
|
|
393
|
+
}
|
|
394
|
+
/** Per-resource read-through cache configuration. */
|
|
395
|
+
export interface ResourceCacheConfig {
|
|
396
|
+
/** Time-to-live for cached reads, in seconds. `0` means **never expire** (cache forever). */
|
|
397
|
+
ttlSeconds: number;
|
|
290
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* Default page size applied when a resource sets no `defaultLimit` and the caller omits
|
|
401
|
+
* `?limit=`. Chosen as a generous safety ceiling — large enough for typical "show everything"
|
|
402
|
+
* UIs, small enough to prevent a runaway full-table scan. `getMany` always returns the true
|
|
403
|
+
* total `count`, so a page is never a silent drop; a resource can set `defaultLimit: 0` to
|
|
404
|
+
* return all rows by default.
|
|
405
|
+
*/
|
|
406
|
+
export declare const DEFAULT_PAGE_LIMIT = 5000;
|
|
407
|
+
/**
|
|
408
|
+
* Default hard cap on page size applied when a resource sets no `maxLimit`. A resource can set
|
|
409
|
+
* `maxLimit: 0` to remove the cap entirely (no pagination).
|
|
410
|
+
*/
|
|
411
|
+
export declare const MAX_PAGE_LIMIT = 5000;
|
|
291
412
|
/** Default permissions applied to every resource — all CRUD operations enabled. */
|
|
292
413
|
export declare const defaultCrudPermissions: Required<CrudPermissions>;
|
|
293
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/core/types.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
49
|
-
*
|
|
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
|
package/dist/core/validation.js
CHANGED
|
@@ -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
|
-
*
|
|
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(
|
|
49
|
+
throw new BadRequestError(message);
|
|
37
50
|
}
|
|
38
51
|
const isInt = isValidInt32(value);
|
|
39
|
-
const
|
|
40
|
-
if (!isInt && !
|
|
41
|
-
throw new BadRequestError(
|
|
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
|
|
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
|
|
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
|
|
98
|
-
*
|
|
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 ??
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
package/dist/enums/SqlOrder.d.ts
CHANGED
package/dist/enums/SqlOrder.js
CHANGED
package/dist/errors/HttpError.js
CHANGED