create-projx 1.6.5 → 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 +75 -1
- package/src/templates/docker-compose.dev.yml.ejs +0 -189
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
import type { Model, ModelStatic } from 'sequelize';
|
|
3
|
+
import { registerInRegistry } from './registry.js';
|
|
4
|
+
import {
|
|
5
|
+
buildOrder,
|
|
6
|
+
buildPagination,
|
|
7
|
+
buildSearchWhere,
|
|
8
|
+
buildWhere,
|
|
9
|
+
combineWhere,
|
|
10
|
+
parseRawQuery,
|
|
11
|
+
} from './query-engine.js';
|
|
12
|
+
|
|
13
|
+
export type BeforeCreateHook = (
|
|
14
|
+
request: FastifyRequest,
|
|
15
|
+
data: Record<string, unknown>,
|
|
16
|
+
) => void | Promise<void>;
|
|
17
|
+
export type AfterCreateHook = (
|
|
18
|
+
request: FastifyRequest,
|
|
19
|
+
record: Record<string, unknown>,
|
|
20
|
+
) => void | Promise<void>;
|
|
21
|
+
export type BeforeUpdateHook = (
|
|
22
|
+
request: FastifyRequest,
|
|
23
|
+
reply: FastifyReply,
|
|
24
|
+
data: Record<string, unknown>,
|
|
25
|
+
) => void | Promise<void>;
|
|
26
|
+
export type AfterUpdateHook = (
|
|
27
|
+
request: FastifyRequest,
|
|
28
|
+
before: Record<string, unknown>,
|
|
29
|
+
after: Record<string, unknown>,
|
|
30
|
+
) => void | Promise<void>;
|
|
31
|
+
export type BeforeDeleteHook = (
|
|
32
|
+
request: FastifyRequest,
|
|
33
|
+
recordId: string,
|
|
34
|
+
) => void | Promise<void>;
|
|
35
|
+
|
|
36
|
+
export interface SequelizeEntityConfig {
|
|
37
|
+
name: string;
|
|
38
|
+
apiPrefix: string;
|
|
39
|
+
tag: string;
|
|
40
|
+
model: ModelStatic<Model>;
|
|
41
|
+
primaryKey?: string;
|
|
42
|
+
searchableFields?: string[];
|
|
43
|
+
readonly?: boolean;
|
|
44
|
+
bulkOperations?: boolean;
|
|
45
|
+
beforeCreate?: BeforeCreateHook;
|
|
46
|
+
afterCreate?: AfterCreateHook;
|
|
47
|
+
beforeUpdate?: BeforeUpdateHook;
|
|
48
|
+
afterUpdate?: AfterUpdateHook;
|
|
49
|
+
beforeDelete?: BeforeDeleteHook;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function attributes(model: ModelStatic<Model>): Set<string> {
|
|
53
|
+
return new Set(Object.keys(model.getAttributes()));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerEntityRoutes(
|
|
57
|
+
app: FastifyInstance,
|
|
58
|
+
config: SequelizeEntityConfig,
|
|
59
|
+
): void {
|
|
60
|
+
registerInRegistry({ name: config.name, apiPrefix: config.apiPrefix });
|
|
61
|
+
const pk = config.primaryKey ?? 'id';
|
|
62
|
+
const attrs = attributes(config.model);
|
|
63
|
+
|
|
64
|
+
app.get('/', { schema: { tags: [config.tag] } }, async (request, reply) => {
|
|
65
|
+
const rawQs = request.url.split('?')[1] ?? '';
|
|
66
|
+
const query = parseRawQuery(rawQs);
|
|
67
|
+
const filterWhere = buildWhere(attrs, query.filters);
|
|
68
|
+
const searchWhere = buildSearchWhere(
|
|
69
|
+
config.searchableFields ?? [],
|
|
70
|
+
query.search,
|
|
71
|
+
);
|
|
72
|
+
const where = combineWhere(filterWhere, searchWhere);
|
|
73
|
+
const order = buildOrder(attrs, query.order_by);
|
|
74
|
+
const { rows, count } = await config.model.findAndCountAll({
|
|
75
|
+
where,
|
|
76
|
+
order,
|
|
77
|
+
limit: query.page_size,
|
|
78
|
+
offset: (query.page - 1) * query.page_size,
|
|
79
|
+
});
|
|
80
|
+
return reply.send({
|
|
81
|
+
data: rows.map((r) => r.toJSON()),
|
|
82
|
+
pagination: buildPagination(query.page, query.page_size, count),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
app.get(
|
|
87
|
+
'/:id',
|
|
88
|
+
{ schema: { tags: [config.tag] } },
|
|
89
|
+
async (request, reply) => {
|
|
90
|
+
const { id } = request.params as { id: string };
|
|
91
|
+
const record = await config.model.findOne({ where: { [pk]: id } });
|
|
92
|
+
if (!record)
|
|
93
|
+
return reply
|
|
94
|
+
.status(404)
|
|
95
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
96
|
+
return reply.send(record.toJSON());
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (config.readonly) return;
|
|
101
|
+
|
|
102
|
+
app.post('/', { schema: { tags: [config.tag] } }, async (request, reply) => {
|
|
103
|
+
const data = request.body as Record<string, unknown>;
|
|
104
|
+
await config.beforeCreate?.(request, data);
|
|
105
|
+
const created = await config.model.create(data);
|
|
106
|
+
const record = created.toJSON() as Record<string, unknown>;
|
|
107
|
+
if (config.afterCreate) {
|
|
108
|
+
try {
|
|
109
|
+
await config.afterCreate(request, record);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
request.log.error(
|
|
112
|
+
{
|
|
113
|
+
err,
|
|
114
|
+
entity: config.name,
|
|
115
|
+
record_id: (record as { id?: string }).id,
|
|
116
|
+
},
|
|
117
|
+
'afterCreate hook failed (record persisted; hook is best-effort)',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return reply.status(201).send(record);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.patch(
|
|
125
|
+
'/:id',
|
|
126
|
+
{ schema: { tags: [config.tag] } },
|
|
127
|
+
async (request, reply) => {
|
|
128
|
+
const { id } = request.params as { id: string };
|
|
129
|
+
const data = request.body as Record<string, unknown>;
|
|
130
|
+
if (!data || Object.keys(data).length === 0) {
|
|
131
|
+
return reply.status(400).send({
|
|
132
|
+
detail: 'Request body cannot be empty',
|
|
133
|
+
request_id: request.id,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (config.beforeUpdate) {
|
|
137
|
+
await config.beforeUpdate(request, reply, data);
|
|
138
|
+
if (reply.sent) return;
|
|
139
|
+
}
|
|
140
|
+
const existing = await config.model.findOne({ where: { [pk]: id } });
|
|
141
|
+
if (!existing)
|
|
142
|
+
return reply
|
|
143
|
+
.status(404)
|
|
144
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
145
|
+
const before = existing.toJSON() as Record<string, unknown>;
|
|
146
|
+
await existing.update(data);
|
|
147
|
+
const after = existing.toJSON() as Record<string, unknown>;
|
|
148
|
+
if (config.afterUpdate) {
|
|
149
|
+
try {
|
|
150
|
+
await config.afterUpdate(request, before, after);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
request.log.error(
|
|
153
|
+
{ err, entity: config.name, record_id: id },
|
|
154
|
+
'afterUpdate hook failed (record persisted; hook is best-effort)',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return reply.send(after);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
app.delete(
|
|
163
|
+
'/:id',
|
|
164
|
+
{ schema: { tags: [config.tag] } },
|
|
165
|
+
async (request, reply) => {
|
|
166
|
+
const { id } = request.params as { id: string };
|
|
167
|
+
if (config.beforeDelete) await config.beforeDelete(request, id);
|
|
168
|
+
const removed = await config.model.destroy({ where: { [pk]: id } });
|
|
169
|
+
if (removed === 0) {
|
|
170
|
+
return reply
|
|
171
|
+
.status(404)
|
|
172
|
+
.send({ detail: 'Not found', request_id: request.id });
|
|
173
|
+
}
|
|
174
|
+
return reply.status(204).send();
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!config.bulkOperations) return;
|
|
179
|
+
|
|
180
|
+
app.post(
|
|
181
|
+
'/bulk',
|
|
182
|
+
{ schema: { tags: [config.tag] } },
|
|
183
|
+
async (request, reply) => {
|
|
184
|
+
const { items } = request.body as { items: Record<string, unknown>[] };
|
|
185
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
186
|
+
return reply.status(400).send({
|
|
187
|
+
detail: 'items must be a non-empty array',
|
|
188
|
+
request_id: request.id,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
for (const item of items) {
|
|
192
|
+
await config.beforeCreate?.(request, item);
|
|
193
|
+
}
|
|
194
|
+
const rows = await config.model.bulkCreate(items);
|
|
195
|
+
return reply
|
|
196
|
+
.status(201)
|
|
197
|
+
.send({ data: rows.map((r) => r.toJSON()), count: rows.length });
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
app.delete(
|
|
202
|
+
'/bulk',
|
|
203
|
+
{ schema: { tags: [config.tag] } },
|
|
204
|
+
async (request, reply) => {
|
|
205
|
+
const { ids } = request.body as { ids: string[] };
|
|
206
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
207
|
+
return reply.status(400).send({
|
|
208
|
+
detail: 'ids must be a non-empty array',
|
|
209
|
+
request_id: request.id,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
await config.model.destroy({ where: { [pk]: ids } });
|
|
213
|
+
return reply.status(204).send();
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { registerEntityRoutes } from './auto-routes.js';
|
|
2
|
+
export type {
|
|
3
|
+
SequelizeEntityConfig,
|
|
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
|
+
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 Sequelize 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: 'sequelize' });
|
|
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,17 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import { __ENTITY_PASCAL__ } from '../../models/__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
|
+
model: __ENTITY_PASCAL__,
|
|
13
|
+
searchableFields: [__SEARCHABLE_FIELDS_ARRAY__],
|
|
14
|
+
bulkOperations: __BULK_OPERATIONS__,
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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/models/__ENTITY_KEBAB__.js';
|
|
5
|
+
import { sequelize, closeDatabase } from '../src/db/client.js';
|
|
6
|
+
|
|
7
|
+
const app = buildApp();
|
|
8
|
+
|
|
9
|
+
describe('__ENTITY_PASCAL__ CRUD', () => {
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
await sequelize.sync({ force: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await __ENTITY_PASCAL__.destroy({ where: {}, truncate: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await closeDatabase();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('POST creates a record', async () => {
|
|
23
|
+
const res = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
24
|
+
expect(res.status).toBe(201);
|
|
25
|
+
expect(res.body).toMatchObject(__SAMPLE_PAYLOAD__);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('GET / lists records with pagination', async () => {
|
|
29
|
+
await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
30
|
+
const res = await request(app).get('/api/v1__API_PREFIX__');
|
|
31
|
+
expect(res.status).toBe(200);
|
|
32
|
+
expect(res.body.data).toHaveLength(1);
|
|
33
|
+
expect(res.body.pagination).toMatchObject({ current_page: 1, total_records: 1 });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('GET /:id returns one record', async () => {
|
|
37
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
38
|
+
const { id } = created.body as { id: string };
|
|
39
|
+
const res = await request(app).get(`/api/v1__API_PREFIX__/${id}`);
|
|
40
|
+
expect(res.status).toBe(200);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('GET /:id returns 404 when not found', async () => {
|
|
44
|
+
const res = await request(app).get(
|
|
45
|
+
'/api/v1__API_PREFIX__/00000000-0000-0000-0000-000000000000',
|
|
46
|
+
);
|
|
47
|
+
expect(res.status).toBe(404);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('PATCH /:id updates a record', async () => {
|
|
51
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
52
|
+
const { id } = created.body as { id: string };
|
|
53
|
+
const res = await request(app).patch(`/api/v1__API_PREFIX__/${id}`).send(__UPDATE_PAYLOAD__);
|
|
54
|
+
expect(res.status).toBe(200);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('DELETE /:id removes a record', async () => {
|
|
58
|
+
const created = await request(app).post('/api/v1__API_PREFIX__').send(__SAMPLE_PAYLOAD__);
|
|
59
|
+
const { id } = created.body as { id: string };
|
|
60
|
+
const del = await request(app).delete(`/api/v1__API_PREFIX__/${id}`);
|
|
61
|
+
expect(del.status).toBe(204);
|
|
62
|
+
const get = await request(app).get(`/api/v1__API_PREFIX__/${id}`);
|
|
63
|
+
expect(get.status).toBe(404);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import { __ENTITY_PASCAL__ } from '../../models/__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
|
+
model: __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/models/__ENTITY_KEBAB__.js';
|
|
5
|
+
import { sequelize } from '../../src/db/client.js';
|
|
6
|
+
|
|
7
|
+
describe('__ENTITY_PASCAL__ CRUD', () => {
|
|
8
|
+
let app: FastifyInstance;
|
|
9
|
+
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
app = await buildApp({ logger: false });
|
|
12
|
+
await sequelize.sync({ force: true });
|
|
13
|
+
await app.ready();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await __ENTITY_PASCAL__.destroy({ where: {}, truncate: true });
|
|
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,21 @@
|
|
|
1
|
+
import { DataTypes, Model } from 'sequelize';
|
|
2
|
+
import { sequelize } from '../db/client.js';
|
|
3
|
+
|
|
4
|
+
export class __ENTITY_PASCAL__ extends Model {
|
|
5
|
+
declare id: string;
|
|
6
|
+
__FIELD_DECLARATIONS__
|
|
7
|
+
declare createdAt: Date;
|
|
8
|
+
declare updatedAt: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
__ENTITY_PASCAL__.init(
|
|
12
|
+
{
|
|
13
|
+
id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
|
|
14
|
+
__FIELD_DEFINITIONS__
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
sequelize,
|
|
18
|
+
modelName: '__ENTITY_PASCAL__',
|
|
19
|
+
tableName: '__TABLE_NAME__',
|
|
20
|
+
},
|
|
21
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sequelize",
|
|
3
|
+
"displayName": "Sequelize",
|
|
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": "Sequelize" },
|
|
31
|
+
"removeDependencies": ["@prisma/client"],
|
|
32
|
+
"removeDevDependencies": ["prisma"],
|
|
33
|
+
"addDependencies": {
|
|
34
|
+
"sequelize": "^6.37.5",
|
|
35
|
+
"pg": "^8.16.3",
|
|
36
|
+
"pg-hstore": "^2.3.4"
|
|
37
|
+
},
|
|
38
|
+
"addDevDependencies": {
|
|
39
|
+
"@types/pg": "^8.15.5",
|
|
40
|
+
"sequelize-cli": "^6.6.3"
|
|
41
|
+
},
|
|
42
|
+
"removeScriptPrefixes": ["prisma:"],
|
|
43
|
+
"addScripts": {
|
|
44
|
+
"db:sync": "tsx scripts/db-sync.ts",
|
|
45
|
+
"db:migrate": "sequelize-cli db:migrate",
|
|
46
|
+
"db:migrate:undo": "sequelize-cli db:migrate:undo"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dockerfile": {
|
|
50
|
+
"extraConfigFiles": [],
|
|
51
|
+
"migrateCommand": "tsx scripts/db-sync.ts"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { sequelize } from '../src/db/client.js';
|
|
2
|
+
import '../src/models/index.js';
|
|
3
|
+
|
|
4
|
+
async function main(): Promise<void> {
|
|
5
|
+
await sequelize.authenticate();
|
|
6
|
+
await sequelize.sync({ alter: true });
|
|
7
|
+
console.log('Sequelize schema synced.');
|
|
8
|
+
await sequelize.close();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
main().catch((err: unknown) => {
|
|
12
|
+
console.error('db-sync failed:', err);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Sequelize } from 'sequelize';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
|
|
4
|
+
export const sequelize = new Sequelize(config.DATABASE_URL, {
|
|
5
|
+
dialect: 'postgres',
|
|
6
|
+
logging: false,
|
|
7
|
+
define: {
|
|
8
|
+
underscored: true,
|
|
9
|
+
timestamps: true,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export async function checkDatabase(): Promise<void> {
|
|
14
|
+
await sequelize.authenticate();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function closeDatabase(): Promise<void> {
|
|
18
|
+
await sequelize.close();
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Aggregator for all Sequelize models. Each model module calls `Model.init(...)` at import
|
|
2
|
+
// time, attaching itself to the shared sequelize instance from `../db/client.js`.
|
|
3
|
+
// `gen entity` appends new model imports + exports below the anchors.
|
|
4
|
+
|
|
5
|
+
// projx-anchor: model-imports
|
|
6
|
+
|
|
7
|
+
export const models = {
|
|
8
|
+
// projx-anchor: model-exports
|
|
9
|
+
};
|