@edium/halifax 2.2.2 → 2.3.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 (44) hide show
  1. package/CHANGELOG.md +161 -1
  2. package/README.md +15 -15
  3. package/README_AUTH.md +2 -2
  4. package/README_AUTOCRUD.md +5 -4
  5. package/README_CACHE.md +6 -0
  6. package/README_CLASSES.md +13 -6
  7. package/README_INTERFACES.md +13 -11
  8. package/README_OPENAPI.md +1 -1
  9. package/README_REPO_ADAPTERS.md +10 -0
  10. package/dist/adapters/orm/drizzle/DrizzleAdapter.d.ts +5 -0
  11. package/dist/adapters/orm/drizzle/DrizzleAdapter.js +57 -14
  12. package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +1 -2
  13. package/dist/adapters/orm/prisma/PrismaAdapter.js +149 -39
  14. package/dist/adapters/orm/prisma/astToPrisma.js +6 -0
  15. package/dist/auth/strategies/JwtClaimsAuthStrategy.js +2 -8
  16. package/dist/auth/strategies/PassportStrategies.js +3 -9
  17. package/dist/auth/strategies/types.d.ts +7 -0
  18. package/dist/auth/strategies/types.js +13 -1
  19. package/dist/core/cache/CacheStore.d.ts +12 -0
  20. package/dist/core/cache/createCachingRepository.js +10 -1
  21. package/dist/core/cache/in-memory/InMemoryCacheStore.d.ts +14 -1
  22. package/dist/core/cache/in-memory/InMemoryCacheStore.js +29 -1
  23. package/dist/core/cache/redis/RedisCacheStore.d.ts +6 -0
  24. package/dist/core/cache/redis/RedisCacheStore.js +14 -0
  25. package/dist/core/cache/redis/RedisLikeClient.d.ts +2 -0
  26. package/dist/core/crudRouter.js +2 -20
  27. package/dist/core/fields.d.ts +11 -1
  28. package/dist/core/fields.js +19 -0
  29. package/dist/core/handlerUtils.d.ts +6 -0
  30. package/dist/core/handlerUtils.js +16 -11
  31. package/dist/core/handlers/create.js +3 -2
  32. package/dist/core/handlers/query.js +3 -5
  33. package/dist/core/handlers/readMany.js +3 -5
  34. package/dist/core/handlers/readOne.js +3 -6
  35. package/dist/core/handlers/updateMany.js +3 -4
  36. package/dist/core/queryString.d.ts +10 -0
  37. package/dist/core/queryString.js +23 -0
  38. package/dist/core/validation.js +5 -11
  39. package/dist/errors/ConflictError.d.ts +5 -0
  40. package/dist/errors/ConflictError.js +8 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +1 -0
  43. package/dist/openapi/specGenerator.js +24 -19
  44. package/package.json +2 -2
@@ -1,3 +1,4 @@
1
+ import { checkRequiredPermissions } from '../auth/strategies/types.js';
1
2
  import { HttpError } from '../errors/HttpError.js';
2
3
  import { NotAcceptableError } from '../errors/NotAcceptableError.js';
3
4
  import { UnprocessableEntityError } from '../errors/UnprocessableEntityError.js';
