@visulima/crud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE.md +21 -0
- package/README.md +101 -0
- package/dist/chunk-FJWRITBO.js +52 -0
- package/dist/chunk-FJWRITBO.js.map +1 -0
- package/dist/chunk-UBXIGP5H.mjs +52 -0
- package/dist/chunk-UBXIGP5H.mjs.map +1 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.js +1101 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1101 -0
- package/dist/index.mjs.map +1 -0
- package/dist/next/index.d.ts +8 -0
- package/dist/next/index.js +729 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/index.mjs +729 -0
- package/dist/next/index.mjs.map +1 -0
- package/dist/types.d-6817d247.d.ts +155 -0
- package/package.json +136 -0
- package/src/adapter/prisma/index.ts +241 -0
- package/src/adapter/prisma/types.d.ts +46 -0
- package/src/adapter/prisma/utils/models-to-route-names.ts +12 -0
- package/src/adapter/prisma/utils/parse-cursor.ts +26 -0
- package/src/adapter/prisma/utils/parse-order-by.ts +21 -0
- package/src/adapter/prisma/utils/parse-recursive.ts +26 -0
- package/src/adapter/prisma/utils/parse-where.ts +197 -0
- package/src/base-crud-handler.ts +181 -0
- package/src/handler/create.ts +21 -0
- package/src/handler/delete.ts +27 -0
- package/src/handler/list.ts +62 -0
- package/src/handler/read.ts +27 -0
- package/src/handler/update.ts +29 -0
- package/src/index.ts +27 -0
- package/src/next/api/edge/index.ts +23 -0
- package/src/next/api/node/index.ts +27 -0
- package/src/next/index.ts +2 -0
- package/src/query-parser.ts +94 -0
- package/src/swagger/adapter/prisma/index.ts +95 -0
- package/src/swagger/json-schema-parser.ts +456 -0
- package/src/swagger/parameters.ts +83 -0
- package/src/swagger/types.d.ts +53 -0
- package/src/swagger/utils/format-example-ref.ts +4 -0
- package/src/swagger/utils/format-schema-ref.ts +4 -0
- package/src/swagger/utils/get-models-accessible-routes.ts +23 -0
- package/src/swagger/utils/get-swagger-paths.ts +244 -0
- package/src/swagger/utils/get-swagger-tags.ts +13 -0
- package/src/types.d.ts +124 -0
- package/src/utils/format-resource-id.ts +3 -0
- package/src/utils/get-accessible-routes.ts +18 -0
- package/src/utils/get-resource-name-from-url.ts +23 -0
- package/src/utils/get-route-type.ts +99 -0
- package/src/utils/is-primitive.ts +5 -0
- package/src/utils/validate-adapter-methods.ts +15 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const modelsToRouteNames = <M extends string = string>(mappingsMap: { [key: string]: object }, models: M[]) => {
|
|
2
|
+
const routesMap: { [key in M]?: string } = {};
|
|
3
|
+
|
|
4
|
+
models?.forEach((model) => {
|
|
5
|
+
// @ts-ignore
|
|
6
|
+
routesMap[model] = mappingsMap[model].plural;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return routesMap;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default modelsToRouteNames;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import isPrimitive from "../../../utils/is-primitive";
|
|
2
|
+
import type { PrismaCursor } from "../types.d";
|
|
3
|
+
|
|
4
|
+
const parsePrismaCursor = (
|
|
5
|
+
cursor: Record<string, string | number | boolean>,
|
|
6
|
+
): PrismaCursor => {
|
|
7
|
+
const parsed: PrismaCursor = {};
|
|
8
|
+
|
|
9
|
+
Object.keys(cursor).forEach((key) => {
|
|
10
|
+
const value = cursor[key];
|
|
11
|
+
|
|
12
|
+
if (isPrimitive(value)) {
|
|
13
|
+
parsed[key as keyof typeof cursor] = value as string | number | boolean;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
if (Object.keys(parsed).length !== 1) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"cursor needs to be an object with exactly 1 property with a primitive value",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return parsed;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default parsePrismaCursor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OrderByField, OrderByOperator } from "../../../types.d";
|
|
2
|
+
import type { PrismaOrderBy, PrismaOrderByOperator } from "../types.d";
|
|
3
|
+
|
|
4
|
+
const operatorsAssociation: Record<OrderByOperator, PrismaOrderByOperator> = {
|
|
5
|
+
$asc: "asc",
|
|
6
|
+
$desc: "desc",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const parsePrismaOrderBy = (orderBy: OrderByField): PrismaOrderBy => {
|
|
10
|
+
const parsed: PrismaOrderBy = {};
|
|
11
|
+
|
|
12
|
+
Object.keys(orderBy).forEach((key) => {
|
|
13
|
+
const value = orderBy[key];
|
|
14
|
+
|
|
15
|
+
parsed[key] = operatorsAssociation[value as OrderByOperator];
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default parsePrismaOrderBy;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RecursiveField } from "../../../types.d";
|
|
2
|
+
import type { PrismaRecursive, PrismaRecursiveField } from "../types.d";
|
|
3
|
+
|
|
4
|
+
const parsePrismaRecursiveField = <T extends PrismaRecursiveField>(
|
|
5
|
+
select: RecursiveField,
|
|
6
|
+
fieldName: T,
|
|
7
|
+
): PrismaRecursive<T> => {
|
|
8
|
+
const parsed: PrismaRecursive<T> = {};
|
|
9
|
+
|
|
10
|
+
Object.keys(select).forEach((field) => {
|
|
11
|
+
if (select[field] !== true) {
|
|
12
|
+
parsed[field] = {
|
|
13
|
+
[fieldName]: parsePrismaRecursiveField(
|
|
14
|
+
select[field] as RecursiveField,
|
|
15
|
+
fieldName,
|
|
16
|
+
),
|
|
17
|
+
} as Record<T, PrismaRecursive<T>>;
|
|
18
|
+
} else {
|
|
19
|
+
parsed[field] = true;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return parsed;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default parsePrismaRecursiveField;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Condition, SearchCondition, WhereCondition, WhereField, WhereOperator,
|
|
3
|
+
} from "../../../types.d";
|
|
4
|
+
import isPrimitive from "../../../utils/is-primitive";
|
|
5
|
+
import type {
|
|
6
|
+
PrismaFieldFilter, PrismaRelationFilter, PrismaWhereField, PrismaWhereOperator,
|
|
7
|
+
} from "../types.d";
|
|
8
|
+
|
|
9
|
+
const isObject = (a: any) => a instanceof Object;
|
|
10
|
+
|
|
11
|
+
const operatorsAssociation: {
|
|
12
|
+
[key in WhereOperator]?: PrismaWhereOperator;
|
|
13
|
+
} = {
|
|
14
|
+
$eq: "equals",
|
|
15
|
+
$neq: "not",
|
|
16
|
+
$cont: "contains",
|
|
17
|
+
$ends: "endsWith",
|
|
18
|
+
$gt: "gt",
|
|
19
|
+
$gte: "gte",
|
|
20
|
+
$in: "in",
|
|
21
|
+
$lt: "lt",
|
|
22
|
+
$lte: "lte",
|
|
23
|
+
$notin: "notIn",
|
|
24
|
+
$starts: "startsWith",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const isDateString = (value: string) => /^\d{4}-[01]\d-[0-3]\d(?:T[0-2](?:\d:[0-5]){2}\d(?:\.\d+)?(?:Z|[+-][0-2]\d(?::?[0-5]\d)?)?)?$/g.test(value);
|
|
28
|
+
|
|
29
|
+
const getSearchValue = (originalValue: any): SearchCondition => {
|
|
30
|
+
if (isDateString(originalValue)) {
|
|
31
|
+
return new Date(originalValue);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof originalValue === "string" && originalValue === "$isnull") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return originalValue;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const isRelation = (key: string, manyRelations: string[]): boolean => {
|
|
42
|
+
// Get the key containing . and remove the property name
|
|
43
|
+
const splitKey = key.split(".");
|
|
44
|
+
splitKey.splice(-1, 1);
|
|
45
|
+
|
|
46
|
+
return manyRelations.includes(splitKey.join("."));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const parseSimpleField = (value: Condition): undefined | { [key: string]: Condition } => {
|
|
50
|
+
const operator = Object.keys(value)[0];
|
|
51
|
+
const prismaOperator: undefined | PrismaWhereOperator = operatorsAssociation[operator as keyof typeof operatorsAssociation];
|
|
52
|
+
|
|
53
|
+
if (prismaOperator) {
|
|
54
|
+
return {
|
|
55
|
+
[prismaOperator]: value[operator as string],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const parseRelation = (
|
|
63
|
+
value: string | number | boolean | Date | Condition | WhereCondition,
|
|
64
|
+
key: string,
|
|
65
|
+
parsed: PrismaWhereField,
|
|
66
|
+
manyRelations: string[],
|
|
67
|
+
) => {
|
|
68
|
+
// Reverse the keys so that we can format our object by nesting
|
|
69
|
+
const fields = key.split(".").reverse();
|
|
70
|
+
|
|
71
|
+
let formatFields: { [key: string]: any } = {};
|
|
72
|
+
|
|
73
|
+
fields.forEach((field, index) => {
|
|
74
|
+
// If we iterate over the property name, which is index 0, we parse it like a normal field
|
|
75
|
+
if (index === 0) {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
77
|
+
basicParse(value, field, formatFields, manyRelations);
|
|
78
|
+
// Else we format the relation filter in the prisma way
|
|
79
|
+
} else {
|
|
80
|
+
formatFields = {
|
|
81
|
+
[field]: {
|
|
82
|
+
some: formatFields,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Retrieve the main relation field
|
|
89
|
+
const initialFieldKey = fields.reverse()[0] as string;
|
|
90
|
+
// Retrieve the old parsed version
|
|
91
|
+
const oldParsed = parsed[initialFieldKey] as PrismaRelationFilter;
|
|
92
|
+
|
|
93
|
+
// Format correctly in the prisma way
|
|
94
|
+
// eslint-disable-next-line no-param-reassign
|
|
95
|
+
parsed[initialFieldKey] = {
|
|
96
|
+
some: {
|
|
97
|
+
...(oldParsed?.some as object),
|
|
98
|
+
...(formatFields[initialFieldKey as string]?.some as object),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const parseObjectCombination = (object: Condition, manyRelations: string[]): PrismaFieldFilter => {
|
|
104
|
+
const parsed: PrismaFieldFilter = {};
|
|
105
|
+
|
|
106
|
+
Object.keys(object).forEach((key) => {
|
|
107
|
+
const value = object[key];
|
|
108
|
+
|
|
109
|
+
if (isRelation(key, manyRelations)) {
|
|
110
|
+
parseRelation(value, key, parsed, manyRelations);
|
|
111
|
+
} else if (isPrimitive(value)) {
|
|
112
|
+
parsed[key] = value as SearchCondition;
|
|
113
|
+
} else if (isObject(value)) {
|
|
114
|
+
const fieldResult = parseSimpleField(value as Condition);
|
|
115
|
+
|
|
116
|
+
if (fieldResult) {
|
|
117
|
+
parsed[key] = fieldResult;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return parsed;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const basicParse = (value: string | number | boolean | Condition | Date | WhereCondition, key: string, parsed: PrismaWhereField, manyRelations: string[]) => {
|
|
126
|
+
if (isPrimitive(value)) {
|
|
127
|
+
// eslint-disable-next-line no-param-reassign
|
|
128
|
+
parsed[key] = getSearchValue(value);
|
|
129
|
+
} else {
|
|
130
|
+
switch (key) {
|
|
131
|
+
case "$or": {
|
|
132
|
+
if (isObject(value)) {
|
|
133
|
+
// eslint-disable-next-line no-param-reassign
|
|
134
|
+
parsed.OR = parseObjectCombination(value as Condition, manyRelations);
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "$and": {
|
|
139
|
+
if (isObject(value)) {
|
|
140
|
+
// eslint-disable-next-line no-param-reassign
|
|
141
|
+
parsed.AND = parseObjectCombination(value as Condition, manyRelations);
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "$not": {
|
|
146
|
+
if (isObject(value)) {
|
|
147
|
+
// eslint-disable-next-line no-param-reassign
|
|
148
|
+
parsed.NOT = parseObjectCombination(value as Condition, manyRelations);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
default: {
|
|
153
|
+
// eslint-disable-next-line no-param-reassign
|
|
154
|
+
parsed[key] = parseSimpleField(value as Condition);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const parsePrismaWhere = (where: WhereField, manyRelations: string[]): PrismaWhereField => {
|
|
162
|
+
const parsed: PrismaWhereField = {};
|
|
163
|
+
|
|
164
|
+
Object.keys(where).forEach((key) => {
|
|
165
|
+
const value = where[key];
|
|
166
|
+
/**
|
|
167
|
+
* If the key without property name is a relation
|
|
168
|
+
*
|
|
169
|
+
* We want the following example input:
|
|
170
|
+
*
|
|
171
|
+
* posts.author.id: 1
|
|
172
|
+
*
|
|
173
|
+
* to output
|
|
174
|
+
*
|
|
175
|
+
* {
|
|
176
|
+
* posts: {
|
|
177
|
+
* some: {
|
|
178
|
+
* author: {
|
|
179
|
+
* some: {
|
|
180
|
+
* id: 1
|
|
181
|
+
* }
|
|
182
|
+
* }
|
|
183
|
+
* }
|
|
184
|
+
* }
|
|
185
|
+
* }
|
|
186
|
+
*/
|
|
187
|
+
if (isRelation(key, manyRelations)) {
|
|
188
|
+
parseRelation(value, key, parsed, manyRelations);
|
|
189
|
+
} else {
|
|
190
|
+
basicParse(value, key, parsed, manyRelations);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return parsed;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default parsePrismaWhere;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { HttpError } from "http-errors";
|
|
2
|
+
import createHttpError from "http-errors";
|
|
3
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
4
|
+
import { ApiError } from "next/dist/server/api-utils";
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
+
|
|
7
|
+
import createHandler from "./handler/create";
|
|
8
|
+
import deleteHandler from "./handler/delete";
|
|
9
|
+
import listHandler from "./handler/list";
|
|
10
|
+
import readHandler from "./handler/read";
|
|
11
|
+
import updateHandler from "./handler/update";
|
|
12
|
+
import parseQuery from "./query-parser";
|
|
13
|
+
import type {
|
|
14
|
+
Adapter, ExecuteHandler, HandlerOptions, HandlerParameters, ParsedQueryParameters,
|
|
15
|
+
} from "./types.d";
|
|
16
|
+
import { RouteType } from "./types.d";
|
|
17
|
+
import formatResourceId from "./utils/format-resource-id";
|
|
18
|
+
import getAccessibleRoutes from "./utils/get-accessible-routes";
|
|
19
|
+
import { getResourceNameFromUrl } from "./utils/get-resource-name-from-url";
|
|
20
|
+
import getRouteType from "./utils/get-route-type";
|
|
21
|
+
import validateAdapterMethods from "./utils/validate-adapter-methods";
|
|
22
|
+
|
|
23
|
+
type ResponseConfig = { status: number; data: any };
|
|
24
|
+
|
|
25
|
+
async function baseHandler<R extends Request, Context extends unknown, T, Q extends ParsedQueryParameters = any, M extends string = string>(
|
|
26
|
+
responseExecutor: (responseOrContext: Context, responseConfig: ResponseConfig) => Promise<Response>,
|
|
27
|
+
finalExecutor: (responseOrContext: Context) => Promise<void>,
|
|
28
|
+
adapter: Adapter<T, Q>,
|
|
29
|
+
options?: HandlerOptions<M>,
|
|
30
|
+
): Promise<ExecuteHandler<R, Context>>;
|
|
31
|
+
|
|
32
|
+
async function baseHandler<R extends IncomingMessage, RResponse extends ServerResponse, T, Q extends ParsedQueryParameters = any, M extends string = string>(
|
|
33
|
+
responseExecutor: (responseOrContext: RResponse, responseConfig: ResponseConfig) => Promise<void>,
|
|
34
|
+
finalExecutor: (responseOrContext: RResponse) => Promise<void>,
|
|
35
|
+
adapter: Adapter<T, Q>,
|
|
36
|
+
options?: HandlerOptions<M>,
|
|
37
|
+
): Promise<ExecuteHandler<R, RResponse>>;
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line radar/cognitive-complexity,max-len
|
|
40
|
+
async function baseHandler<R extends { url: string; method: string }, RResponse, T, Q extends ParsedQueryParameters = any, M extends string = string>(
|
|
41
|
+
responseExecutor: (responseOrContext: RResponse, responseConfig: ResponseConfig) => Promise<RResponse>,
|
|
42
|
+
finalExecutor: (responseOrContext: RResponse) => Promise<void>,
|
|
43
|
+
adapter: Adapter<T, Q>,
|
|
44
|
+
options?: HandlerOptions<M>,
|
|
45
|
+
): Promise<ExecuteHandler<R, RResponse>> {
|
|
46
|
+
try {
|
|
47
|
+
validateAdapterMethods(adapter);
|
|
48
|
+
} catch (error_: any) {
|
|
49
|
+
const error = error_ as HttpError;
|
|
50
|
+
|
|
51
|
+
throw new ApiError(error.statusCode, error.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await adapter.init?.();
|
|
55
|
+
|
|
56
|
+
const config = {
|
|
57
|
+
formatResourceId,
|
|
58
|
+
pagination: {
|
|
59
|
+
perPage: 20,
|
|
60
|
+
},
|
|
61
|
+
...options,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const routeNames = await adapter.mapModelsToRouteNames?.();
|
|
65
|
+
const modelRoutes: { [key in M]?: string } = {};
|
|
66
|
+
|
|
67
|
+
adapter.getModels().forEach((modelName) => {
|
|
68
|
+
modelRoutes[modelName as M] = config?.models?.[modelName as M]?.name || routeNames?.[modelName] || modelName;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return async (request, responseOrContext) => {
|
|
72
|
+
const { resourceName, modelName } = getResourceNameFromUrl(request.url as string, modelRoutes);
|
|
73
|
+
|
|
74
|
+
if (!resourceName) {
|
|
75
|
+
if (process.env.NODE_ENV === "development") {
|
|
76
|
+
const mappedModels = await adapter.mapModelsToRouteNames?.();
|
|
77
|
+
|
|
78
|
+
if (typeof mappedModels === "object") {
|
|
79
|
+
throw createHttpError(404, `Resource not found, possible models: ${Object.values(mappedModels).join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw createHttpError(404, `Resource not found: ${request.url}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { routeType, resourceId } = getRouteType(request.method as string, request.url as string, resourceName);
|
|
87
|
+
|
|
88
|
+
if (routeType === null) {
|
|
89
|
+
throw createHttpError(404, `Route not found: ${request.url}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const modelConfig = options?.models?.[modelName as M];
|
|
93
|
+
|
|
94
|
+
const accessibleRoutes = getAccessibleRoutes(modelConfig?.only, modelConfig?.exclude, options?.exposeStrategy || "all");
|
|
95
|
+
|
|
96
|
+
if (!accessibleRoutes.includes(routeType)) {
|
|
97
|
+
throw createHttpError(404, `Route not found: ${request.url}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const resourceIdFormatted = modelConfig?.formatResourceId?.(resourceId as string) ?? config.formatResourceId(resourceId as string);
|
|
102
|
+
|
|
103
|
+
await adapter.connect?.();
|
|
104
|
+
|
|
105
|
+
const parsedQuery = parseQuery((request.url as string).split("?")[1]);
|
|
106
|
+
const parameters: HandlerParameters<T, Q> = {
|
|
107
|
+
adapter,
|
|
108
|
+
query: adapter.parseQuery(modelName as M, parsedQuery),
|
|
109
|
+
resourceName: modelName as string,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
let responseConfig: ResponseConfig;
|
|
114
|
+
|
|
115
|
+
switch (routeType) {
|
|
116
|
+
case RouteType.READ_ONE: {
|
|
117
|
+
responseConfig = await (config?.handlers?.get || readHandler)<T, Q>({
|
|
118
|
+
...parameters,
|
|
119
|
+
resourceId: resourceIdFormatted,
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case RouteType.READ_ALL: {
|
|
124
|
+
responseConfig = await (config?.handlers?.list || listHandler)<T, Q>({
|
|
125
|
+
...parameters,
|
|
126
|
+
query: {
|
|
127
|
+
...parameters.query,
|
|
128
|
+
page: parsedQuery.page ? Number(parsedQuery.page) : undefined,
|
|
129
|
+
limit: parsedQuery.limit ? Number(parsedQuery.limit) : undefined,
|
|
130
|
+
},
|
|
131
|
+
pagination: config.pagination,
|
|
132
|
+
});
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case RouteType.CREATE: {
|
|
136
|
+
responseConfig = await (config?.handlers?.create || createHandler)<T, Q, R>({
|
|
137
|
+
...parameters,
|
|
138
|
+
request: request as R & { body: Record<string, any> },
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case RouteType.UPDATE: {
|
|
143
|
+
responseConfig = await (config?.handlers?.update || updateHandler)<T, Q, R>({
|
|
144
|
+
...parameters,
|
|
145
|
+
resourceId: resourceIdFormatted,
|
|
146
|
+
request: request as R & { body: Partial<T> },
|
|
147
|
+
});
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case RouteType.DELETE: {
|
|
151
|
+
responseConfig = await (config?.handlers?.delete || deleteHandler)<T, Q>({
|
|
152
|
+
...parameters,
|
|
153
|
+
resourceId: resourceIdFormatted,
|
|
154
|
+
});
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
default: {
|
|
158
|
+
responseConfig = {
|
|
159
|
+
status: 404,
|
|
160
|
+
data: "Method not found",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await responseExecutor(responseOrContext, responseConfig);
|
|
166
|
+
} catch (error: any) {
|
|
167
|
+
if (adapter.handleError && !(error instanceof ApiError)) {
|
|
168
|
+
adapter.handleError(error);
|
|
169
|
+
} else {
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
await adapter.disconnect?.();
|
|
175
|
+
|
|
176
|
+
await finalExecutor(responseOrContext);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default baseHandler;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HandlerParameters } from "../types.d";
|
|
2
|
+
|
|
3
|
+
const createHandler: Handler = async ({
|
|
4
|
+
adapter, query, resourceName, request,
|
|
5
|
+
}) => {
|
|
6
|
+
const resources = await adapter.create(resourceName, request.body, query);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
data: resources,
|
|
10
|
+
status: 201,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type Handler = <T, Q, Request>(
|
|
15
|
+
parameters: HandlerParameters<T, Q> & { request: Request & { body: Record<string, any> } },
|
|
16
|
+
) => Promise<{
|
|
17
|
+
data: any;
|
|
18
|
+
status: number;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export default createHandler;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import createHttpError from "http-errors";
|
|
2
|
+
|
|
3
|
+
import type { UniqueResourceHandlerParameters } from "../types.d";
|
|
4
|
+
|
|
5
|
+
const deleteHandler: Handler = async ({
|
|
6
|
+
adapter, query, resourceName, resourceId,
|
|
7
|
+
}) => {
|
|
8
|
+
const resource = await adapter.getOne(resourceName, resourceId, query);
|
|
9
|
+
|
|
10
|
+
if (resource) {
|
|
11
|
+
const deletedResource = await adapter.delete(resourceName, resourceId, query);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
data: deletedResource,
|
|
15
|
+
status: 200,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
throw createHttpError(404, `${resourceName} ${resourceId} not found`);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type Handler = <T, Q>(
|
|
22
|
+
parameters: UniqueResourceHandlerParameters<T, Q>,
|
|
23
|
+
) => Promise<{
|
|
24
|
+
data: any;
|
|
25
|
+
status: number;
|
|
26
|
+
}>;
|
|
27
|
+
export default deleteHandler;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { paginate } from "@visulima/pagination";
|
|
2
|
+
|
|
3
|
+
import type { HandlerParameters, PaginationConfig, ParsedQueryParameters } from "../types.d";
|
|
4
|
+
|
|
5
|
+
type PaginationOptions = {
|
|
6
|
+
page: number;
|
|
7
|
+
perPage: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const listHandler: Handler = async ({
|
|
11
|
+
adapter, query, resourceName, pagination,
|
|
12
|
+
}) => {
|
|
13
|
+
let isPaginated = false;
|
|
14
|
+
let paginationOptions: PaginationOptions | undefined;
|
|
15
|
+
|
|
16
|
+
if (typeof query?.page !== "undefined") {
|
|
17
|
+
if (query?.page <= 0) {
|
|
18
|
+
throw new Error("page query must be a strictly positive number");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
paginationOptions = {
|
|
22
|
+
page: query?.page,
|
|
23
|
+
perPage: query?.limit || pagination.perPage,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (paginationOptions) {
|
|
28
|
+
isPaginated = true;
|
|
29
|
+
|
|
30
|
+
// eslint-disable-next-line no-param-reassign
|
|
31
|
+
query.skip = (paginationOptions.page - 1) * paginationOptions.perPage;
|
|
32
|
+
// eslint-disable-next-line no-param-reassign
|
|
33
|
+
query.limit = paginationOptions.perPage;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const resources = await adapter.getAll(resourceName, query);
|
|
37
|
+
|
|
38
|
+
if (isPaginated) {
|
|
39
|
+
const { page, total } = await adapter.getPaginationData(resourceName, query);
|
|
40
|
+
|
|
41
|
+
const paginator = paginate(page, (paginationOptions as PaginationOptions).perPage as number, total, resources);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
data: paginator.toJSON(),
|
|
45
|
+
status: 200,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
data: resources,
|
|
51
|
+
status: 200,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type Handler = <T, Q extends ParsedQueryParameters>(
|
|
56
|
+
parameters: HandlerParameters<T, Q> & { pagination: PaginationConfig },
|
|
57
|
+
) => Promise<{
|
|
58
|
+
data: any;
|
|
59
|
+
status: number;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
export default listHandler;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import createHttpError from "http-errors";
|
|
2
|
+
|
|
3
|
+
import type { UniqueResourceHandlerParameters } from "../types.d";
|
|
4
|
+
|
|
5
|
+
const readHandler: Handler = async ({
|
|
6
|
+
adapter, query, resourceName, resourceId,
|
|
7
|
+
}) => {
|
|
8
|
+
const resource = await adapter.getOne(resourceName, resourceId, query);
|
|
9
|
+
|
|
10
|
+
if (!resource) {
|
|
11
|
+
throw createHttpError(404, `${resourceName} ${resourceId} not found`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
data: resource,
|
|
16
|
+
status: 200,
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type Handler = <T, Q>(
|
|
21
|
+
parameters: UniqueResourceHandlerParameters<T, Q>,
|
|
22
|
+
) => Promise<{
|
|
23
|
+
data: any;
|
|
24
|
+
status: number;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export default readHandler;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import createHttpError from "http-errors";
|
|
2
|
+
|
|
3
|
+
import type { UniqueResourceHandlerParameters } from "../types.d";
|
|
4
|
+
|
|
5
|
+
const updateHandler: Handler = async ({
|
|
6
|
+
adapter, query, resourceName, resourceId, request,
|
|
7
|
+
}) => {
|
|
8
|
+
const resource = await adapter.getOne(resourceName, resourceId, query);
|
|
9
|
+
|
|
10
|
+
if (resource) {
|
|
11
|
+
const updatedResource = await adapter.update(resourceName, resourceId, request.body, query);
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
status: 201,
|
|
15
|
+
data: updatedResource,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
throw createHttpError(404, `${resourceName} ${resourceId} not found`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type Handler = <T, Q, Request>(
|
|
23
|
+
parameters: UniqueResourceHandlerParameters<T, Q> & { request: Request & { body: Partial<T> } },
|
|
24
|
+
) => Promise<{
|
|
25
|
+
data: any;
|
|
26
|
+
status: number;
|
|
27
|
+
}>;
|
|
28
|
+
|
|
29
|
+
export default updateHandler;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export { default as PrismaAdapter } from "./adapter/prisma";
|
|
2
|
+
|
|
3
|
+
export { RouteType } from "./types.d";
|
|
4
|
+
export type {
|
|
5
|
+
ParsedQueryParameters,
|
|
6
|
+
PaginationConfig,
|
|
7
|
+
HandlerParameters,
|
|
8
|
+
Adapter,
|
|
9
|
+
ModelsOptions,
|
|
10
|
+
HandlerOptions as CrudHandlerOptions,
|
|
11
|
+
ModelOption,
|
|
12
|
+
UniqueResourceHandlerParameters,
|
|
13
|
+
Condition,
|
|
14
|
+
OrderByField,
|
|
15
|
+
OrderByOperator,
|
|
16
|
+
RecursiveField,
|
|
17
|
+
WhereField,
|
|
18
|
+
WhereOperator,
|
|
19
|
+
WhereCondition,
|
|
20
|
+
SearchCondition,
|
|
21
|
+
PaginationData,
|
|
22
|
+
} from "./types.d";
|
|
23
|
+
|
|
24
|
+
export type { SwaggerModelsConfig } from "./swagger/types.d";
|
|
25
|
+
|
|
26
|
+
export type { ModelsToOpenApiParameters } from "./swagger/adapter/prisma";
|
|
27
|
+
export { default as modelsToOpenApi } from "./swagger/adapter/prisma";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import baseHandler from "../../../base-crud-handler";
|
|
2
|
+
import type {
|
|
3
|
+
Adapter, ExecuteHandler, HandlerOptions, ParsedQueryParameters,
|
|
4
|
+
} from "../../../types.d";
|
|
5
|
+
|
|
6
|
+
async function handler<T, R extends Request, Context, Q extends ParsedQueryParameters = any, M extends string = string>(
|
|
7
|
+
adapter: Adapter<T, Q>,
|
|
8
|
+
options?: HandlerOptions<M>,
|
|
9
|
+
): Promise<ExecuteHandler<R, Context>> {
|
|
10
|
+
return baseHandler<R, Context, T, Q, M>(
|
|
11
|
+
async (_, responseConfig) => new Response(JSON.stringify(responseConfig.data), {
|
|
12
|
+
status: responseConfig.status,
|
|
13
|
+
headers: {
|
|
14
|
+
"content-type": "application/json; charset=utf-8",
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
async () => {},
|
|
18
|
+
adapter,
|
|
19
|
+
options,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default handler;
|