@edium/halifax 2.1.0 → 2.2.1
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 +64 -1
- package/README.md +73 -17
- package/README_AUTH.md +38 -0
- package/README_AUTOCRUD.md +5 -5
- package/README_CLASSES.md +322 -0
- package/README_HOOKS.md +275 -0
- package/README_INTERFACES.md +601 -0
- package/README_OPENAPI.md +471 -0
- package/README_REPO_ADAPTERS.md +77 -0
- package/README_TYPES.md +114 -0
- package/dist/adapters/orm/drizzle/DrizzleAdapter.d.ts +128 -0
- package/dist/adapters/orm/drizzle/DrizzleAdapter.js +255 -0
- package/dist/adapters/orm/drizzle/astToDrizzle.d.ts +21 -0
- package/dist/adapters/orm/drizzle/astToDrizzle.js +121 -0
- package/dist/adapters/orm/drizzle/index.d.ts +4 -0
- package/dist/adapters/orm/drizzle/index.js +2 -0
- package/dist/adapters/orm/prisma/PrismaAdapter.d.ts +1 -1
- package/dist/adapters/orm/prisma/PrismaAdapter.js +24 -1
- package/dist/adapters/orm/prisma/astToPrisma.d.ts +1 -2
- package/dist/adapters/orm/prisma/astToPrisma.js +1 -3
- package/dist/adapters/orm/prisma/helpers.js +1 -1
- package/dist/adapters/orm/prisma/types.d.ts +11 -11
- package/dist/auth/AuthStrategy.d.ts +6 -189
- package/dist/auth/AuthStrategy.js +4 -220
- package/dist/auth/strategies/AllowAllAuthStrategy.d.ts +6 -0
- package/dist/auth/strategies/AllowAllAuthStrategy.js +6 -0
- package/dist/auth/strategies/ApiKeyAuthStrategy.d.ts +25 -0
- package/dist/auth/strategies/ApiKeyAuthStrategy.js +39 -0
- package/dist/auth/strategies/JwtClaimsAuthStrategy.d.ts +32 -0
- package/dist/auth/strategies/JwtClaimsAuthStrategy.js +52 -0
- package/dist/auth/strategies/PassportStrategies.d.ts +94 -0
- package/dist/auth/strategies/PassportStrategies.js +142 -0
- package/dist/auth/strategies/types.d.ts +70 -0
- package/dist/core/crudRouter.d.ts +11 -18
- package/dist/core/crudRouter.js +95 -390
- package/dist/core/fields.d.ts +8 -0
- package/dist/core/fields.js +14 -0
- package/dist/core/handlerUtils.d.ts +70 -0
- package/dist/core/handlerUtils.js +193 -0
- package/dist/core/handlers/create.d.ts +3 -0
- package/dist/core/handlers/create.js +26 -0
- package/dist/core/handlers/deleteMany.d.ts +3 -0
- package/dist/core/handlers/deleteMany.js +24 -0
- package/dist/core/handlers/deleteOne.d.ts +3 -0
- package/dist/core/handlers/deleteOne.js +19 -0
- package/dist/core/handlers/query.d.ts +3 -0
- package/dist/core/handlers/query.js +23 -0
- package/dist/core/handlers/readMany.d.ts +3 -0
- package/dist/core/handlers/readMany.js +18 -0
- package/dist/core/handlers/readOne.d.ts +3 -0
- package/dist/core/handlers/readOne.js +23 -0
- package/dist/core/handlers/updateMany.d.ts +3 -0
- package/dist/core/handlers/updateMany.js +34 -0
- package/dist/core/handlers/updateOne.d.ts +3 -0
- package/dist/core/handlers/updateOne.js +20 -0
- package/dist/core/handlers/upsertOne.d.ts +3 -0
- package/dist/core/handlers/upsertOne.js +20 -0
- package/dist/core/hooks.d.ts +217 -0
- package/dist/core/queryString.js +1 -1
- package/dist/core/types.d.ts +38 -29
- package/dist/core/validation.d.ts +1 -2
- package/dist/core/validation.js +1 -3
- package/dist/index.d.ts +3 -6
- package/dist/index.js +3 -6
- package/dist/openapi/generateDocsHtml.d.ts +1 -0
- package/dist/openapi/generateDocsHtml.js +47 -0
- package/dist/openapi/index.d.ts +3 -0
- package/dist/openapi/index.js +2 -0
- package/dist/openapi/specGenerator.d.ts +149 -0
- package/dist/openapi/specGenerator.js +770 -0
- package/package.json +38 -22
- package/dist/enums/SqlComparison.d.ts +0 -28
- package/dist/enums/SqlComparison.js +0 -29
- package/dist/enums/SqlOperator.d.ts +0 -5
- package/dist/enums/SqlOperator.js +0 -6
- package/dist/enums/SqlOrder.d.ts +0 -5
- package/dist/enums/SqlOrder.js +0 -6
- package/dist/interfaces/IQueryFilter.d.ts +0 -17
- package/dist/interfaces/IQueryOptions.d.ts +0 -20
- package/dist/interfaces/ISort.d.ts +0 -8
- package/dist/interfaces/ISort.js +0 -1
- /package/dist/{interfaces/IQueryFilter.js → auth/strategies/types.js} +0 -0
- /package/dist/{interfaces/IQueryOptions.js → core/hooks.js} +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AuthContext, AuthStrategy } from '../auth/AuthStrategy.js';
|
|
2
|
+
import type { CrudHooks, HookContext, MaybePromise } from '../core/hooks.js';
|
|
3
|
+
import { type CrudAction, type ResourceDefinition } from '../core/types.js';
|
|
4
|
+
import type { HttpRequest, HttpResponse, Repository } from '../core/types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Converts any thrown value to a structured `{ status, code, message, details }` object.
|
|
7
|
+
* {@link HttpError} subclasses preserve their status; all other errors become 500.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizeError(error: unknown): {
|
|
10
|
+
status: number;
|
|
11
|
+
code: string;
|
|
12
|
+
message: string;
|
|
13
|
+
details?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Serialises a caught error and writes it as a JSON `{ errors: [...] }` response.
|
|
17
|
+
*/
|
|
18
|
+
export declare function sendError(error: unknown, res: HttpResponse): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Invokes a data-transforming hook and falls back to `value` when the hook returns void.
|
|
21
|
+
*/
|
|
22
|
+
export declare function applyHook<T>(hook: ((value: T, ctx: HookContext) => MaybePromise<T | void>) | undefined, value: T, ctx: HookContext): Promise<T>;
|
|
23
|
+
/**
|
|
24
|
+
* Writes a success body as JSON, wrapping it under `envelope` when one is configured.
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeSuccess(res: HttpResponse, status: number, body: unknown, envelope: string | null): void | Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Parses and validates a raw `:id` route parameter.
|
|
29
|
+
* @throws {@link BadRequestError} when the value is not a valid integer, UUID, or ObjectId.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseId(raw: string | undefined): string | number;
|
|
32
|
+
/**
|
|
33
|
+
* Strips non-writable fields from a request body and rejects unknown fields with a 422.
|
|
34
|
+
* Fields with `writable: false` are dropped; fields gated by `writeRoles` the caller lacks
|
|
35
|
+
* are also dropped.
|
|
36
|
+
* @throws {@link UnprocessableEntityError} for keys not defined on the resource.
|
|
37
|
+
*/
|
|
38
|
+
export declare function filterWritableFields(resource: ResourceDefinition, data: Record<string, unknown>, auth?: AuthContext): Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Strips fields the caller is not permitted to read based on per-field `readRoles`.
|
|
41
|
+
* Fast-path returns the record unchanged when no fields carry read restrictions.
|
|
42
|
+
*/
|
|
43
|
+
export declare function filterReadableFields(resource: ResourceDefinition, record: Record<string, unknown>, auth?: AuthContext): Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* Runs the auth strategy for `action` and throws {@link AuthorizationError} when not allowed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function authorizeRequest(req: HttpRequest, resource: ResourceDefinition, action: CrudAction, authStrategy: AuthStrategy): Promise<AuthContext>;
|
|
48
|
+
/**
|
|
49
|
+
* Reads a single header value by name (case-insensitive).
|
|
50
|
+
*/
|
|
51
|
+
export declare function getHeaderValue(req: HttpRequest, name: string): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Returns true when the request asks to force-refresh the cache.
|
|
54
|
+
* For `Cache-Control` this means a `no-cache`/`no-store` directive; for any custom header,
|
|
55
|
+
* mere presence with a non-empty value triggers the bust.
|
|
56
|
+
*/
|
|
57
|
+
export declare function wantsCacheBust(req: HttpRequest, header: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Wraps a route handler with Content-Type / Accept checks, error serialisation,
|
|
60
|
+
* and `X-Correlation-ID` echo-back.
|
|
61
|
+
*/
|
|
62
|
+
export declare function wrap(handler: (req: HttpRequest, res: HttpResponse) => Promise<void>): (req: HttpRequest, res: HttpResponse) => Promise<void>;
|
|
63
|
+
/** Shared context passed to every route-handler registration function. */
|
|
64
|
+
export interface RouteHandlerContext {
|
|
65
|
+
resource: ResourceDefinition;
|
|
66
|
+
authStrategy: AuthStrategy;
|
|
67
|
+
envelope: string | null;
|
|
68
|
+
hooks: CrudHooks<Record<string, unknown>, Record<string, unknown>, Record<string, unknown>> | undefined;
|
|
69
|
+
resolveRepo: (req: HttpRequest, auth: AuthContext) => Promise<Repository>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { HttpError } from '../errors/HttpError.js';
|
|
2
|
+
import { NotAcceptableError } from '../errors/NotAcceptableError.js';
|
|
3
|
+
import { UnprocessableEntityError } from '../errors/UnprocessableEntityError.js';
|
|
4
|
+
import { UnsupportedMediaTypeError } from '../errors/UnsupportedMediaTypeError.js';
|
|
5
|
+
import { AuthorizationError } from '../errors/AuthorizationError.js';
|
|
6
|
+
import {} from '../core/types.js';
|
|
7
|
+
import { validateId, isValidUuid, isValidObjectId } from '../core/validation.js';
|
|
8
|
+
/** Maps HTTP status codes to machine-readable error code strings. */
|
|
9
|
+
const statusCodeMap = {
|
|
10
|
+
400: 'BAD_REQUEST',
|
|
11
|
+
401: 'UNAUTHORIZED',
|
|
12
|
+
403: 'FORBIDDEN',
|
|
13
|
+
404: 'NOT_FOUND',
|
|
14
|
+
405: 'METHOD_NOT_ALLOWED',
|
|
15
|
+
406: 'NOT_ACCEPTABLE',
|
|
16
|
+
415: 'UNSUPPORTED_MEDIA_TYPE',
|
|
17
|
+
422: 'UNPROCESSABLE_ENTITY',
|
|
18
|
+
501: 'NOT_IMPLEMENTED'
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Converts any thrown value to a structured `{ status, code, message, details }` object.
|
|
22
|
+
* {@link HttpError} subclasses preserve their status; all other errors become 500.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeError(error) {
|
|
25
|
+
if (error instanceof HttpError) {
|
|
26
|
+
return {
|
|
27
|
+
status: error.status,
|
|
28
|
+
code: statusCodeMap[error.status] ?? 'INTERNAL_ERROR',
|
|
29
|
+
message: error.message,
|
|
30
|
+
details: error.details
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { status: 500, code: 'INTERNAL_ERROR', message: 'Internal server error' };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Serialises a caught error and writes it as a JSON `{ errors: [...] }` response.
|
|
37
|
+
*/
|
|
38
|
+
export async function sendError(error, res) {
|
|
39
|
+
const { status, code, message, details } = normalizeError(error);
|
|
40
|
+
const item = { code, message };
|
|
41
|
+
if (details !== undefined)
|
|
42
|
+
item['details'] = details;
|
|
43
|
+
await res.status(status).json({ errors: [item] });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Invokes a data-transforming hook and falls back to `value` when the hook returns void.
|
|
47
|
+
*/
|
|
48
|
+
export async function applyHook(hook, value, ctx) {
|
|
49
|
+
if (!hook)
|
|
50
|
+
return value;
|
|
51
|
+
return (await hook(value, ctx)) ?? value;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Writes a success body as JSON, wrapping it under `envelope` when one is configured.
|
|
55
|
+
*/
|
|
56
|
+
export function writeSuccess(res, status, body, envelope) {
|
|
57
|
+
return res.status(status).json(envelope ? { [envelope]: body } : body);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parses and validates a raw `:id` route parameter.
|
|
61
|
+
* @throws {@link BadRequestError} when the value is not a valid integer, UUID, or ObjectId.
|
|
62
|
+
*/
|
|
63
|
+
export function parseId(raw) {
|
|
64
|
+
validateId(raw);
|
|
65
|
+
if (typeof raw === 'string' && (isValidUuid(raw) || isValidObjectId(raw)))
|
|
66
|
+
return raw;
|
|
67
|
+
return typeof raw === 'string' ? parseInt(raw, 10) : raw;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Strips non-writable fields from a request body and rejects unknown fields with a 422.
|
|
71
|
+
* Fields with `writable: false` are dropped; fields gated by `writeRoles` the caller lacks
|
|
72
|
+
* are also dropped.
|
|
73
|
+
* @throws {@link UnprocessableEntityError} for keys not defined on the resource.
|
|
74
|
+
*/
|
|
75
|
+
export function filterWritableFields(resource, data, auth) {
|
|
76
|
+
const fields = resource.fields ?? [];
|
|
77
|
+
const fieldMap = new Map(fields.map((f) => [f.name, f]));
|
|
78
|
+
const unknownFields = Object.keys(data).filter((key) => !fieldMap.has(key));
|
|
79
|
+
if (unknownFields.length) {
|
|
80
|
+
throw new UnprocessableEntityError(`Unknown field(s): ${unknownFields.join(', ')}.`);
|
|
81
|
+
}
|
|
82
|
+
const userRoles = new Set([...(auth?.roles ?? []), ...(auth?.permissions ?? [])]);
|
|
83
|
+
return Object.fromEntries(Object.entries(data).filter(([key]) => {
|
|
84
|
+
const field = fieldMap.get(key);
|
|
85
|
+
if (field?.writable === false)
|
|
86
|
+
return false;
|
|
87
|
+
if (field?.writeRoles?.length) {
|
|
88
|
+
return field.writeRoles.some((r) => userRoles.has(r));
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Strips fields the caller is not permitted to read based on per-field `readRoles`.
|
|
95
|
+
* Fast-path returns the record unchanged when no fields carry read restrictions.
|
|
96
|
+
*/
|
|
97
|
+
export function filterReadableFields(resource, record, auth) {
|
|
98
|
+
const fields = resource.fields ?? [];
|
|
99
|
+
if (!fields.some((f) => (f.readRoles?.length ?? 0) > 0))
|
|
100
|
+
return record;
|
|
101
|
+
const fieldMap = new Map(fields.map((f) => [f.name, f]));
|
|
102
|
+
const userRoles = new Set([...(auth?.roles ?? []), ...(auth?.permissions ?? [])]);
|
|
103
|
+
return Object.fromEntries(Object.entries(record).filter(([key]) => {
|
|
104
|
+
const field = fieldMap.get(key);
|
|
105
|
+
if (!field?.readRoles?.length)
|
|
106
|
+
return true;
|
|
107
|
+
return field.readRoles.some((r) => userRoles.has(r));
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Runs the auth strategy for `action` and throws {@link AuthorizationError} when not allowed.
|
|
112
|
+
*/
|
|
113
|
+
export async function authorizeRequest(req, resource, action, authStrategy) {
|
|
114
|
+
const auth = await authStrategy.authenticate(req);
|
|
115
|
+
const requiredPermissions = resource.requiredPermissions?.[action] ?? [];
|
|
116
|
+
if (authStrategy.authorize) {
|
|
117
|
+
const allowed = await authStrategy.authorize({
|
|
118
|
+
auth,
|
|
119
|
+
action,
|
|
120
|
+
resource,
|
|
121
|
+
requiredPermissions,
|
|
122
|
+
req
|
|
123
|
+
});
|
|
124
|
+
if (!allowed)
|
|
125
|
+
throw new AuthorizationError();
|
|
126
|
+
return auth;
|
|
127
|
+
}
|
|
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
|
+
}
|
|
135
|
+
return auth;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Reads a single header value by name (case-insensitive).
|
|
139
|
+
*/
|
|
140
|
+
export function getHeaderValue(req, name) {
|
|
141
|
+
const raw = req.headers[name.toLowerCase()] ?? req.headers[name];
|
|
142
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
143
|
+
return typeof value === 'string' ? value : undefined;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Returns true when the request asks to force-refresh the cache.
|
|
147
|
+
* For `Cache-Control` this means a `no-cache`/`no-store` directive; for any custom header,
|
|
148
|
+
* mere presence with a non-empty value triggers the bust.
|
|
149
|
+
*/
|
|
150
|
+
export function wantsCacheBust(req, header) {
|
|
151
|
+
const value = getHeaderValue(req, header);
|
|
152
|
+
if (!value)
|
|
153
|
+
return false;
|
|
154
|
+
if (header.toLowerCase() === 'cache-control')
|
|
155
|
+
return /no-cache|no-store/i.test(value);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
function checkContentType(req) {
|
|
159
|
+
if (!['POST', 'PATCH', 'PUT', 'DELETE'].includes(req.method.toUpperCase()))
|
|
160
|
+
return;
|
|
161
|
+
const contentType = getHeaderValue(req, 'content-type') ?? '';
|
|
162
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
163
|
+
throw new UnsupportedMediaTypeError();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function checkAcceptHeader(req) {
|
|
167
|
+
const accept = getHeaderValue(req, 'accept') ?? '';
|
|
168
|
+
if (accept &&
|
|
169
|
+
!accept.includes('*/*') &&
|
|
170
|
+
!accept.includes('application/*') &&
|
|
171
|
+
!accept.includes('application/json')) {
|
|
172
|
+
throw new NotAcceptableError();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Wraps a route handler with Content-Type / Accept checks, error serialisation,
|
|
177
|
+
* and `X-Correlation-ID` echo-back.
|
|
178
|
+
*/
|
|
179
|
+
export function wrap(handler) {
|
|
180
|
+
return async (req, res) => {
|
|
181
|
+
const correlationId = getHeaderValue(req, 'x-correlation-id');
|
|
182
|
+
if (correlationId)
|
|
183
|
+
res.setHeader?.('X-Correlation-ID', correlationId);
|
|
184
|
+
try {
|
|
185
|
+
checkContentType(req);
|
|
186
|
+
checkAcceptHeader(req);
|
|
187
|
+
await handler(req, res);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
await sendError(error, res);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, getHeaderValue, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
2
|
+
export function registerCreate(server, basePath, ctx) {
|
|
3
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
4
|
+
server.registerRoute('POST', basePath, wrap(async (req, res) => {
|
|
5
|
+
const auth = await authorizeRequest(req, resource, 'create', authStrategy);
|
|
6
|
+
const repo = await resolveRepo(req, auth);
|
|
7
|
+
const idempotencyKey = getHeaderValue(req, 'idempotency-key');
|
|
8
|
+
const createOptions = idempotencyKey ? { idempotencyKey } : undefined;
|
|
9
|
+
const hookCtx = { auth, resource, req };
|
|
10
|
+
const rawItems = (Array.isArray(req.body) ? req.body : [req.body ?? {}]).map((item) => filterWritableFields(resource, item, auth));
|
|
11
|
+
const items = hooks?.beforeCreate
|
|
12
|
+
? await Promise.all(rawItems.map((d) => applyHook(hooks.beforeCreate, d, hookCtx)))
|
|
13
|
+
: rawItems;
|
|
14
|
+
if (items.length === 1) {
|
|
15
|
+
const rawResult = await repo.createOne(items[0], createOptions);
|
|
16
|
+
const result = await applyHook(hooks?.afterCreate, rawResult, hookCtx);
|
|
17
|
+
await writeSuccess(res, 201, filterReadableFields(resource, result, auth), envelope);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const rawResults = await repo.createMany(items, createOptions);
|
|
21
|
+
const results = hooks?.afterCreate
|
|
22
|
+
? await Promise.all(rawResults.map((r) => applyHook(hooks.afterCreate, r, hookCtx)))
|
|
23
|
+
: rawResults;
|
|
24
|
+
await writeSuccess(res, 201, results.map((r) => filterReadableFields(resource, r, auth)), envelope);
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validateAdvancedQuery } from '../../core/validation.js';
|
|
2
|
+
import { NotImplementedError } from '../../errors/NotImplementedError.js';
|
|
3
|
+
import { UnprocessableEntityError } from '../../errors/UnprocessableEntityError.js';
|
|
4
|
+
import { applyHook, authorizeRequest, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
5
|
+
export function registerDeleteMany(server, basePath, ctx) {
|
|
6
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
7
|
+
server.registerRoute('DELETE', basePath, wrap(async (req, res) => {
|
|
8
|
+
const auth = await authorizeRequest(req, resource, 'deleteMany', authStrategy);
|
|
9
|
+
const repo = await resolveRepo(req, auth);
|
|
10
|
+
if (!repo.deleteMany)
|
|
11
|
+
throw new NotImplementedError('This resource does not support deleteMany.');
|
|
12
|
+
const hookCtx = { auth, resource, req };
|
|
13
|
+
const body = (req.body ?? {});
|
|
14
|
+
const query = { ...body };
|
|
15
|
+
validateAdvancedQuery(resource, query);
|
|
16
|
+
if (!query.where?.length)
|
|
17
|
+
throw new UnprocessableEntityError('deleteMany requires at least one WHERE filter to prevent unintended bulk deletes.');
|
|
18
|
+
if (hooks?.beforeDeleteMany)
|
|
19
|
+
await hooks.beforeDeleteMany(query, hookCtx);
|
|
20
|
+
const rawResult = await repo.deleteMany(query);
|
|
21
|
+
const result = await applyHook(hooks?.afterDeleteMany, rawResult, hookCtx);
|
|
22
|
+
await writeSuccess(res, 200, result, envelope);
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NotFoundError } from '../../errors/NotFoundError.js';
|
|
2
|
+
import { authorizeRequest, parseId, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
3
|
+
export function registerDeleteOne(server, basePath, ctx) {
|
|
4
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
5
|
+
server.registerRoute('DELETE', `${basePath}/:id`, wrap(async (req, res) => {
|
|
6
|
+
const auth = await authorizeRequest(req, resource, 'deleteOne', authStrategy);
|
|
7
|
+
const repo = await resolveRepo(req, auth);
|
|
8
|
+
const id = parseId(req.params['id']);
|
|
9
|
+
const hookCtx = { auth, resource, req };
|
|
10
|
+
if (hooks?.beforeDeleteOne)
|
|
11
|
+
await hooks.beforeDeleteOne(id, hookCtx);
|
|
12
|
+
const deleted = await repo.deleteOne(id);
|
|
13
|
+
if (!deleted)
|
|
14
|
+
throw new NotFoundError();
|
|
15
|
+
if (hooks?.afterDeleteOne)
|
|
16
|
+
await hooks.afterDeleteOne(id, hookCtx);
|
|
17
|
+
await writeSuccess(res, 200, { deleted: true }, envelope);
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { validateAdvancedQuery } from '../../core/validation.js';
|
|
2
|
+
import { NotImplementedError } from '../../errors/NotImplementedError.js';
|
|
3
|
+
import { applyHook, authorizeRequest, filterReadableFields, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
4
|
+
export function registerQuery(server, basePath, queryBuilderPath, ctx) {
|
|
5
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
6
|
+
server.registerRoute('POST', `${basePath}/${queryBuilderPath}`, wrap(async (req, res) => {
|
|
7
|
+
const auth = await authorizeRequest(req, resource, 'readManyWithQueryBuilder', authStrategy);
|
|
8
|
+
const repo = await resolveRepo(req, auth);
|
|
9
|
+
if (!repo.executeQuery)
|
|
10
|
+
throw new NotImplementedError('This resource does not support the query builder.');
|
|
11
|
+
const hookCtx = { auth, resource, req };
|
|
12
|
+
const body = (req.body ?? {});
|
|
13
|
+
const parsedQuery = { ...body };
|
|
14
|
+
const query = await applyHook(hooks?.beforeQuery, parsedQuery, hookCtx);
|
|
15
|
+
validateAdvancedQuery(resource, query);
|
|
16
|
+
const rawResult = await repo.executeQuery(query);
|
|
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);
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { parseListOptions } from '../../core/queryString.js';
|
|
2
|
+
import { applyHook, authorizeRequest, filterReadableFields, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
3
|
+
export function registerReadMany(server, basePath, ctx) {
|
|
4
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
5
|
+
server.registerRoute('GET', basePath, wrap(async (req, res) => {
|
|
6
|
+
const auth = await authorizeRequest(req, resource, 'readMany', authStrategy);
|
|
7
|
+
const repo = await resolveRepo(req, auth);
|
|
8
|
+
const hookCtx = { auth, resource, req };
|
|
9
|
+
const parsedOptions = parseListOptions(req.query, resource);
|
|
10
|
+
const listOptions = await applyHook(hooks?.beforeReadMany, parsedOptions, hookCtx);
|
|
11
|
+
const rawResult = await repo.getMany(listOptions);
|
|
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);
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseListOptions } from '../../core/queryString.js';
|
|
2
|
+
import { NotFoundError } from '../../errors/NotFoundError.js';
|
|
3
|
+
import { applyHook, authorizeRequest, filterReadableFields, parseId, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
4
|
+
export function registerReadOne(server, basePath, ctx) {
|
|
5
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
6
|
+
server.registerRoute('GET', `${basePath}/:id`, wrap(async (req, res) => {
|
|
7
|
+
const auth = await authorizeRequest(req, resource, 'readOne', authStrategy);
|
|
8
|
+
const repo = await resolveRepo(req, auth);
|
|
9
|
+
const id = parseId(req.params['id']);
|
|
10
|
+
const hookCtx = { auth, resource, req };
|
|
11
|
+
if (hooks?.beforeReadOne)
|
|
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
|
+
});
|
|
18
|
+
if (!rawResult)
|
|
19
|
+
throw new NotFoundError();
|
|
20
|
+
const result = await applyHook(hooks?.afterReadOne, rawResult, hookCtx);
|
|
21
|
+
await writeSuccess(res, 200, filterReadableFields(resource, result, auth), envelope);
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { validateAdvancedQuery } from '../../core/validation.js';
|
|
2
|
+
import { NotImplementedError } from '../../errors/NotImplementedError.js';
|
|
3
|
+
import { UnprocessableEntityError } from '../../errors/UnprocessableEntityError.js';
|
|
4
|
+
import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
5
|
+
export function registerUpdateMany(server, basePath, ctx) {
|
|
6
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
7
|
+
server.registerRoute('PATCH', basePath, wrap(async (req, res) => {
|
|
8
|
+
const auth = await authorizeRequest(req, resource, 'updateMany', authStrategy);
|
|
9
|
+
const repo = await resolveRepo(req, auth);
|
|
10
|
+
if (!repo.updateMany)
|
|
11
|
+
throw new NotImplementedError('This resource does not support updateMany.');
|
|
12
|
+
const hookCtx = { auth, resource, req };
|
|
13
|
+
const { update, ...queryBody } = (req.body ?? {});
|
|
14
|
+
const filteredUpdate = filterWritableFields(resource, (update ?? {}), auth);
|
|
15
|
+
if (!Object.keys(filteredUpdate).length)
|
|
16
|
+
throw new UnprocessableEntityError('updateMany requires at least one writable field in the update payload.');
|
|
17
|
+
const query = { ...queryBody };
|
|
18
|
+
validateAdvancedQuery(resource, query);
|
|
19
|
+
if (!query.where?.length)
|
|
20
|
+
throw new UnprocessableEntityError('updateMany requires at least one WHERE filter to prevent unintended bulk updates.');
|
|
21
|
+
if (hooks?.beforeUpdateMany)
|
|
22
|
+
await hooks.beforeUpdateMany(query, filteredUpdate, hookCtx);
|
|
23
|
+
const rawResult = await repo.updateMany(query, filteredUpdate);
|
|
24
|
+
const result = await applyHook(hooks?.afterUpdateMany, rawResult, hookCtx);
|
|
25
|
+
await writeSuccess(res, 200, {
|
|
26
|
+
...result,
|
|
27
|
+
...(result.results
|
|
28
|
+
? {
|
|
29
|
+
results: result.results.map((r) => filterReadableFields(resource, r, auth))
|
|
30
|
+
}
|
|
31
|
+
: {})
|
|
32
|
+
}, envelope);
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NotFoundError } from '../../errors/NotFoundError.js';
|
|
2
|
+
import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, parseId, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
3
|
+
export function registerUpdateOne(server, basePath, ctx) {
|
|
4
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
5
|
+
server.registerRoute('PATCH', `${basePath}/:id`, wrap(async (req, res) => {
|
|
6
|
+
const auth = await authorizeRequest(req, resource, 'updateOne', authStrategy);
|
|
7
|
+
const repo = await resolveRepo(req, auth);
|
|
8
|
+
const id = parseId(req.params['id']);
|
|
9
|
+
const hookCtx = { auth, resource, req };
|
|
10
|
+
const rawBody = filterWritableFields(resource, (req.body ?? {}), auth);
|
|
11
|
+
const body = hooks?.beforeUpdateOne
|
|
12
|
+
? ((await hooks.beforeUpdateOne(id, rawBody, hookCtx)) ?? rawBody)
|
|
13
|
+
: rawBody;
|
|
14
|
+
const rawResult = await repo.updateOne(id, body);
|
|
15
|
+
if (!rawResult)
|
|
16
|
+
throw new NotFoundError();
|
|
17
|
+
const result = await applyHook(hooks?.afterUpdateOne, rawResult, hookCtx);
|
|
18
|
+
await writeSuccess(res, 200, filterReadableFields(resource, result, auth), envelope);
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NotImplementedError } from '../../errors/NotImplementedError.js';
|
|
2
|
+
import { applyHook, authorizeRequest, filterReadableFields, filterWritableFields, parseId, wrap, writeSuccess } from '../../core/handlerUtils.js';
|
|
3
|
+
export function registerUpsertOne(server, basePath, ctx) {
|
|
4
|
+
const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
|
|
5
|
+
server.registerRoute('PUT', `${basePath}/:id`, wrap(async (req, res) => {
|
|
6
|
+
const auth = await authorizeRequest(req, resource, 'upsertOne', authStrategy);
|
|
7
|
+
const repo = await resolveRepo(req, auth);
|
|
8
|
+
if (!repo.upsertOne)
|
|
9
|
+
throw new NotImplementedError('This resource does not support upsert.');
|
|
10
|
+
const id = parseId(req.params['id']);
|
|
11
|
+
const hookCtx = { auth, resource, req };
|
|
12
|
+
const rawBody = filterWritableFields(resource, (req.body ?? {}), auth);
|
|
13
|
+
const body = hooks?.beforeUpsertOne
|
|
14
|
+
? ((await hooks.beforeUpsertOne(id, rawBody, hookCtx)) ?? rawBody)
|
|
15
|
+
: rawBody;
|
|
16
|
+
const rawResult = await repo.upsertOne(id, body);
|
|
17
|
+
const result = await applyHook(hooks?.afterUpsertOne, rawResult, hookCtx);
|
|
18
|
+
await writeSuccess(res, 200, filterReadableFields(resource, result, auth), envelope);
|
|
19
|
+
}));
|
|
20
|
+
}
|