@@ -13,6 +14,7 @@ const statusCodeMap = {
13
14
  404: 'NOT_FOUND',
14
15
  405: 'METHOD_NOT_ALLOWED',
15
16
  406: 'NOT_ACCEPTABLE',
17
+ 409: 'CONFLICT',
16
18
  415: 'UNSUPPORTED_MEDIA_TYPE',
17
19
  422: 'UNPROCESSABLE_ENTITY',
18
20
  501: 'NOT_IMPLEMENTED'
@@ -62,9 +64,9 @@ export function writeSuccess(res, status, body, envelope) {
62
64
  */
63
65
  export function parseId(raw) {
64
66
  validateId(raw);
65
- if (typeof raw === 'string' && (isValidUuid(raw) || isValidObjectId(raw)))
67
+ if (isValidUuid(raw) || isValidObjectId(raw))
66
68
  return raw;
67
- return typeof raw === 'string' ? parseInt(raw, 10) : raw;
69
+ return parseInt(raw, 10);
68
70
  }
69
71
  /**
70
72
  * Strips non-writable fields from a request body and rejects unknown fields with a 422.
@@ -95,12 +97,20 @@ export function filterWritableFields(resource, data, auth) {
95
97
  * Fast-path returns the record unchanged when no fields carry read restrictions.
96
98
  */
97
99
  export function filterReadableFields(resource, record, auth) {
100
+ return makeReadableFieldFilter(resource, auth)(record);
101
+ }
102
+ /**
103
+ * Returns a reusable filter function that strips read-restricted fields.
104
+ * Build this once per request (outside a `.map()` loop) so the fieldMap and
105
+ * userRoles Set are not reconstructed for every record in a bulk response.
106
+ */
107
+ export function makeReadableFieldFilter(resource, auth) {
98
108
  const fields = resource.fields ?? [];
99
109
  if (!fields.some((f) => (f.readRoles?.length ?? 0) > 0))
100
- return record;
110
+ return (r) => r;
101
111
  const fieldMap = new Map(fields.map((f) => [f.name, f]));
102
112
  const userRoles = new Set([...(auth?.roles ?? []), ...(auth?.permissions ?? [])]);
103
- return Object.fromEntries(Object.entries(record).filter(([key]) => {
113
+ return (record) => Object.fromEntries(Object.entries(record).filter(([key]) => {
104
114
  const field = fieldMap.get(key);
105
115
  if (!field?.readRoles?.length)
106
116
  return true;
@@ -125,13 +135,8 @@ export async function authorizeRequest(req, resource, action, authStrategy) {
125
135
  throw new AuthorizationError();
126
136
  return auth;
127
137
  }
128
- if (requiredPermissions.length) {
129
- const permissions = new Set(auth.permissions ?? []);
130
- const roles = new Set(auth.roles ?? []);
131
- const allowed = requiredPermissions.some((permission) => permissions.has(permission) || roles.has(permission));
132
- if (!allowed)
133
- throw new AuthorizationError();
134
- }
138
+ if (!checkRequiredPermissions(auth, requiredPermissions))
139
+ throw new AuthorizationError();
135
140
  return auth;
136
141
  }
137
142
  /**
@@ -1,4 +1,4 @@
1
- import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, getHeaderValue, wrap, writeSuccess } from '../../core/handlerUtils.js';
1
+ import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, getHeaderValue, makeReadableFieldFilter, wrap, writeSuccess } from '../../core/handlerUtils.js';
2
2
  export function registerCreate(server, basePath, ctx) {
3
3
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
4
4
  server.registerRoute('POST', basePath, wrap(async (req, res) => {
@@ -21,6 +21,7 @@ export function registerCreate(server, basePath, ctx) {
21
21
  const results = hooks?.afterCreate
22
22
  ? await Promise.all(rawResults.map((r) => applyHook(hooks.afterCreate, r, hookCtx)))
23
23
  : rawResults;
24
- await writeSuccess(res, 201, results.map((r) => filterReadableFields(resource, r, auth)), envelope);
24
+ const filterRecord = makeReadableFieldFilter(resource, auth);
25
+ await writeSuccess(res, 201, results.map(filterRecord), envelope);
25
26
  }));
26
27
  }
@@ -1,6 +1,6 @@
1
1
  import { validateAdvancedQuery } from '../../core/validation.js';
2
2
  import { NotImplementedError } from '../../errors/NotImplementedError.js';
3
- import { applyHook, authorizeRequest, filterReadableFields, wrap, writeSuccess } from '../../core/handlerUtils.js';
3
+ import { applyHook, authorizeRequest, makeReadableFieldFilter, wrap, writeSuccess } from '../../core/handlerUtils.js';
4
4
  export function registerQuery(server, basePath, queryBuilderPath, ctx) {
5
5
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
6
6
  server.registerRoute('POST', `${basePath}/${queryBuilderPath}`, wrap(async (req, res) => {
@@ -15,9 +15,7 @@ export function registerQuery(server, basePath, queryBuilderPath, ctx) {
15
15
  validateAdvancedQuery(resource, query);
16
16
  const rawResult = await repo.executeQuery(query);
17
17
  const result = await applyHook(hooks?.afterQuery, rawResult, hookCtx);
18
- await writeSuccess(res, 200, {
19
- ...result,
20
- results: result.results.map((r) => filterReadableFields(resource, r, auth))
21
- }, envelope);
18
+ const filterRecord = makeReadableFieldFilter(resource, auth);
19
+ await writeSuccess(res, 200, { ...result, results: result.results.map(filterRecord) }, envelope);
22
20
  }));
23
21
  }
@@ -1,5 +1,5 @@
1
1
  import { parseListOptions } from '../../core/queryString.js';
2
- import { applyHook, authorizeRequest, filterReadableFields, wrap, writeSuccess } from '../../core/handlerUtils.js';
2
+ import { applyHook, authorizeRequest, makeReadableFieldFilter, wrap, writeSuccess } from '../../core/handlerUtils.js';
3
3
  export function registerReadMany(server, basePath, ctx) {
4
4
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
5
5
  server.registerRoute('GET', basePath, wrap(async (req, res) => {
@@ -10,9 +10,7 @@ export function registerReadMany(server, basePath, ctx) {
10
10
  const listOptions = await applyHook(hooks?.beforeReadMany, parsedOptions, hookCtx);
11
11
  const rawResult = await repo.getMany(listOptions);
12
12
  const result = await applyHook(hooks?.afterReadMany, rawResult, hookCtx);
13
- await writeSuccess(res, 200, {
14
- ...result,
15
- results: result.results.map((r) => filterReadableFields(resource, r, auth))
16
- }, envelope);
13
+ const filterRecord = makeReadableFieldFilter(resource, auth);
14
+ await writeSuccess(res, 200, { ...result, results: result.results.map(filterRecord) }, envelope);
17
15
  }));
18
16
  }
@@ -1,4 +1,4 @@
1
- import { parseListOptions } from '../../core/queryString.js';
1
+ import { parseGetOneOptions } from '../../core/queryString.js';
2
2
  import { NotFoundError } from '../../errors/NotFoundError.js';
3
3
  import { applyHook, authorizeRequest, filterReadableFields, parseId, wrap, writeSuccess } from '../../core/handlerUtils.js';
4
4
  export function registerReadOne(server, basePath, ctx) {
@@ -10,11 +10,8 @@ export function registerReadOne(server, basePath, ctx) {
10
10
  const hookCtx = { auth, resource, req };
11
11
  if (hooks?.beforeReadOne)
12
12
  await hooks.beforeReadOne(id, hookCtx);
13
- const listOptions = parseListOptions(req.query, resource);
14
- const rawResult = await repo.getOne(id, {
15
- fields: listOptions.fields,
16
- include: listOptions.include
17
- });
13
+ const { fields, include } = parseGetOneOptions(req.query, resource);
14
+ const rawResult = await repo.getOne(id, { fields, include });
18
15
  if (!rawResult)
19
16
  throw new NotFoundError();
20
17
  const result = await applyHook(hooks?.afterReadOne, rawResult, hookCtx);
@@ -1,7 +1,7 @@
1
1
  import { validateAdvancedQuery } from '../../core/validation.js';
2
2
  import { NotImplementedError } from '../../errors/NotImplementedError.js';
3
3
  import { UnprocessableEntityError } from '../../errors/UnprocessableEntityError.js';
4
- import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, wrap, writeSuccess } from '../../core/handlerUtils.js';
4
+ import { applyHook, authorizeRequest, filterWritableFields, makeReadableFieldFilter, wrap, writeSuccess } from '../../core/handlerUtils.js';
5
5
  export function registerUpdateMany(server, basePath, ctx) {
6
6
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
7
7
  server.registerRoute('PATCH', basePath, wrap(async (req, res) => {
@@ -22,12 +22,11 @@ export function registerUpdateMany(server, basePath, ctx) {
22
22
  await hooks.beforeUpdateMany(query, filteredUpdate, hookCtx);
23
23
  const rawResult = await repo.updateMany(query, filteredUpdate);
24
24
  const result = await applyHook(hooks?.afterUpdateMany, rawResult, hookCtx);
25
+ const filterRecord = makeReadableFieldFilter(resource, auth);
25
26
  await writeSuccess(res, 200, {
26
27
  ...result,
27
28
  ...(result.results
28
- ? {
29
- results: result.results.map((r) => filterReadableFields(resource, r, auth))
30
- }
29
+ ? { results: result.results.map((r) => filterRecord(r)) }
31
30
  : {})
32
31
  }, envelope);
33
32
  }));
@@ -1,4 +1,14 @@
1
1
  import { type ListOptions, type ResourceDefinition } from '../core/types.js';
2
+ /**
3
+ * Parses and validates the query-string for a single-record GET (`GET /resource/:id`).
4
+ * Only `?fields=` and `?include=` are meaningful for that endpoint — this avoids the wasted
5
+ * work and silent-discard behaviour of calling the full `parseListOptions` on a by-ID route.
6
+ *
7
+ * @param query - The raw query-string object from the HTTP request.
8
+ * @param resource - The resource definition used for field and relation validation.
9
+ * @returns Typed projection options ready to pass to `repository.getOne()`.
10
+ */
11
+ export declare function parseGetOneOptions(query: Record<string, unknown>, resource: ResourceDefinition): Pick<ListOptions, 'fields' | 'include'>;
2
12
  /**
3
13
  * Parses and validates the raw query-string from a GET request into typed {@link ListOptions}.
4
14
  *
@@ -31,6 +31,29 @@ function parseCsv(value) {
31
31
  .map((item) => item.trim())
32
32
  .filter(Boolean);
33
33
  }
34
+ /**
35
+ * Parses and validates the query-string for a single-record GET (`GET /resource/:id`).
36
+ * Only `?fields=` and `?include=` are meaningful for that endpoint — this avoids the wasted
37
+ * work and silent-discard behaviour of calling the full `parseListOptions` on a by-ID route.
38
+ *
39
+ * @param query - The raw query-string object from the HTTP request.
40
+ * @param resource - The resource definition used for field and relation validation.
41
+ * @returns Typed projection options ready to pass to `repository.getOne()`.
42
+ */
43
+ export function parseGetOneOptions(query, resource) {
44
+ const fields = parseCsv(query.fields);
45
+ const include = parseCsv(query.include);
46
+ if (fields) {
47
+ validateFields(resource, fields);
48
+ validateSelectableFields(resource, fields);
49
+ }
50
+ if (include)
51
+ validateIncludes(resource, include);
52
+ if (fields && include) {
53
+ throw new UnprocessableEntityError('Cannot use both ?fields= and ?include= in the same request.');
54
+ }
55
+ return { fields, include };
56
+ }
34
57
  /**
35
58
  * Parses and validates the raw query-string from a GET request into typed {@link ListOptions}.
36
59
  *
@@ -58,9 +58,7 @@ export function validateId(value) {
58
58
  * @returns Array of field name strings.
59
59
  */
60
60
  export function getFieldNames(resource) {
61
- return (resource.fields ?? []).map((field) => {
62
- return field.name;
63
- });
61
+ return (resource.fields ?? []).map((f) => f.name);
64
62
  }
65
63
  /**
66
64
  * Throws {@link UnprocessableEntityError} when any of `fields` are not defined on the resource.
@@ -82,10 +80,8 @@ export function validateFields(resource, fields = []) {
82
80
  * @param fields - Field names to check for selectability.
83
81
  */
84
82
  export function validateSelectableFields(resource, fields) {
85
- const nonSelectable = fields.filter((name) => {
86
- const field = resource.fields?.find((f) => f.name === name);
87
- return field?.selectable === false;
88
- });
83
+ const fieldMap = new Map((resource.fields ?? []).map((f) => [f.name, f]));
84
+ const nonSelectable = fields.filter((name) => fieldMap.get(name)?.selectable === false);
89
85
  if (nonSelectable.length) {
90
86
  throw new UnprocessableEntityError(`Field(s) not selectable: ${nonSelectable.join(', ')}.`);
91
87
  }
@@ -96,10 +92,8 @@ export function validateSelectableFields(resource, fields) {
96
92
  * @param fields - Field names to check for sortability.
97
93
  */
98
94
  export function validateSortableFields(resource, fields) {
99
- const nonSortable = fields.filter((name) => {
100
- const field = resource.fields?.find((f) => f.name === name);
101
- return field?.sortable === false;
102
- });
95
+ const fieldMap = new Map((resource.fields ?? []).map((f) => [f.name, f]));
96
+ const nonSortable = fields.filter((name) => fieldMap.get(name)?.sortable === false);
103
97
  if (nonSortable.length) {
104
98
  throw new UnprocessableEntityError(`Field(s) not sortable: ${nonSortable.join(', ')}.`);
105
99
  }
@@ -0,0 +1,5 @@
1
+ import { HttpError } from './HttpError.js';
2
+ /** Thrown when a write conflicts with an existing record — e.g. a duplicate unique field (HTTP 409). */
3
+ export declare class ConflictError extends HttpError {
4
+ constructor(message?: string, details?: unknown);
5
+ }
@@ -0,0 +1,8 @@
1
+ import { HttpError } from './HttpError.js';
2
+ /** Thrown when a write conflicts with an existing record — e.g. a duplicate unique field (HTTP 409). */
3
+ export class ConflictError extends HttpError {
4
+ constructor(message = 'Conflict', details) {
5
+ super(message, 409, details);
6
+ this.name = 'ConflictError';
7
+ }
8
+ }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './core/types.js';
10
10
  export * from './core/validation.js';
11
11
  export * from '@edium/halifax-types';
12
12
  export * from './errors/AuthenticationError.js';
13
+ export * from './errors/ConflictError.js';
13
14
  export * from './errors/AuthorizationError.js';
14
15
  export * from './errors/BadRequestError.js';
15
16
  export * from './errors/HttpError.js';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export * from './core/types.js';
10
10
  export * from './core/validation.js';
11
11
  export * from '@edium/halifax-types';
12
12
  export * from './errors/AuthenticationError.js';
13
+ export * from './errors/ConflictError.js';
13
14
  export * from './errors/AuthorizationError.js';
14
15
  export * from './errors/BadRequestError.js';
15
16
  export * from './errors/HttpError.js';
@@ -1,5 +1,5 @@
1
1
  import { defaultCrudPermissions } from '../core/types.js';
2
- import { mergeFieldDefinitions } from '../core/fields.js';
2
+ import { mergeFieldDefinitions, mergeRelationDefinitions, normalizeEnvelope } from '../core/fields.js';
3
3
  // ─── Helpers ──────────────────────────────────────────────────────────────────
4
4
  // Exhaustive map from every FieldType to its JSON Schema type string.
5
5
  // Adding a new FieldType causes a compile error here until the map is updated — no switch to edit.
@@ -21,9 +21,6 @@ function toPascalCase(routePrefix) {
21
21
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
22
22
  .join('');
23
23
  }
24
- function normalizeEnvelope(value) {
25
- return typeof value === 'string' && value.length > 0 ? value : null;
26
- }
27
24
  function mergeFields(resource) {
28
25
  const idField = resource.repository?.idField ?? 'id';
29
26
  return mergeFieldDefinitions(resource).map((f) => ({
@@ -31,14 +28,6 @@ function mergeFields(resource) {
31
28
  writable: f.name === idField ? f.writable === true : f.writable !== false
32
29
  }));
33
30
  }
34
- function mergeRelations(resource) {
35
- const byName = new Map();
36
- for (const r of resource.repository?.relations ?? [])
37
- byName.set(r.name, r);
38
- for (const r of resource.relations ?? [])
39
- byName.set(r.name, r);
40
- return [...byName.values()];
41
- }
42
31
  // Wraps a schema under an envelope key if one is active.
43
32
  function withEnvelope(schema, envelope) {
44
33
  if (!envelope)
@@ -202,7 +191,7 @@ const sharedSchemas = {
202
191
  ' { "field": "published", "comparison": "=", "value": true },',
203
192
  ' { "field": "createdAt", "comparison": ">=", "value": "2024-01-01T00:00:00Z" }',
204
193
  ' ],',
205
- ' "orderBy": [{ "field": "createdAt", "direction": "desc" }],',
194
+ ' "orderBy": [{ "field": "createdAt", "order": "DESC" }],',
206
195
  ' "limit": 20,',
207
196
  ' "offset": 0,',
208
197
  ' "fields": ["id", "title", "createdAt"],',
@@ -228,10 +217,10 @@ const sharedSchemas = {
228
217
  description: 'Sort order. Multiple entries produce multi-column sorting.',
229
218
  items: {
230
219
  type: 'object',
231
- required: ['field', 'direction'],
220
+ required: ['field', 'order'],
232
221
  properties: {
233
222
  field: { type: 'string', description: 'Field name to sort by (must be sortable).' },
234
- direction: { type: 'string', enum: ['asc', 'desc'] }
223
+ order: { type: 'string', enum: ['ASC', 'DESC'] }
235
224
  }
236
225
  }
237
226
  },
@@ -240,7 +229,11 @@ const sharedSchemas = {
240
229
  items: { type: 'string' },
241
230
  description: 'Relation names to eagerly load (if the resource supports includes).'
242
231
  },
243
- distinct: { type: 'boolean', description: 'When `true`, de-duplicates results.' }
232
+ distinct: {
233
+ type: 'array',
234
+ items: { type: 'string' },
235
+ description: 'Field names to de-duplicate results on (maps to SQL DISTINCT ON these columns).'
236
+ }
244
237
  }
245
238
  }
246
239
  };
@@ -262,6 +255,7 @@ const badRequestError = errorRef('Bad Request — malformed query string, invali
262
255
  const notFoundError = errorRef('Not Found — the record with the given ID does not exist.');
263
256
  const unprocessableError = errorRef('Unprocessable Entity — request body contains unknown or non-writable fields.');
264
257
  const notImplementedError = errorRef('Not Implemented — the underlying repository does not support this operation.');
258
+ const conflictError = errorRef('Conflict — the write was rejected because it would violate a unique constraint.');
265
259
  // ─── Main export ──────────────────────────────────────────────────────────────
266
260
  export function generateOpenApiSpec(resources, options = {}) {
267
261
  const globalEnvelope = normalizeEnvelope(options.envelope);
@@ -284,10 +278,17 @@ export function generateOpenApiSpec(resources, options = {}) {
284
278
  : {})
285
279
  }
286
280
  };
281
+ // Note: resources here are the *raw* definitions as passed by the caller — they have NOT
282
+ // been through crudRouter's `normalizeResource()`. That means `mergeFields` and
283
+ // `mergeRelationDefinitions` below re-derive the same merged views that the router already
284
+ // computed at startup. This is intentional: the spec generator is a standalone function
285
+ // (called outside the router for static generation tooling), so it can't rely on the
286
+ // router's normalized state. If crudRouter ever caches normalized resources, pass them
287
+ // here instead to avoid the duplicate merge work.
287
288
  for (const resource of resources) {
288
289
  const permissions = { ...defaultCrudPermissions, ...resource.permissions };
289
290
  const fields = mergeFields(resource);
290
- const relations = mergeRelations(resource);
291
+ const relations = mergeRelationDefinitions(resource);
291
292
  const idField = resource.repository?.idField ?? 'id';
292
293
  const schemaBase = toPascalCase(resource.routePrefix);
293
294
  const tag = resource.name ?? schemaBase;
@@ -458,6 +459,7 @@ export function generateOpenApiSpec(resources, options = {}) {
458
459
  content: { 'application/json': { schema: { oneOf: [singleResponse, arrayResponse] } } }
459
460
  },
460
461
  '400': badRequestError,
462
+ '409': conflictError,
461
463
  '422': unprocessableError,
462
464
  ...commonErrors,
463
465
  ...writeErrors
@@ -500,10 +502,10 @@ export function generateOpenApiSpec(resources, options = {}) {
500
502
  type: 'array',
501
503
  items: {
502
504
  type: 'object',
503
- required: ['field', 'direction'],
505
+ required: ['field', 'order'],
504
506
  properties: {
505
507
  field: { type: 'string' },
506
- direction: { type: 'string', enum: ['asc', 'desc'] }
508
+ order: { type: 'string', enum: ['ASC', 'DESC'] }
507
509
  }
508
510
  }
509
511
  }
@@ -538,6 +540,7 @@ export function generateOpenApiSpec(resources, options = {}) {
538
540
  }
539
541
  },
540
542
  '400': badRequestError,
543
+ '409': conflictError,
541
544
  '422': unprocessableError,
542
545
  '501': notImplementedError,
543
546
  ...commonErrors,
@@ -697,6 +700,7 @@ export function generateOpenApiSpec(resources, options = {}) {
697
700
  },
698
701
  '400': badRequestError,
699
702
  '404': notFoundError,
703
+ '409': conflictError,
700
704
  '422': unprocessableError,
701
705
  ...commonErrors,
702
706
  ...writeErrors
@@ -733,6 +737,7 @@ export function generateOpenApiSpec(resources, options = {}) {
733
737
  }
734
738
  },
735
739
  '400': badRequestError,
740
+ '409': conflictError,
736
741
  '422': unprocessableError,
737
742
  '501': notImplementedError,
738
743
  ...commonErrors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edium/halifax",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Auto-generate type-safe REST CRUD APIs from your data models. Adapter-driven: Express/Fastify/HyperExpress, Prisma (PostgreSQL, MySQL, MariaDB, SQL Server, CockroachDB, SQLite), JWT/API-key auth, multi-tenancy, a dynamic query builder, and pluggable Redis caching.",
5
5
  "author": "David LaTour <david@edium.com>",
6
6
  "homepage": "https://github.com/splayfee/halifax#readme",
@@ -166,7 +166,7 @@
166
166
  ],
167
167
  "dependencies": {
168
168
  "uuid": "^14.0.0",
169
- "@edium/halifax-types": "2.2.2"
169
+ "@edium/halifax-types": "2.3.0"
170
170
  },
171
171
  "scripts": {
172
172
  "build": "rm -rf dist && tsc --build && tsc-alias",