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,239 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import type {
|
|
3
|
+
DeepPartial,
|
|
4
|
+
EntityTarget,
|
|
5
|
+
ObjectLiteral,
|
|
6
|
+
Repository,
|
|
7
|
+
} from 'typeorm';
|
|
8
|
+
import { dataSource } from '../../db/data-source.js';
|
|
9
|
+
import { registerInRegistry } from './registry.js';
|
|
10
|
+
import {
|
|
11
|
+
buildOrder,
|
|
12
|
+
buildPagination,
|
|
13
|
+
buildSearchWheres,
|
|
14
|
+
buildWhere,
|
|
15
|
+
parseRawQuery,
|
|
16
|
+
} from './query-engine.js';
|
|
17
|
+
|
|
18
|
+
export type BeforeCreateHook = (
|
|
19
|
+
request: FastifyRequest,
|
|
20
|
+
data: Record<string, unknown>,
|
|
21
|
+
) => void | Promise<void>;
|
|
22
|
+
export type AfterCreateHook = (
|
|
23
|
+
request: FastifyRequest,
|
|
24
|
+
record: Record<string, unknown>,
|
|
25
|
+
) => void | Promise<void>;
|
|
26
|
+
export type BeforeUpdateHook = (
|
|
27
|
+
request: FastifyRequest,
|
|
28
|
+
reply: FastifyReply,
|
|
29
|
+
data: Record<string, unknown>,
|
|
30
|
+
) => void | Promise<void>;
|
|
31
|
+
export type AfterUpdateHook = (
|
|
32
|
+
request: FastifyRequest,
|
|
33
|
+
before: Record<string, unknown>,
|
|
34
|
+
after: Record<string, unknown>,
|
|
35
|
+
) => void | Promise<void>;
|
|
36
|
+
export type BeforeDeleteHook = (
|
|
37
|
+
request: FastifyRequest,
|
|
38
|
+
recordId: string,
|
|
39
|
+
) => void | Promise<void>;
|
|
40
|
+
|
|
41
|
+
export interface TypeormEntityConfig<T extends ObjectLiteral> {
|
|
42
|
+
name: string;
|
|
43
|
+
apiPrefix: string;
|
|
44
|
+
tag: string;
|
|
45
|
+
entity: EntityTarget<T>;
|
|
46
|
+
primaryKey?: string;
|
|
47
|
+
searchableFields?: string[];
|
|
48
|
+
readonly?: boolean;
|
|
49
|
+
bulkOperations?: boolean;
|
|
50
|
+
beforeCreate?: BeforeCreateHook;
|
|
51
|
+
afterCreate?: AfterCreateHook;
|
|
52
|
+
beforeUpdate?: BeforeUpdateHook;
|
|
53
|
+
afterUpdate?: AfterUpdateHook;
|
|
54
|
+
beforeDelete?: BeforeDeleteHook;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function columnNames<T extends ObjectLiteral>(
|
|
58
|
+
repo: Repository<T>,
|
|
59
|
+
): Set<string> {
|
|
60
|
+
return new Set(repo.metadata.columns.map((c) => c.propertyName));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function registerEntityRoutes<T extends ObjectLiteral>(
|
|
64
|
+
app: FastifyInstance,
|
|
65
|
+
config: TypeormEntityConfig<T>,
|
|
66
|
+
): void {
|
|
67
|
+
registerInRegistry({ name: config.name, apiPrefix: config.apiPrefix });
|
|
68
|
+
const pk = config.primaryKey ?? 'id';
|
|
69
|
+
|
|
70
|
+
function repo(): Repository<T> {
|
|
71
|
+
return dataSource.getRepository(config.entity);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
app.get('/', { schema: { tags: [config.tag] } }, async (request, reply) => {
|
|
75
|
+
const rawQs = request.url.split('?')[1] ?? '';
|
|
76
|
+
const query = parseRawQuery(rawQs);
|
|
77
|
+
const r = repo();
|
|
78
|
+
const cols = columnNames(r);
|
|
79
|
+
const filterWhere = buildWhere<T>(cols, query.filters);
|
|
80
|
+
const searchWheres = buildSearchWheres<T>(
|
|
81
|
+
config.searchableFields ?? [],
|
|
82
|
+
query.search,
|
|
83
|
+
);
|
|
84
|
+
const where =
|
|
85
|
+
searchWheres.length > 0
|
|
86
|
+
? searchWheres.map((s) => ({ ...filterWhere, ...s }))
|
|
87
|
+
: filterWhere;
|
|
88
|
+
const order = buildOrder<T>(cols, query.order_by);
|
|
89
|
+
const [rows, count] = await r.findAndCount({
|
|
90
|
+
where,
|
|
91
|
+
order,
|
|
92
|
+
skip: (query.page - 1) * query.page_size,
|
|
93
|
+
take: query.page_size,
|
|
94
|
+
});
|
|
95
|
+
return reply.send({
|
|
96
|
+
data: rows,
|
|
97
|
+
pagination: buildPagination(query.page, query.page_size, count),
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
app.get(
|
|
102
|
+
'/:id',
|
|
103
|
+
{ schema: { tags: [config.tag] } },
|
|
104
|
+
async (request, reply) => {
|
|
105
|
+
const { id } = request.params as { id: string };
|
|
106
|
+
const r = repo();
|
|
107
|
+
const record = await r.findOne({ where: { [pk]: id } as never });
|
|
108
|
+
if (!record)
|
|
109
|
+
return reply
|
|
110
|
+
.status(404)
|
|
111
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
112
|
+
return reply.send(record);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (config.readonly) return;
|
|
117
|
+
|
|
118
|
+
app.post('/', { schema: { tags: [config.tag] } }, async (request, reply) => {
|
|
119
|
+
const data = request.body as Record<string, unknown>;
|
|
120
|
+
await config.beforeCreate?.(request, data);
|
|
121
|
+
const r = repo();
|
|
122
|
+
const entity = r.create(data as DeepPartial<T>);
|
|
123
|
+
const record = (await r.save(entity)) as unknown as Record<string, unknown>;
|
|
124
|
+
if (config.afterCreate) {
|
|
125
|
+
try {
|
|
126
|
+
await config.afterCreate(request, record);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
request.log.error(
|
|
129
|
+
{
|
|
130
|
+
err,
|
|
131
|
+
entity: config.name,
|
|
132
|
+
record_id: (record as { id?: string }).id,
|
|
133
|
+
},
|
|
134
|
+
'afterCreate hook failed (record persisted; hook is best-effort)',
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return reply.status(201).send(record);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
app.patch(
|
|
142
|
+
'/:id',
|
|
143
|
+
{ schema: { tags: [config.tag] } },
|
|
144
|
+
async (request, reply) => {
|
|
145
|
+
const { id } = request.params as { id: string };
|
|
146
|
+
const data = request.body as Record<string, unknown>;
|
|
147
|
+
if (!data || Object.keys(data).length === 0) {
|
|
148
|
+
return reply.status(400).send({
|
|
149
|
+
detail: 'Request body cannot be empty',
|
|
150
|
+
request_id: request.id,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (config.beforeUpdate) {
|
|
154
|
+
await config.beforeUpdate(request, reply, data);
|
|
155
|
+
if (reply.sent) return;
|
|
156
|
+
}
|
|
157
|
+
const r = repo();
|
|
158
|
+
const existing = await r.findOne({ where: { [pk]: id } as never });
|
|
159
|
+
if (!existing)
|
|
160
|
+
return reply
|
|
161
|
+
.status(404)
|
|
162
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
163
|
+
const before = { ...(existing as Record<string, unknown>) };
|
|
164
|
+
Object.assign(existing, data);
|
|
165
|
+
const saved = (await r.save(existing)) as Record<string, unknown>;
|
|
166
|
+
if (config.afterUpdate) {
|
|
167
|
+
try {
|
|
168
|
+
await config.afterUpdate(request, before, saved);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
request.log.error(
|
|
171
|
+
{ err, entity: config.name, record_id: id },
|
|
172
|
+
'afterUpdate hook failed (record persisted; hook is best-effort)',
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return reply.send(saved);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
app.delete(
|
|
181
|
+
'/:id',
|
|
182
|
+
{ schema: { tags: [config.tag] } },
|
|
183
|
+
async (request, reply) => {
|
|
184
|
+
const { id } = request.params as { id: string };
|
|
185
|
+
if (config.beforeDelete) await config.beforeDelete(request, id);
|
|
186
|
+
const r = repo();
|
|
187
|
+
const result = await r.delete({ [pk]: id } as never);
|
|
188
|
+
if (!result.affected) {
|
|
189
|
+
return reply
|
|
190
|
+
.status(404)
|
|
191
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
192
|
+
}
|
|
193
|
+
return reply.status(204).send();
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!config.bulkOperations) return;
|
|
198
|
+
|
|
199
|
+
app.post(
|
|
200
|
+
'/bulk',
|
|
201
|
+
{ schema: { tags: [config.tag] } },
|
|
202
|
+
async (request, reply) => {
|
|
203
|
+
const { items } = request.body as { items: Record<string, unknown>[] };
|
|
204
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
205
|
+
return reply.status(400).send({
|
|
206
|
+
detail: 'items must be a non-empty array',
|
|
207
|
+
request_id: request.id,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
for (const item of items) {
|
|
211
|
+
await config.beforeCreate?.(request, item);
|
|
212
|
+
}
|
|
213
|
+
const r = repo();
|
|
214
|
+
const entities = r.create(items as DeepPartial<T>[]);
|
|
215
|
+
const rows = (await r.save(entities)) as unknown as Record<
|
|
216
|
+
string,
|
|
217
|
+
unknown
|
|
218
|
+
>[];
|
|
219
|
+
return reply.status(201).send({ data: rows, count: rows.length });
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
app.delete(
|
|
224
|
+
'/bulk',
|
|
225
|
+
{ schema: { tags: [config.tag] } },
|
|
226
|
+
async (request, reply) => {
|
|
227
|
+
const { ids } = request.body as { ids: string[] };
|
|
228
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
229
|
+
return reply.status(400).send({
|
|
230
|
+
detail: 'ids must be a non-empty array',
|
|
231
|
+
request_id: request.id,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const r = repo();
|
|
235
|
+
await r.delete(ids as never);
|
|
236
|
+
return reply.status(204).send();
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { registerEntityRoutes } from './auto-routes.js';
|
|
2
|
+
export type {
|
|
3
|
+
TypeormEntityConfig,
|
|
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
|
+
buildOrder,
|
|
14
|
+
buildPagination,
|
|
15
|
+
buildSearchWheres,
|
|
16
|
+
buildWhere,
|
|
17
|
+
parseRawQuery,
|
|
18
|
+
} from './query-engine.js';
|
|
19
|
+
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 TypeORM 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: 'typeorm' });
|
|
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 {
|
|
2
|
+
Column,
|
|
3
|
+
CreateDateColumn,
|
|
4
|
+
Entity,
|
|
5
|
+
PrimaryGeneratedColumn,
|
|
6
|
+
UpdateDateColumn,
|
|
7
|
+
} from 'typeorm';
|
|
8
|
+
|
|
9
|
+
@Entity({ name: '__TABLE_NAME__' })
|
|
10
|
+
export class __ENTITY_PASCAL__ {
|
|
11
|
+
@PrimaryGeneratedColumn('uuid')
|
|
12
|
+
id!: string;
|
|
13
|
+
|
|
14
|
+
__COLUMN_DECORATORS__
|
|
15
|
+
|
|
16
|
+
@CreateDateColumn({ name: 'created_at' })
|
|
17
|
+
createdAt!: Date;
|
|
18
|
+
|
|
19
|
+
@UpdateDateColumn({ name: 'updated_at' })
|
|
20
|
+
updatedAt!: Date;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import { __ENTITY_PASCAL__ } from '../../entities/__ENTITY_KEBAB__.js';
|
|
3
|
+
import { registerEntityRoutes } from '../_base/index.js';
|
|
4
|
+
|
|
5
|
+
export function register__ENTITY_PASCAL__Entity(app: Express): void {
|
|
6
|
+
app.use(
|
|
7
|
+
'/api/v1__API_PREFIX__',
|
|
8
|
+
registerEntityRoutes({
|
|
9
|
+
name: '__ENTITY_PASCAL__',
|
|
10
|
+
apiPrefix: '__API_PREFIX__',
|
|
11
|
+
tag: '__TAG__',
|
|
12
|
+
entity: __ENTITY_PASCAL__,
|
|
13
|
+
searchableFields: [__SEARCHABLE_FIELDS_ARRAY__],
|
|
14
|
+
bulkOperations: __BULK_OPERATIONS__,
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
|
|
2
|
+
import request from 'supertest';
|
|
3
|
+
import { buildApp } from '../src/app.js';
|
|
4
|
+
import { __ENTITY_PASCAL__ } from '../src/entities/__ENTITY_KEBAB__.js';
|
|
5
|
+
import { dataSource, closeDatabase } from '../src/db/data-source.js';
|
|
6
|
+
|
|
7
|
+
const app = buildApp();
|
|
8
|
+
|
|
9
|
+
describe('__ENTITY_PASCAL__ CRUD', () => {
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
if (!dataSource.isInitialized) await dataSource.initialize();
|
|
12
|
+
await dataSource.synchronize(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
await dataSource.getRepository(__ENTITY_PASCAL__).clear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await closeDatabase();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('POST creates a record', async () => {
|
|
24
|
+
const res = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
25
|
+
expect(res.status).toBe(201);
|
|
26
|
+
expect(res.body).toMatchObject(__SAMPLE_PAYLOAD__);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('GET / lists records with pagination', async () => {
|
|
30
|
+
await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
31
|
+
const res = await request(app).get('/api/v1__API_PREFIX__');
|
|
32
|
+
expect(res.status).toBe(200);
|
|
33
|
+
expect(res.body.data).toHaveLength(1);
|
|
34
|
+
expect(res.body.pagination).toMatchObject({ current_page: 1, total_records: 1 });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('GET /:id returns one record', async () => {
|
|
38
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
39
|
+
const { id } = created.body as { id: string };
|
|
40
|
+
const res = await request(app).get(`/api/v1__API_PREFIX__/${id}`);
|
|
41
|
+
expect(res.status).toBe(200);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('GET /:id returns 404 when not found', async () => {
|
|
45
|
+
const res = await request(app).get(
|
|
46
|
+
'/api/v1__API_PREFIX__/00000000-0000-0000-0000-000000000000',
|
|
47
|
+
);
|
|
48
|
+
expect(res.status).toBe(404);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('PATCH /:id updates a record', async () => {
|
|
52
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
53
|
+
const { id } = created.body as { id: string };
|
|
54
|
+
const res = await request(app).patch(`/api/v1__API_PREFIX__/${id}`).send(__UPDATE_PAYLOAD__);
|
|
55
|
+
expect(res.status).toBe(200);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('DELETE /:id removes a record', async () => {
|
|
59
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
60
|
+
const { id } = created.body as { id: string };
|
|
61
|
+
const del = await request(app).delete(`/api/v1__API_PREFIX__/${id}`);
|
|
62
|
+
expect(del.status).toBe(204);
|
|
63
|
+
const get = await request(app).get(`/api/v1__API_PREFIX__/${id}`);
|
|
64
|
+
expect(get.status).toBe(404);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { __ENTITY_PASCAL__ } from '../../entities/__ENTITY_KEBAB__.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
|
+
entity: __ENTITY_PASCAL__,
|
|
13
|
+
searchableFields: [__SEARCHABLE_FIELDS_ARRAY__],
|
|
14
|
+
bulkOperations: __BULK_OPERATIONS__,
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
{ prefix: '/api/v1__API_PREFIX__' },
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { __ENTITY_PASCAL__ } from '../../src/entities/__ENTITY_KEBAB__.js';
|
|
5
|
+
import { dataSource } from '../../src/db/data-source.js';
|
|
6
|
+
|
|
7
|
+
describe('__ENTITY_PASCAL__ CRUD', () => {
|
|
8
|
+
let app: FastifyInstance;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
app = await buildApp({ logger: false });
|
|
12
|
+
await dataSource.synchronize(true);
|
|
13
|
+
await app.ready();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await dataSource.getRepository(__ENTITY_PASCAL__).clear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(async () => {
|
|
21
|
+
await app.close();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('POST creates a record', async () => {
|
|
25
|
+
const res = await app.inject({
|
|
26
|
+
method: 'POST',
|
|
27
|
+
url: '/api/v1__API_PREFIX__',
|
|
28
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
29
|
+
});
|
|
30
|
+
expect(res.statusCode).toBe(201);
|
|
31
|
+
expect(res.json()).toMatchObject(__SAMPLE_PAYLOAD__);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('GET / lists records with pagination', async () => {
|
|
35
|
+
await app.inject({ method: 'POST', url: '/api/v1__API_PREFIX__', payload: __SAMPLE_PAYLOAD__ });
|
|
36
|
+
const res = await app.inject({ method: 'GET', url: '/api/v1__API_PREFIX__' });
|
|
37
|
+
expect(res.statusCode).toBe(200);
|
|
38
|
+
const body = res.json();
|
|
39
|
+
expect(body.data).toHaveLength(1);
|
|
40
|
+
expect(body.pagination).toMatchObject({ current_page: 1, total_records: 1 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('GET /:id returns one record', async () => {
|
|
44
|
+
const created = await app.inject({
|
|
45
|
+
method: 'POST',
|
|
46
|
+
url: '/api/v1__API_PREFIX__',
|
|
47
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
48
|
+
});
|
|
49
|
+
const { id } = created.json() as { id: string };
|
|
50
|
+
const res = await app.inject({ method: 'GET', url: `/api/v1__API_PREFIX__/${id}` });
|
|
51
|
+
expect(res.statusCode).toBe(200);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('GET /:id returns 404 when not found', async () => {
|
|
55
|
+
const res = await app.inject({
|
|
56
|
+
method: 'GET',
|
|
57
|
+
url: '/api/v1__API_PREFIX__/00000000-0000-0000-0000-000000000000',
|
|
58
|
+
});
|
|
59
|
+
expect(res.statusCode).toBe(404);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('PATCH /:id updates a record', async () => {
|
|
63
|
+
const created = await app.inject({
|
|
64
|
+
method: 'POST',
|
|
65
|
+
url: '/api/v1__API_PREFIX__',
|
|
66
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
67
|
+
});
|
|
68
|
+
const { id } = created.json() as { id: string };
|
|
69
|
+
const res = await app.inject({
|
|
70
|
+
method: 'PATCH',
|
|
71
|
+
url: `/api/v1__API_PREFIX__/${id}`,
|
|
72
|
+
payload: __UPDATE_PAYLOAD__,
|
|
73
|
+
});
|
|
74
|
+
expect(res.statusCode).toBe(200);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('DELETE /:id removes a record', async () => {
|
|
78
|
+
const created = await app.inject({
|
|
79
|
+
method: 'POST',
|
|
80
|
+
url: '/api/v1__API_PREFIX__',
|
|
81
|
+
payload: __SAMPLE_PAYLOAD__,
|
|
82
|
+
});
|
|
83
|
+
const { id } = created.json() as { id: string };
|
|
84
|
+
const del = await app.inject({ method: 'DELETE', url: `/api/v1__API_PREFIX__/${id}` });
|
|
85
|
+
expect(del.statusCode).toBe(204);
|
|
86
|
+
const get = await app.inject({ method: 'GET', url: `/api/v1__API_PREFIX__/${id}` });
|
|
87
|
+
expect(get.statusCode).toBe(404);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typeorm",
|
|
3
|
+
"displayName": "TypeORM",
|
|
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
|
+
"tsconfig.json",
|
|
13
|
+
"tests/modules/audit-logs.test.ts",
|
|
14
|
+
"tests/modules/audit-middleware.test.ts",
|
|
15
|
+
"tests/modules/auto-routes.test.ts",
|
|
16
|
+
"tests/modules/entity-validation.test.ts",
|
|
17
|
+
"tests/modules/expand.test.ts",
|
|
18
|
+
"tests/modules/field-privacy.test.ts",
|
|
19
|
+
"tests/modules/meta.test.ts",
|
|
20
|
+
"tests/modules/query-engine.test.ts",
|
|
21
|
+
"tests/modules/repository.test.ts",
|
|
22
|
+
"tests/modules/service.test.ts",
|
|
23
|
+
"tests/helpers/crud-test-base.ts",
|
|
24
|
+
"tests/helpers/crud-test-base.test.ts",
|
|
25
|
+
"tests/helpers/migration-checksum.ts",
|
|
26
|
+
"tests/helpers/migration-checksum.test.ts",
|
|
27
|
+
"tests/global-setup.ts",
|
|
28
|
+
"tests"
|
|
29
|
+
],
|
|
30
|
+
"packageOverrides": {
|
|
31
|
+
"descriptionReplace": { "from": "Prisma", "to": "TypeORM" },
|
|
32
|
+
"removeDependencies": ["@prisma/client"],
|
|
33
|
+
"removeDevDependencies": ["prisma"],
|
|
34
|
+
"addDependencies": {
|
|
35
|
+
"typeorm": "^0.3.20",
|
|
36
|
+
"reflect-metadata": "^0.2.2",
|
|
37
|
+
"pg": "^8.16.3"
|
|
38
|
+
},
|
|
39
|
+
"addDevDependencies": {
|
|
40
|
+
"@types/pg": "^8.15.5"
|
|
41
|
+
},
|
|
42
|
+
"removeScriptPrefixes": ["prisma:"],
|
|
43
|
+
"addScripts": {
|
|
44
|
+
"db:sync": "tsx scripts/db-sync.ts",
|
|
45
|
+
"db:migrate:generate": "typeorm-ts-node-esm migration:generate -d src/db/data-source.ts",
|
|
46
|
+
"db:migrate": "typeorm-ts-node-esm migration:run -d src/db/data-source.ts"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dockerfile": {
|
|
50
|
+
"extraConfigFiles": [],
|
|
51
|
+
"migrateCommand": "tsx scripts/db-sync.ts"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { dataSource } from '../src/db/data-source.js';
|
|
3
|
+
|
|
4
|
+
async function main(): Promise<void> {
|
|
5
|
+
await dataSource.initialize();
|
|
6
|
+
await dataSource.synchronize();
|
|
7
|
+
console.log('TypeORM schema synced.');
|
|
8
|
+
await dataSource.destroy();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
main().catch((err: unknown) => {
|
|
12
|
+
console.error('db-sync failed:', err);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { DataSource } from 'typeorm';
|
|
3
|
+
import { config } from '../config.js';
|
|
4
|
+
import { entities } from '../entities/index.js';
|
|
5
|
+
|
|
6
|
+
export const dataSource = new DataSource({
|
|
7
|
+
type: 'postgres',
|
|
8
|
+
url: config.DATABASE_URL,
|
|
9
|
+
entities,
|
|
10
|
+
synchronize: false,
|
|
11
|
+
logging: false,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export async function checkDatabase(): Promise<void> {
|
|
15
|
+
if (!dataSource.isInitialized) await dataSource.initialize();
|
|
16
|
+
await dataSource.query('SELECT 1');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function closeDatabase(): Promise<void> {
|
|
20
|
+
if (dataSource.isInitialized) await dataSource.destroy();
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Aggregator for all TypeORM entity classes. The DataSource registers these
|
|
2
|
+
// in `../db/data-source.ts`. `gen entity` appends new imports + entries below the anchors.
|
|
3
|
+
|
|
4
|
+
// projx-anchor: model-imports
|
|
5
|
+
|
|
6
|
+
export const entities = [
|
|
7
|
+
// projx-anchor: model-exports
|
|
8
|
+
];
|