create-projx 1.6.4 → 1.7.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/README.md +92 -19
- package/dist/{baseline-PZM4KJJW.js → baseline-FHOZNS4D.js} +2 -2
- package/dist/{chunk-6YRBHJ2V.js → chunk-HAT7D4G2.js} +25 -8
- package/dist/{chunk-XQ7FE4U3.js → chunk-IMZKHDIL.js} +161 -19
- package/dist/index.js +1499 -276
- package/dist/{utils-AVKSTHIF.js → utils-BZGSJ7XZ.js} +5 -1
- package/package.json +13 -7
- package/src/addons/orms/drizzle/express/src/app.ts +81 -0
- package/src/addons/orms/drizzle/express/src/modules/_base/auto-routes.ts +278 -0
- package/src/addons/orms/drizzle/express/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/drizzle/express/src/server.ts +32 -0
- package/src/addons/orms/drizzle/express/tests/app.test.ts +24 -0
- package/src/addons/orms/drizzle/express/vitest.config.ts +20 -0
- package/src/addons/orms/drizzle/fastify/src/app.ts +90 -0
- package/src/addons/orms/drizzle/fastify/src/modules/_base/auto-routes.ts +268 -0
- package/src/addons/orms/drizzle/fastify/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/drizzle/fastify/tests/modules/app.test.ts +20 -0
- package/src/addons/orms/drizzle/fastify/vitest.config.ts +31 -0
- package/src/addons/orms/drizzle/gen-entity/express-router.ts +21 -0
- package/src/addons/orms/drizzle/gen-entity/express-test.ts +61 -0
- package/src/addons/orms/drizzle/gen-entity/fastify-router.ts +19 -0
- package/src/addons/orms/drizzle/gen-entity/fastify-test.ts +87 -0
- package/src/addons/orms/drizzle/manifest.json +52 -0
- package/src/addons/orms/drizzle/shared/drizzle.config.ts +12 -0
- package/src/addons/orms/drizzle/shared/src/db/client.ts +17 -0
- package/src/addons/orms/drizzle/shared/src/db/schema.ts +14 -0
- package/src/addons/orms/drizzle/shared/src/modules/_base/query-engine.ts +115 -0
- package/src/addons/orms/drizzle/shared/src/modules/_base/registry.ts +15 -0
- package/src/addons/orms/sequelize/express/src/app.ts +82 -0
- package/src/addons/orms/sequelize/express/src/modules/_base/auto-routes.ts +226 -0
- package/src/addons/orms/sequelize/express/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/sequelize/express/src/server.ts +32 -0
- package/src/addons/orms/sequelize/express/tests/app.test.ts +24 -0
- package/src/addons/orms/sequelize/express/vitest.config.ts +20 -0
- package/src/addons/orms/sequelize/fastify/src/app.ts +83 -0
- package/src/addons/orms/sequelize/fastify/src/modules/_base/auto-routes.ts +216 -0
- package/src/addons/orms/sequelize/fastify/src/modules/_base/index.ts +20 -0
- package/src/addons/orms/sequelize/fastify/tests/modules/app.test.ts +20 -0
- package/src/addons/orms/sequelize/fastify/vitest.config.ts +31 -0
- package/src/addons/orms/sequelize/gen-entity/express-router.ts +17 -0
- package/src/addons/orms/sequelize/gen-entity/express-test.ts +65 -0
- package/src/addons/orms/sequelize/gen-entity/fastify-router.ts +19 -0
- package/src/addons/orms/sequelize/gen-entity/fastify-test.ts +89 -0
- package/src/addons/orms/sequelize/gen-entity/model.ts +21 -0
- package/src/addons/orms/sequelize/manifest.json +53 -0
- package/src/addons/orms/sequelize/shared/scripts/db-sync.ts +14 -0
- package/src/addons/orms/sequelize/shared/src/db/client.ts +19 -0
- package/src/addons/orms/sequelize/shared/src/models/index.ts +9 -0
- package/src/addons/orms/sequelize/shared/src/modules/_base/query-engine.ts +101 -0
- package/src/addons/orms/sequelize/shared/src/modules/_base/registry.ts +15 -0
- package/src/addons/orms/typeorm/express/src/app.ts +82 -0
- package/src/addons/orms/typeorm/express/src/modules/_base/auto-routes.ts +249 -0
- package/src/addons/orms/typeorm/express/src/modules/_base/index.ts +19 -0
- package/src/addons/orms/typeorm/express/src/server.ts +43 -0
- package/src/addons/orms/typeorm/express/tests/app.test.ts +24 -0
- package/src/addons/orms/typeorm/express/vitest.config.ts +20 -0
- package/src/addons/orms/typeorm/fastify/src/app.ts +86 -0
- package/src/addons/orms/typeorm/fastify/src/modules/_base/auto-routes.ts +239 -0
- package/src/addons/orms/typeorm/fastify/src/modules/_base/index.ts +19 -0
- package/src/addons/orms/typeorm/fastify/tests/modules/app.test.ts +20 -0
- package/src/addons/orms/typeorm/fastify/vitest.config.ts +31 -0
- package/src/addons/orms/typeorm/gen-entity/entity.ts +21 -0
- package/src/addons/orms/typeorm/gen-entity/express-router.ts +17 -0
- package/src/addons/orms/typeorm/gen-entity/express-test.ts +66 -0
- package/src/addons/orms/typeorm/gen-entity/fastify-router.ts +19 -0
- package/src/addons/orms/typeorm/gen-entity/fastify-test.ts +89 -0
- package/src/addons/orms/typeorm/manifest.json +53 -0
- package/src/addons/orms/typeorm/shared/scripts/db-sync.ts +14 -0
- package/src/addons/orms/typeorm/shared/src/db/data-source.ts +21 -0
- package/src/addons/orms/typeorm/shared/src/entities/index.ts +8 -0
- package/src/addons/orms/typeorm/shared/src/modules/_base/query-engine.ts +94 -0
- package/src/addons/orms/typeorm/shared/src/modules/_base/registry.ts +15 -0
- package/src/addons/orms/typeorm/shared/tsconfig.json +16 -0
- package/src/templates/README.md.ejs +21 -4
- package/src/templates/ci.yml.ejs +167 -37
- package/src/templates/docker-compose.yml.ejs +72 -5
- package/src/templates/pre-commit.ejs +28 -4
- package/src/templates/setup.sh.ejs +95 -6
- package/src/templates/docker-compose.dev.yml.ejs +0 -189
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import { eq, inArray, sql } from 'drizzle-orm';
|
|
3
|
+
import type { PgTable } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { registerInRegistry } from './registry.js';
|
|
5
|
+
import {
|
|
6
|
+
buildOrderBy,
|
|
7
|
+
buildPagination,
|
|
8
|
+
buildSearchWhere,
|
|
9
|
+
buildWhere,
|
|
10
|
+
combineWhere,
|
|
11
|
+
parseRawQuery,
|
|
12
|
+
} from './query-engine.js';
|
|
13
|
+
|
|
14
|
+
export type BeforeCreateHook = (
|
|
15
|
+
request: FastifyRequest,
|
|
16
|
+
data: Record<string, unknown>,
|
|
17
|
+
) => void | Promise<void>;
|
|
18
|
+
export type AfterCreateHook = (
|
|
19
|
+
request: FastifyRequest,
|
|
20
|
+
record: Record<string, unknown>,
|
|
21
|
+
) => void | Promise<void>;
|
|
22
|
+
export type BeforeUpdateHook = (
|
|
23
|
+
request: FastifyRequest,
|
|
24
|
+
reply: FastifyReply,
|
|
25
|
+
data: Record<string, unknown>,
|
|
26
|
+
) => void | Promise<void>;
|
|
27
|
+
export type AfterUpdateHook = (
|
|
28
|
+
request: FastifyRequest,
|
|
29
|
+
before: Record<string, unknown>,
|
|
30
|
+
after: Record<string, unknown>,
|
|
31
|
+
) => void | Promise<void>;
|
|
32
|
+
export type BeforeDeleteHook = (
|
|
33
|
+
request: FastifyRequest,
|
|
34
|
+
recordId: string,
|
|
35
|
+
) => void | Promise<void>;
|
|
36
|
+
|
|
37
|
+
export interface DrizzleEntityConfig {
|
|
38
|
+
name: string;
|
|
39
|
+
apiPrefix: string;
|
|
40
|
+
tag: string;
|
|
41
|
+
table: PgTable;
|
|
42
|
+
primaryKey?: string;
|
|
43
|
+
searchableFields?: string[];
|
|
44
|
+
readonly?: boolean;
|
|
45
|
+
bulkOperations?: boolean;
|
|
46
|
+
beforeCreate?: BeforeCreateHook;
|
|
47
|
+
afterCreate?: AfterCreateHook;
|
|
48
|
+
beforeUpdate?: BeforeUpdateHook;
|
|
49
|
+
afterUpdate?: AfterUpdateHook;
|
|
50
|
+
beforeDelete?: BeforeDeleteHook;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function column(table: PgTable, key: string): unknown {
|
|
54
|
+
return (table as unknown as Record<string, unknown>)[key];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pkColumn(config: DrizzleEntityConfig): unknown {
|
|
58
|
+
const key = config.primaryKey ?? 'id';
|
|
59
|
+
const col = column(config.table, key);
|
|
60
|
+
if (!col) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Primary key column "${key}" not found on table ${config.name}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return col;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function registerEntityRoutes(
|
|
69
|
+
app: FastifyInstance,
|
|
70
|
+
config: DrizzleEntityConfig,
|
|
71
|
+
): void {
|
|
72
|
+
registerInRegistry({ name: config.name, apiPrefix: config.apiPrefix });
|
|
73
|
+
const pk = pkColumn(config) as Parameters<typeof eq>[0];
|
|
74
|
+
|
|
75
|
+
app.get('/', { schema: { tags: [config.tag] } }, async (request, reply) => {
|
|
76
|
+
const rawQs = request.url.split('?')[1] ?? '';
|
|
77
|
+
const query = parseRawQuery(rawQs);
|
|
78
|
+
const filterWhere = buildWhere(config.table, query.filters);
|
|
79
|
+
const searchWhere = buildSearchWhere(
|
|
80
|
+
config.table,
|
|
81
|
+
config.searchableFields ?? [],
|
|
82
|
+
query.search,
|
|
83
|
+
);
|
|
84
|
+
const where = combineWhere(filterWhere, searchWhere);
|
|
85
|
+
const order = buildOrderBy(config.table, query.order_by);
|
|
86
|
+
const offset = (query.page - 1) * query.page_size;
|
|
87
|
+
|
|
88
|
+
const baseSelect = app.db.select().from(config.table);
|
|
89
|
+
const baseCount = app.db
|
|
90
|
+
.select({ count: sql<number>`count(*)::int` })
|
|
91
|
+
.from(config.table);
|
|
92
|
+
|
|
93
|
+
const rows = await (where
|
|
94
|
+
? baseSelect
|
|
95
|
+
.where(where)
|
|
96
|
+
.orderBy(...order)
|
|
97
|
+
.limit(query.page_size)
|
|
98
|
+
.offset(offset)
|
|
99
|
+
: baseSelect
|
|
100
|
+
.orderBy(...order)
|
|
101
|
+
.limit(query.page_size)
|
|
102
|
+
.offset(offset));
|
|
103
|
+
const [{ count }] = await (where ? baseCount.where(where) : baseCount);
|
|
104
|
+
|
|
105
|
+
return reply.send({
|
|
106
|
+
data: rows,
|
|
107
|
+
pagination: buildPagination(query.page, query.page_size, Number(count)),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
app.get(
|
|
112
|
+
'/:id',
|
|
113
|
+
{ schema: { tags: [config.tag] } },
|
|
114
|
+
async (request, reply) => {
|
|
115
|
+
const { id } = request.params as { id: string };
|
|
116
|
+
const [record] = await app.db
|
|
117
|
+
.select()
|
|
118
|
+
.from(config.table)
|
|
119
|
+
.where(eq(pk, id))
|
|
120
|
+
.limit(1);
|
|
121
|
+
if (!record)
|
|
122
|
+
return reply
|
|
123
|
+
.status(404)
|
|
124
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
125
|
+
return reply.send(record);
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (config.readonly) return;
|
|
130
|
+
|
|
131
|
+
app.post('/', { schema: { tags: [config.tag] } }, async (request, reply) => {
|
|
132
|
+
const data = request.body as Record<string, unknown>;
|
|
133
|
+
await config.beforeCreate?.(request, data);
|
|
134
|
+
const [record] = await app.db
|
|
135
|
+
.insert(config.table)
|
|
136
|
+
.values(data as never)
|
|
137
|
+
.returning();
|
|
138
|
+
if (config.afterCreate) {
|
|
139
|
+
try {
|
|
140
|
+
await config.afterCreate(request, record as Record<string, unknown>);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
request.log.error(
|
|
143
|
+
{
|
|
144
|
+
err,
|
|
145
|
+
entity: config.name,
|
|
146
|
+
record_id: (record as { id?: string }).id,
|
|
147
|
+
},
|
|
148
|
+
'afterCreate hook failed (record persisted; hook is best-effort)',
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return reply.status(201).send(record);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
app.patch(
|
|
156
|
+
'/:id',
|
|
157
|
+
{ schema: { tags: [config.tag] } },
|
|
158
|
+
async (request, reply) => {
|
|
159
|
+
const { id } = request.params as { id: string };
|
|
160
|
+
const data = request.body as Record<string, unknown>;
|
|
161
|
+
if (!data || Object.keys(data).length === 0) {
|
|
162
|
+
return reply.status(400).send({
|
|
163
|
+
detail: 'Request body cannot be empty',
|
|
164
|
+
request_id: request.id,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (config.beforeUpdate) {
|
|
168
|
+
await config.beforeUpdate(request, reply, data);
|
|
169
|
+
if (reply.sent) return;
|
|
170
|
+
}
|
|
171
|
+
let before: Record<string, unknown> | null = null;
|
|
172
|
+
if (config.afterUpdate) {
|
|
173
|
+
const [existing] = await app.db
|
|
174
|
+
.select()
|
|
175
|
+
.from(config.table)
|
|
176
|
+
.where(eq(pk, id))
|
|
177
|
+
.limit(1);
|
|
178
|
+
before = (existing as Record<string, unknown>) ?? null;
|
|
179
|
+
}
|
|
180
|
+
const [record] = await app.db
|
|
181
|
+
.update(config.table)
|
|
182
|
+
.set(data as never)
|
|
183
|
+
.where(eq(pk, id))
|
|
184
|
+
.returning();
|
|
185
|
+
if (!record) {
|
|
186
|
+
return reply
|
|
187
|
+
.status(404)
|
|
188
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
189
|
+
}
|
|
190
|
+
if (config.afterUpdate && before) {
|
|
191
|
+
try {
|
|
192
|
+
await config.afterUpdate(
|
|
193
|
+
request,
|
|
194
|
+
before,
|
|
195
|
+
record as Record<string, unknown>,
|
|
196
|
+
);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
request.log.error(
|
|
199
|
+
{ err, entity: config.name, record_id: id },
|
|
200
|
+
'afterUpdate hook failed (record persisted; hook is best-effort)',
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return reply.send(record);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
app.delete(
|
|
209
|
+
'/:id',
|
|
210
|
+
{ schema: { tags: [config.tag] } },
|
|
211
|
+
async (request, reply) => {
|
|
212
|
+
const { id } = request.params as { id: string };
|
|
213
|
+
if (config.beforeDelete) await config.beforeDelete(request, id);
|
|
214
|
+
const deleted = await app.db
|
|
215
|
+
.delete(config.table)
|
|
216
|
+
.where(eq(pk, id))
|
|
217
|
+
.returning();
|
|
218
|
+
if (deleted.length === 0) {
|
|
219
|
+
return reply
|
|
220
|
+
.status(404)
|
|
221
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
222
|
+
}
|
|
223
|
+
return reply.status(204).send();
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!config.bulkOperations) return;
|
|
228
|
+
|
|
229
|
+
app.post(
|
|
230
|
+
'/bulk',
|
|
231
|
+
{ schema: { tags: [config.tag] } },
|
|
232
|
+
async (request, reply) => {
|
|
233
|
+
const { items } = request.body as { items: Record<string, unknown>[] };
|
|
234
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
235
|
+
return reply.status(400).send({
|
|
236
|
+
detail: 'items must be a non-empty array',
|
|
237
|
+
request_id: request.id,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
for (const item of items) {
|
|
241
|
+
await config.beforeCreate?.(request, item);
|
|
242
|
+
}
|
|
243
|
+
const rows = await app.db
|
|
244
|
+
.insert(config.table)
|
|
245
|
+
.values(items as never)
|
|
246
|
+
.returning();
|
|
247
|
+
return reply.status(201).send({ data: rows, count: rows.length });
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
app.delete(
|
|
252
|
+
'/bulk',
|
|
253
|
+
{ schema: { tags: [config.tag] } },
|
|
254
|
+
async (request, reply) => {
|
|
255
|
+
const { ids } = request.body as { ids: string[] };
|
|
256
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
257
|
+
return reply.status(400).send({
|
|
258
|
+
detail: 'ids must be a non-empty array',
|
|
259
|
+
request_id: request.id,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
await app.db
|
|
263
|
+
.delete(config.table)
|
|
264
|
+
.where(inArray(pk as Parameters<typeof inArray>[0], ids));
|
|
265
|
+
return reply.status(204).send();
|
|
266
|
+
},
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { registerEntityRoutes } from './auto-routes.js';
|
|
2
|
+
export type {
|
|
3
|
+
DrizzleEntityConfig,
|
|
4
|
+
BeforeCreateHook,
|
|
5
|
+
AfterCreateHook,
|
|
6
|
+
BeforeUpdateHook,
|
|
7
|
+
AfterUpdateHook,
|
|
8
|
+
BeforeDeleteHook,
|
|
9
|
+
} from './auto-routes.js';
|
|
10
|
+
export { listEntities, registerInRegistry } from './registry.js';
|
|
11
|
+
export type { RegisteredEntity } from './registry.js';
|
|
12
|
+
export {
|
|
13
|
+
buildOrderBy,
|
|
14
|
+
buildPagination,
|
|
15
|
+
buildSearchWhere,
|
|
16
|
+
buildWhere,
|
|
17
|
+
combineWhere,
|
|
18
|
+
parseRawQuery,
|
|
19
|
+
} from './query-engine.js';
|
|
20
|
+
export type { ParsedQuery, PaginationMeta } from './query-engine.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it, afterEach } from 'vitest';
|
|
2
|
+
import type { FastifyInstance } from 'fastify';
|
|
3
|
+
import { buildApp } from '../../src/app.js';
|
|
4
|
+
|
|
5
|
+
describe('Fastify Drizzle app', () => {
|
|
6
|
+
let app: FastifyInstance | undefined;
|
|
7
|
+
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
await app?.close();
|
|
10
|
+
app = undefined;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('exposes empty generated metadata until entities are added', async () => {
|
|
14
|
+
app = await buildApp({ logger: false });
|
|
15
|
+
const res = await app.inject({ method: 'GET', url: '/api/v1/_meta' });
|
|
16
|
+
|
|
17
|
+
expect(res.statusCode).toBe(200);
|
|
18
|
+
expect(res.json()).toEqual({ entities: [], orm: 'drizzle' });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
|
|
4
|
+
config({ path: '.env.test' });
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
globals: true,
|
|
9
|
+
environment: 'node',
|
|
10
|
+
include: ['tests/**/*.test.ts'],
|
|
11
|
+
coverage: {
|
|
12
|
+
provider: 'v8',
|
|
13
|
+
include: ['src/**/*.ts'],
|
|
14
|
+
exclude: [
|
|
15
|
+
'src/server.ts',
|
|
16
|
+
'src/app.ts',
|
|
17
|
+
'src/config.ts',
|
|
18
|
+
'src/plugins/swagger.ts',
|
|
19
|
+
],
|
|
20
|
+
thresholds: {
|
|
21
|
+
statements: 80,
|
|
22
|
+
branches: 80,
|
|
23
|
+
functions: 80,
|
|
24
|
+
lines: 80,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
pool: 'forks',
|
|
28
|
+
testTimeout: 15000,
|
|
29
|
+
hookTimeout: 15000,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import type { DbClient } from '../../db/client.js';
|
|
3
|
+
import { __TABLE_CAMEL__ } from '../../db/schema.js';
|
|
4
|
+
import { registerEntityRoutes } from '../_base/index.js';
|
|
5
|
+
|
|
6
|
+
export function register__ENTITY_PASCAL__Entity(app: Express, db: DbClient): void {
|
|
7
|
+
app.use(
|
|
8
|
+
'/api/v1__API_PREFIX__',
|
|
9
|
+
registerEntityRoutes(
|
|
10
|
+
{
|
|
11
|
+
name: '__ENTITY_PASCAL__',
|
|
12
|
+
apiPrefix: '__API_PREFIX__',
|
|
13
|
+
tag: '__TAG__',
|
|
14
|
+
table: __TABLE_CAMEL__,
|
|
15
|
+
searchableFields: [__SEARCHABLE_FIELDS_ARRAY__],
|
|
16
|
+
bulkOperations: __BULK_OPERATIONS__,
|
|
17
|
+
},
|
|
18
|
+
db,
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import { buildApp } from '../src/app.js';
|
|
4
|
+
import { db, closeDatabase } from '../src/db/client.js';
|
|
5
|
+
import { __TABLE_CAMEL__ } from '../src/db/schema.js';
|
|
6
|
+
|
|
7
|
+
const app = buildApp();
|
|
8
|
+
|
|
9
|
+
describe('__ENTITY_PASCAL__ CRUD', () => {
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await db.delete(__TABLE_CAMEL__);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
await closeDatabase();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('POST creates a record', async () => {
|
|
19
|
+
const res = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
20
|
+
expect(res.status).toBe(201);
|
|
21
|
+
expect(res.body).toMatchObject(__SAMPLE_PAYLOAD__);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('GET / lists records with pagination', async () => {
|
|
25
|
+
await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
26
|
+
const res = await request(app).get('/api/v1__API_PREFIX__');
|
|
27
|
+
expect(res.status).toBe(200);
|
|
28
|
+
expect(res.body.data).toHaveLength(1);
|
|
29
|
+
expect(res.body.pagination).toMatchObject({ current_page: 1, total_records: 1 });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('GET /:id returns one record', async () => {
|
|
33
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
34
|
+
const { id } = created.body as { id: string };
|
|
35
|
+
const res = await request(app).get(`/api/v1__API_PREFIX__/${id}`);
|
|
36
|
+
expect(res.status).toBe(200);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('GET /:id returns 404 when not found', async () => {
|
|
40
|
+
const res = await request(app).get(
|
|
41
|
+
'/api/v1__API_PREFIX__/00000000-0000-0000-0000-000000000000',
|
|
42
|
+
);
|
|
43
|
+
expect(res.status).toBe(404);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('PATCH /:id updates a record', async () => {
|
|
47
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
48
|
+
const { id } = created.body as { id: string };
|
|
49
|
+
const res = await request(app).patch(`/api/v1__API_PREFIX__/${id}`).send(__UPDATE_PAYLOAD__);
|
|
50
|
+
expect(res.status).toBe(200);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('DELETE /:id removes a record', async () => {
|
|
54
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
55
|
+
const { id } = created.body as { id: string };
|
|
56
|
+
const del = await request(app).delete(`/api/v1__API_PREFIX__/${id}`);
|
|
57
|
+
expect(del.status).toBe(204);
|
|
58
|
+
const get = await request(app).get(`/api/v1__API_PREFIX__/${id}`);
|
|
59
|
+
expect(get.status).toBe(404);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { __TABLE_CAMEL__ } from '../../db/schema.js';
|
|
3
|
+
import { registerEntityRoutes } from '../_base/index.js';
|
|
4
|
+
|
|
5
|
+
export async function register__ENTITY_PASCAL__Entity(app: FastifyInstance): Promise<void> {
|
|
6
|
+
await app.register(
|
|
7
|
+
async (instance) => {
|
|
8
|
+
registerEntityRoutes(instance, {
|
|
9
|
+
name: '__ENTITY_PASCAL__',
|
|
10
|
+
apiPrefix: '__API_PREFIX__',
|
|
11
|
+
tag: '__TAG__',
|
|
12
|
+
table: __TABLE_CAMEL__,
|
|
13
|
+
searchableFields: [__SEARCHABLE_FIELDS_ARRAY__],
|
|
14
|
+
bulkOperations: __BULK_OPERATIONS__,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
{ prefix: '/api/v1__API_PREFIX__' },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import type { FastifyInstance } from 'fastify';
|
|
3
|
+
import { buildApp } from '../../src/app.js';
|
|
4
|
+
import { __TABLE_CAMEL__ } from '../../src/db/schema.js';
|
|
5
|
+
|
|
6
|
+
describe('__ENTITY_PASCAL__ CRUD', () => {
|
|
7
|
+
let app: FastifyInstance;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
app = await buildApp({ logger: false });
|
|
11
|
+
await app.ready();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await app.db.delete(__TABLE_CAMEL__);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await app.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('POST creates a record', async () => {
|
|
23
|
+
const res = await app.inject({
|
|
24
|
+
method: 'POST',
|
|
25
|
+
url: '/api/v1__API_PREFIX__',
|
|
26
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
27
|
+
});
|
|
28
|
+
expect(res.statusCode).toBe(201);
|
|
29
|
+
expect(res.json()).toMatchObject(__SAMPLE_PAYLOAD__);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('GET / lists records with pagination', async () => {
|
|
33
|
+
await app.inject({ method: 'POST', url: '/api/v1__API_PREFIX__', payload: __SAMPLE_PAYLOAD__ });
|
|
34
|
+
const res = await app.inject({ method: 'GET', url: '/api/v1__API_PREFIX__' });
|
|
35
|
+
expect(res.statusCode).toBe(200);
|
|
36
|
+
const body = res.json();
|
|
37
|
+
expect(body.data).toHaveLength(1);
|
|
38
|
+
expect(body.pagination).toMatchObject({ current_page: 1, total_records: 1 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('GET /:id returns one record', async () => {
|
|
42
|
+
const created = await app.inject({
|
|
43
|
+
method: 'POST',
|
|
44
|
+
url: '/api/v1__API_PREFIX__',
|
|
45
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
46
|
+
});
|
|
47
|
+
const { id } = created.json() as { id: string };
|
|
48
|
+
const res = await app.inject({ method: 'GET', url: `/api/v1__API_PREFIX__/${id}` });
|
|
49
|
+
expect(res.statusCode).toBe(200);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('GET /:id returns 404 when not found', async () => {
|
|
53
|
+
const res = await app.inject({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
url: '/api/v1__API_PREFIX__/00000000-0000-0000-0000-000000000000',
|
|
56
|
+
});
|
|
57
|
+
expect(res.statusCode).toBe(404);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('PATCH /:id updates a record', async () => {
|
|
61
|
+
const created = await app.inject({
|
|
62
|
+
method: 'POST',
|
|
63
|
+
url: '/api/v1__API_PREFIX__',
|
|
64
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
65
|
+
});
|
|
66
|
+
const { id } = created.json() as { id: string };
|
|
67
|
+
const res = await app.inject({
|
|
68
|
+
method: 'PATCH',
|
|
69
|
+
url: `/api/v1__API_PREFIX__/${id}`,
|
|
70
|
+
payload: __UPDATE_PAYLOAD__,
|
|
71
|
+
});
|
|
72
|
+
expect(res.statusCode).toBe(200);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('DELETE /:id removes a record', async () => {
|
|
76
|
+
const created = await app.inject({
|
|
77
|
+
method: 'POST',
|
|
78
|
+
url: '/api/v1__API_PREFIX__',
|
|
79
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
80
|
+
});
|
|
81
|
+
const { id } = created.json() as { id: string };
|
|
82
|
+
const del = await app.inject({ method: 'DELETE', url: `/api/v1__API_PREFIX__/${id}` });
|
|
83
|
+
expect(del.statusCode).toBe(204);
|
|
84
|
+
const get = await app.inject({ method: 'GET', url: `/api/v1__API_PREFIX__/${id}` });
|
|
85
|
+
expect(get.statusCode).toBe(404);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "drizzle",
|
|
3
|
+
"displayName": "Drizzle",
|
|
4
|
+
"frameworks": ["fastify", "express"],
|
|
5
|
+
"removeFromBase": [
|
|
6
|
+
"prisma",
|
|
7
|
+
"src/plugins/prisma.ts",
|
|
8
|
+
"src/prisma.ts",
|
|
9
|
+
"src/lib/service-config.ts",
|
|
10
|
+
"src/modules/_base",
|
|
11
|
+
"src/modules/audit-logs",
|
|
12
|
+
"tests/modules/audit-logs.test.ts",
|
|
13
|
+
"tests/modules/audit-middleware.test.ts",
|
|
14
|
+
"tests/modules/auto-routes.test.ts",
|
|
15
|
+
"tests/modules/entity-validation.test.ts",
|
|
16
|
+
"tests/modules/expand.test.ts",
|
|
17
|
+
"tests/modules/field-privacy.test.ts",
|
|
18
|
+
"tests/modules/meta.test.ts",
|
|
19
|
+
"tests/modules/query-engine.test.ts",
|
|
20
|
+
"tests/modules/repository.test.ts",
|
|
21
|
+
"tests/modules/service.test.ts",
|
|
22
|
+
"tests/helpers/crud-test-base.ts",
|
|
23
|
+
"tests/helpers/crud-test-base.test.ts",
|
|
24
|
+
"tests/helpers/migration-checksum.ts",
|
|
25
|
+
"tests/helpers/migration-checksum.test.ts",
|
|
26
|
+
"tests/global-setup.ts",
|
|
27
|
+
"tests"
|
|
28
|
+
],
|
|
29
|
+
"packageOverrides": {
|
|
30
|
+
"descriptionReplace": { "from": "Prisma", "to": "Drizzle" },
|
|
31
|
+
"removeDependencies": ["@prisma/client"],
|
|
32
|
+
"removeDevDependencies": ["prisma"],
|
|
33
|
+
"addDependencies": {
|
|
34
|
+
"drizzle-orm": "^0.44.5",
|
|
35
|
+
"pg": "^8.16.3"
|
|
36
|
+
},
|
|
37
|
+
"addDevDependencies": {
|
|
38
|
+
"@types/pg": "^8.15.5",
|
|
39
|
+
"drizzle-kit": "^0.31.4"
|
|
40
|
+
},
|
|
41
|
+
"removeScriptPrefixes": ["prisma:"],
|
|
42
|
+
"addScripts": {
|
|
43
|
+
"db:generate": "drizzle-kit generate",
|
|
44
|
+
"db:migrate": "drizzle-kit migrate",
|
|
45
|
+
"db:push": "drizzle-kit push"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"dockerfile": {
|
|
49
|
+
"extraConfigFiles": ["drizzle.config.ts"],
|
|
50
|
+
"migrateCommand": "drizzle-kit push --force"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineConfig } from 'drizzle-kit';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
schema: './src/db/schema.ts',
|
|
5
|
+
out: './drizzle',
|
|
6
|
+
dialect: 'postgresql',
|
|
7
|
+
dbCredentials: {
|
|
8
|
+
url: process.env.DATABASE_URL ?? '',
|
|
9
|
+
},
|
|
10
|
+
strict: true,
|
|
11
|
+
verbose: true,
|
|
12
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { drizzle } from 'drizzle-orm/node-postgres';
|
|
2
|
+
import { Pool } from 'pg';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import * as schema from './schema.js';
|
|
5
|
+
|
|
6
|
+
export const pool = new Pool({ connectionString: config.DATABASE_URL });
|
|
7
|
+
export const db = drizzle(pool, { schema });
|
|
8
|
+
|
|
9
|
+
export async function checkDatabase(): Promise<void> {
|
|
10
|
+
await pool.query('SELECT 1');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function closeDatabase(): Promise<void> {
|
|
14
|
+
await pool.end();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DbClient = typeof db;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsonb, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
2
|
+
|
|
3
|
+
export const auditLogs = pgTable('audit_logs', {
|
|
4
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
5
|
+
tableName: text('table_name').notNull(),
|
|
6
|
+
recordId: text('record_id').notNull(),
|
|
7
|
+
action: text('action').notNull(),
|
|
8
|
+
oldValue: jsonb('old_value'),
|
|
9
|
+
newValue: jsonb('new_value'),
|
|
10
|
+
performedBy: text('performed_by').notNull().default('system'),
|
|
11
|
+
createdAt: timestamp('created_at', { withTimezone: true })
|
|
12
|
+
.notNull()
|
|
13
|
+
.defaultNow(),
|
|
14
|
+
});
|