create-projx 1.7.0 → 1.7.2
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 +13 -35
- package/dist/{baseline-FHOZNS4D.js → baseline-ZPPJKHBN.js} +2 -2
- package/dist/{chunk-HAT7D4G2.js → chunk-FQPOK3QZ.js} +10 -3
- package/dist/{chunk-IMZKHDIL.js → chunk-XAYCVTHL.js} +15 -18
- package/dist/index.js +166 -385
- package/dist/{utils-BZGSJ7XZ.js → utils-MC7VKL2U.js} +1 -1
- package/package.json +2 -3
- package/src/templates/README.md.ejs +1 -1
- package/src/templates/ci.yml.ejs +14 -15
- package/src/templates/pre-commit.ejs +13 -1
- package/src/addons/orms/drizzle/express/src/app.ts +0 -81
- package/src/addons/orms/drizzle/express/src/modules/_base/auto-routes.ts +0 -278
- package/src/addons/orms/drizzle/express/src/modules/_base/index.ts +0 -20
- package/src/addons/orms/drizzle/express/src/server.ts +0 -32
- package/src/addons/orms/drizzle/express/tests/app.test.ts +0 -24
- package/src/addons/orms/drizzle/express/vitest.config.ts +0 -20
- package/src/addons/orms/drizzle/fastify/src/app.ts +0 -90
- package/src/addons/orms/drizzle/fastify/src/modules/_base/auto-routes.ts +0 -268
- package/src/addons/orms/drizzle/fastify/src/modules/_base/index.ts +0 -20
- package/src/addons/orms/drizzle/fastify/tests/modules/app.test.ts +0 -20
- package/src/addons/orms/drizzle/fastify/vitest.config.ts +0 -31
- package/src/addons/orms/drizzle/gen-entity/express-router.ts +0 -21
- package/src/addons/orms/drizzle/gen-entity/express-test.ts +0 -61
- package/src/addons/orms/drizzle/gen-entity/fastify-router.ts +0 -19
- package/src/addons/orms/drizzle/gen-entity/fastify-test.ts +0 -87
- package/src/addons/orms/drizzle/manifest.json +0 -52
- package/src/addons/orms/drizzle/shared/drizzle.config.ts +0 -12
- package/src/addons/orms/drizzle/shared/src/db/client.ts +0 -17
- package/src/addons/orms/drizzle/shared/src/db/schema.ts +0 -14
- package/src/addons/orms/drizzle/shared/src/modules/_base/query-engine.ts +0 -115
- package/src/addons/orms/drizzle/shared/src/modules/_base/registry.ts +0 -15
- package/src/addons/orms/sequelize/express/src/app.ts +0 -82
- package/src/addons/orms/sequelize/express/src/modules/_base/auto-routes.ts +0 -226
- package/src/addons/orms/sequelize/express/src/modules/_base/index.ts +0 -20
- package/src/addons/orms/sequelize/express/src/server.ts +0 -32
- package/src/addons/orms/sequelize/express/tests/app.test.ts +0 -24
- package/src/addons/orms/sequelize/express/vitest.config.ts +0 -20
- package/src/addons/orms/sequelize/fastify/src/app.ts +0 -83
- package/src/addons/orms/sequelize/fastify/src/modules/_base/auto-routes.ts +0 -216
- package/src/addons/orms/sequelize/fastify/src/modules/_base/index.ts +0 -20
- package/src/addons/orms/sequelize/fastify/tests/modules/app.test.ts +0 -20
- package/src/addons/orms/sequelize/fastify/vitest.config.ts +0 -31
- package/src/addons/orms/sequelize/gen-entity/express-router.ts +0 -17
- package/src/addons/orms/sequelize/gen-entity/express-test.ts +0 -65
- package/src/addons/orms/sequelize/gen-entity/fastify-router.ts +0 -19
- package/src/addons/orms/sequelize/gen-entity/fastify-test.ts +0 -89
- package/src/addons/orms/sequelize/gen-entity/model.ts +0 -21
- package/src/addons/orms/sequelize/manifest.json +0 -53
- package/src/addons/orms/sequelize/shared/scripts/db-sync.ts +0 -14
- package/src/addons/orms/sequelize/shared/src/db/client.ts +0 -19
- package/src/addons/orms/sequelize/shared/src/models/index.ts +0 -9
- package/src/addons/orms/sequelize/shared/src/modules/_base/query-engine.ts +0 -101
- package/src/addons/orms/sequelize/shared/src/modules/_base/registry.ts +0 -15
- package/src/addons/orms/typeorm/express/src/app.ts +0 -82
- package/src/addons/orms/typeorm/express/src/modules/_base/auto-routes.ts +0 -249
- package/src/addons/orms/typeorm/express/src/modules/_base/index.ts +0 -19
- package/src/addons/orms/typeorm/express/src/server.ts +0 -43
- package/src/addons/orms/typeorm/express/tests/app.test.ts +0 -24
- package/src/addons/orms/typeorm/express/vitest.config.ts +0 -20
- package/src/addons/orms/typeorm/fastify/src/app.ts +0 -86
- package/src/addons/orms/typeorm/fastify/src/modules/_base/auto-routes.ts +0 -239
- package/src/addons/orms/typeorm/fastify/src/modules/_base/index.ts +0 -19
- package/src/addons/orms/typeorm/fastify/tests/modules/app.test.ts +0 -20
- package/src/addons/orms/typeorm/fastify/vitest.config.ts +0 -31
- package/src/addons/orms/typeorm/gen-entity/entity.ts +0 -21
- package/src/addons/orms/typeorm/gen-entity/express-router.ts +0 -17
- package/src/addons/orms/typeorm/gen-entity/express-test.ts +0 -66
- package/src/addons/orms/typeorm/gen-entity/fastify-router.ts +0 -19
- package/src/addons/orms/typeorm/gen-entity/fastify-test.ts +0 -89
- package/src/addons/orms/typeorm/manifest.json +0 -53
- package/src/addons/orms/typeorm/shared/scripts/db-sync.ts +0 -14
- package/src/addons/orms/typeorm/shared/src/db/data-source.ts +0 -21
- package/src/addons/orms/typeorm/shared/src/entities/index.ts +0 -8
- package/src/addons/orms/typeorm/shared/src/modules/_base/query-engine.ts +0 -94
- package/src/addons/orms/typeorm/shared/src/modules/_base/registry.ts +0 -15
- package/src/addons/orms/typeorm/shared/tsconfig.json +0 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-projx",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "Scaffold production-grade fullstack projects in seconds. FastAPI, Fastify, Express, React, Flutter, Terraform — with auth, database, CI/CD, E2E tests, and Docker. One command, ready to deploy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
|
-
"src/templates"
|
|
12
|
-
"src/addons"
|
|
11
|
+
"src/templates"
|
|
13
12
|
],
|
|
14
13
|
"keywords": [
|
|
15
14
|
"projx",
|
|
@@ -13,7 +13,7 @@ Scaffolded with [Projx](https://github.com/ukanhaupa/projx).
|
|
|
13
13
|
| **<%= paths.fastify %>/** | Fastify, <%= orm === 'drizzle' ? 'Drizzle' : 'Prisma' %>, TypeBox, TypeScript |
|
|
14
14
|
<% } %>
|
|
15
15
|
<% if (components.includes('express')) { %>
|
|
16
|
-
| **<%= paths.express %>/** | Express 5, TypeScript, <%= orm === 'drizzle' ? 'Drizzle' : 'Prisma
|
|
16
|
+
| **<%= paths.express %>/** | Express 5, TypeScript, <%= orm === 'drizzle' ? 'Drizzle' : 'Prisma' %> |
|
|
17
17
|
<% } %>
|
|
18
18
|
<% if (components.includes('frontend')) { %>
|
|
19
19
|
| **<%= paths.frontend %>/** | React 19, TypeScript, Vite, React Router |
|
package/src/templates/ci.yml.ejs
CHANGED
|
@@ -123,7 +123,7 @@ jobs:
|
|
|
123
123
|
run: |
|
|
124
124
|
run_pip_audit() {
|
|
125
125
|
for attempt in 1 2 3; do
|
|
126
|
-
if uv run pip-audit --ignore-vuln CVE-2026-3219; then
|
|
126
|
+
if uv run pip-audit --ignore-vuln CVE-2026-3219 --ignore-vuln PYSEC-2025-183; then
|
|
127
127
|
return 0
|
|
128
128
|
fi
|
|
129
129
|
if [ "$attempt" -eq 3 ]; then
|
|
@@ -163,15 +163,15 @@ jobs:
|
|
|
163
163
|
JWT_SECRET: ci-test-secret # gitleaks:allow
|
|
164
164
|
steps:
|
|
165
165
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
166
|
-
<% if (pm === 'pnpm') { %>
|
|
166
|
+
<% if (pm.name === 'pnpm') { %>
|
|
167
167
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
168
168
|
with:
|
|
169
169
|
version: 10
|
|
170
170
|
<% } %>
|
|
171
|
-
<% if (pm === 'bun') { %>
|
|
171
|
+
<% if (pm.name === 'bun') { %>
|
|
172
172
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
173
173
|
<% } %>
|
|
174
|
-
<% if (pm !== 'bun') { %>
|
|
174
|
+
<% if (pm.name !== 'bun') { %>
|
|
175
175
|
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
176
176
|
with:
|
|
177
177
|
node-version: 22
|
|
@@ -196,7 +196,7 @@ jobs:
|
|
|
196
196
|
<% for (const inst of expressInstances) { %>
|
|
197
197
|
|
|
198
198
|
<%= inst.path %>:
|
|
199
|
-
name: <%= inst.display %> (format + lint + typecheck + build +
|
|
199
|
+
name: <%= inst.display %> (format + lint + typecheck + build + audit)
|
|
200
200
|
needs: changes
|
|
201
201
|
if: github.event_name == 'workflow_dispatch' || needs.changes.outputs.<%= inst.path %> == 'true'
|
|
202
202
|
runs-on: ubuntu-latest
|
|
@@ -221,15 +221,15 @@ jobs:
|
|
|
221
221
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ci_test
|
|
222
222
|
steps:
|
|
223
223
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
224
|
-
<% if (pm === 'pnpm') { %>
|
|
224
|
+
<% if (pm.name === 'pnpm') { %>
|
|
225
225
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
226
226
|
with:
|
|
227
227
|
version: 10
|
|
228
228
|
<% } %>
|
|
229
|
-
<% if (pm === 'bun') { %>
|
|
229
|
+
<% if (pm.name === 'bun') { %>
|
|
230
230
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
231
231
|
<% } %>
|
|
232
|
-
<% if (pm !== 'bun') { %>
|
|
232
|
+
<% if (pm.name !== 'bun') { %>
|
|
233
233
|
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
234
234
|
with:
|
|
235
235
|
node-version: 22
|
|
@@ -249,7 +249,6 @@ jobs:
|
|
|
249
249
|
- run: <%= pm.exec %> eslint .
|
|
250
250
|
- run: <%= pm.exec %> tsc --noEmit
|
|
251
251
|
- run: <%= pm.run %> build
|
|
252
|
-
- run: <%= pm.exec %> vitest run --coverage.enabled=false
|
|
253
252
|
- run: <%= pm.audit %>
|
|
254
253
|
<% } %>
|
|
255
254
|
<% for (const inst of frontendInstances) { %>
|
|
@@ -264,15 +263,15 @@ jobs:
|
|
|
264
263
|
working-directory: <%= inst.path %>
|
|
265
264
|
steps:
|
|
266
265
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
267
|
-
<% if (pm === 'pnpm') { %>
|
|
266
|
+
<% if (pm.name === 'pnpm') { %>
|
|
268
267
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
269
268
|
with:
|
|
270
269
|
version: 10
|
|
271
270
|
<% } %>
|
|
272
|
-
<% if (pm === 'bun') { %>
|
|
271
|
+
<% if (pm.name === 'bun') { %>
|
|
273
272
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
274
273
|
<% } %>
|
|
275
|
-
<% if (pm !== 'bun') { %>
|
|
274
|
+
<% if (pm.name !== 'bun') { %>
|
|
276
275
|
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
277
276
|
with:
|
|
278
277
|
node-version: 22
|
|
@@ -321,15 +320,15 @@ jobs:
|
|
|
321
320
|
working-directory: <%= inst.path %>
|
|
322
321
|
steps:
|
|
323
322
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
324
|
-
<% if (pm === 'pnpm') { %>
|
|
323
|
+
<% if (pm.name === 'pnpm') { %>
|
|
325
324
|
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
|
|
326
325
|
with:
|
|
327
326
|
version: 10
|
|
328
327
|
<% } %>
|
|
329
|
-
<% if (pm === 'bun') { %>
|
|
328
|
+
<% if (pm.name === 'bun') { %>
|
|
330
329
|
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
|
331
330
|
<% } %>
|
|
332
|
-
<% if (pm !== 'bun') { %>
|
|
331
|
+
<% if (pm.name !== 'bun') { %>
|
|
333
332
|
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
|
334
333
|
with:
|
|
335
334
|
node-version: 22
|
|
@@ -135,9 +135,21 @@ fi
|
|
|
135
135
|
<%= inst.upper %>_TF=$(echo "$STAGED_FILES" | grep '^<%= inst.path %>/.*\.tf$' || true)
|
|
136
136
|
if [ -n "$<%= inst.upper %>_TF" ]; then
|
|
137
137
|
if command -v terraform &> /dev/null; then
|
|
138
|
-
echo "
|
|
138
|
+
echo "Checking <%= inst.path %>..."
|
|
139
139
|
cd <%= inst.path %>/stack
|
|
140
140
|
echo "$<%= inst.upper %>_TF" | sed 's|^<%= inst.path %>/stack/||' | xargs terraform fmt
|
|
141
|
+
[ -d .terraform ] || terraform init -backend=false -input=false >/dev/null
|
|
142
|
+
terraform validate
|
|
143
|
+
if command -v tflint &> /dev/null; then
|
|
144
|
+
tflint --recursive
|
|
145
|
+
else
|
|
146
|
+
echo "Skipping tflint (not installed)"
|
|
147
|
+
fi
|
|
148
|
+
if command -v trivy &> /dev/null; then
|
|
149
|
+
trivy config --exit-code 1 --severity HIGH,CRITICAL .
|
|
150
|
+
else
|
|
151
|
+
echo "Skipping trivy config scan (not installed)"
|
|
152
|
+
fi
|
|
141
153
|
cd ../..
|
|
142
154
|
echo "$<%= inst.upper %>_TF" | xargs git add
|
|
143
155
|
else
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import compression from 'compression';
|
|
3
|
-
import cors from 'cors';
|
|
4
|
-
import express, { type RequestHandler } from 'express';
|
|
5
|
-
import rateLimit from 'express-rate-limit';
|
|
6
|
-
import helmet from 'helmet';
|
|
7
|
-
import pinoHttp from 'pino-http';
|
|
8
|
-
import { allowedOrigins, config } from './config.js';
|
|
9
|
-
import { ApiError, errorHandler, notFoundHandler } from './errors.js';
|
|
10
|
-
import { checkDatabase, db } from './db/client.js';
|
|
11
|
-
import { listEntities } from './modules/_base/index.js';
|
|
12
|
-
// projx-anchor: entity-imports
|
|
13
|
-
|
|
14
|
-
const requestId: RequestHandler = (req, res, next) => {
|
|
15
|
-
const incoming = req.headers['x-request-id'];
|
|
16
|
-
const value =
|
|
17
|
-
typeof incoming === 'string' && incoming.trim()
|
|
18
|
-
? incoming
|
|
19
|
-
: crypto.randomUUID();
|
|
20
|
-
res.locals.requestId = value;
|
|
21
|
-
res.setHeader('x-request-id', value);
|
|
22
|
-
next();
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function corsOrigin(
|
|
26
|
-
origin: string | undefined,
|
|
27
|
-
callback: (err: Error | null, allow?: boolean) => void,
|
|
28
|
-
): void {
|
|
29
|
-
const origins = allowedOrigins();
|
|
30
|
-
if (!origin || origins.includes('*') || origins.includes(origin)) {
|
|
31
|
-
callback(null, true);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
callback(new ApiError(403, 'Origin not allowed', 'origin_not_allowed'));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function buildApp(): express.Express {
|
|
38
|
-
const app = express();
|
|
39
|
-
|
|
40
|
-
app.disable('x-powered-by');
|
|
41
|
-
app.locals.db = db;
|
|
42
|
-
app.use(requestId);
|
|
43
|
-
app.use(pinoHttp({ level: config.LOG_LEVEL }));
|
|
44
|
-
app.use(helmet());
|
|
45
|
-
app.use(cors({ origin: corsOrigin, credentials: true }));
|
|
46
|
-
app.use(compression());
|
|
47
|
-
app.use(express.json({ limit: '1mb' }));
|
|
48
|
-
app.use(express.urlencoded({ extended: false, limit: '1mb' }));
|
|
49
|
-
app.use(
|
|
50
|
-
rateLimit({
|
|
51
|
-
windowMs: config.RATE_LIMIT_WINDOW_MS,
|
|
52
|
-
limit: config.RATE_LIMIT_MAX,
|
|
53
|
-
standardHeaders: 'draft-8',
|
|
54
|
-
legacyHeaders: false,
|
|
55
|
-
}),
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
app.get('/api/health', async (_req, res) => {
|
|
59
|
-
const checks: Record<string, string> = { app: 'ok' };
|
|
60
|
-
try {
|
|
61
|
-
await checkDatabase();
|
|
62
|
-
checks.database = 'ok';
|
|
63
|
-
} catch (e) {
|
|
64
|
-
checks.database = `error: ${e instanceof Error ? e.message : String(e)}`;
|
|
65
|
-
res.status(503).json({ status: 'unhealthy', checks });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
res.json({ status: 'healthy', checks });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
app.get('/api/v1/_meta', (_req, res) => {
|
|
72
|
-
res.json({ entities: listEntities(), orm: 'drizzle' });
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// projx-anchor: entity-registrations
|
|
76
|
-
|
|
77
|
-
app.use(notFoundHandler);
|
|
78
|
-
app.use(errorHandler);
|
|
79
|
-
|
|
80
|
-
return app;
|
|
81
|
-
}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import express, {
|
|
2
|
-
type NextFunction,
|
|
3
|
-
type Request,
|
|
4
|
-
type Response,
|
|
5
|
-
} from 'express';
|
|
6
|
-
import { eq, inArray, sql } from 'drizzle-orm';
|
|
7
|
-
import type { PgTable } from 'drizzle-orm/pg-core';
|
|
8
|
-
import type { DbClient } from '../../db/client.js';
|
|
9
|
-
import { ApiError } from '../../errors.js';
|
|
10
|
-
import { registerInRegistry } from './registry.js';
|
|
11
|
-
import {
|
|
12
|
-
buildOrderBy,
|
|
13
|
-
buildPagination,
|
|
14
|
-
buildSearchWhere,
|
|
15
|
-
buildWhere,
|
|
16
|
-
combineWhere,
|
|
17
|
-
parseRawQuery,
|
|
18
|
-
} from './query-engine.js';
|
|
19
|
-
|
|
20
|
-
export type BeforeCreateHook = (
|
|
21
|
-
request: Request,
|
|
22
|
-
data: Record<string, unknown>,
|
|
23
|
-
) => void | Promise<void>;
|
|
24
|
-
export type AfterCreateHook = (
|
|
25
|
-
request: Request,
|
|
26
|
-
record: Record<string, unknown>,
|
|
27
|
-
) => void | Promise<void>;
|
|
28
|
-
export type BeforeUpdateHook = (
|
|
29
|
-
request: Request,
|
|
30
|
-
response: Response,
|
|
31
|
-
data: Record<string, unknown>,
|
|
32
|
-
) => void | Promise<void>;
|
|
33
|
-
export type AfterUpdateHook = (
|
|
34
|
-
request: Request,
|
|
35
|
-
before: Record<string, unknown>,
|
|
36
|
-
after: Record<string, unknown>,
|
|
37
|
-
) => void | Promise<void>;
|
|
38
|
-
export type BeforeDeleteHook = (
|
|
39
|
-
request: Request,
|
|
40
|
-
recordId: string,
|
|
41
|
-
) => void | Promise<void>;
|
|
42
|
-
|
|
43
|
-
export interface DrizzleEntityConfig {
|
|
44
|
-
name: string;
|
|
45
|
-
apiPrefix: string;
|
|
46
|
-
tag: string;
|
|
47
|
-
table: PgTable;
|
|
48
|
-
primaryKey?: string;
|
|
49
|
-
searchableFields?: string[];
|
|
50
|
-
readonly?: boolean;
|
|
51
|
-
bulkOperations?: boolean;
|
|
52
|
-
beforeCreate?: BeforeCreateHook;
|
|
53
|
-
afterCreate?: AfterCreateHook;
|
|
54
|
-
beforeUpdate?: BeforeUpdateHook;
|
|
55
|
-
afterUpdate?: AfterUpdateHook;
|
|
56
|
-
beforeDelete?: BeforeDeleteHook;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type AsyncHandler = (
|
|
60
|
-
req: Request,
|
|
61
|
-
res: Response,
|
|
62
|
-
next: NextFunction,
|
|
63
|
-
) => Promise<void>;
|
|
64
|
-
|
|
65
|
-
function asyncHandler(handler: AsyncHandler) {
|
|
66
|
-
return (req: Request, res: Response, next: NextFunction): void => {
|
|
67
|
-
handler(req, res, next).catch(next);
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function column(table: PgTable, key: string): unknown {
|
|
72
|
-
return (table as unknown as Record<string, unknown>)[key];
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function pkColumn(config: DrizzleEntityConfig): unknown {
|
|
76
|
-
const key = config.primaryKey ?? 'id';
|
|
77
|
-
const col = column(config.table, key);
|
|
78
|
-
if (!col) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Primary key column "${key}" not found on table ${config.name}`,
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
return col;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function registerEntityRoutes(
|
|
87
|
-
config: DrizzleEntityConfig,
|
|
88
|
-
db: DbClient,
|
|
89
|
-
): express.Router {
|
|
90
|
-
registerInRegistry({ name: config.name, apiPrefix: config.apiPrefix });
|
|
91
|
-
const router = express.Router();
|
|
92
|
-
const pk = pkColumn(config) as Parameters<typeof eq>[0];
|
|
93
|
-
|
|
94
|
-
router.get(
|
|
95
|
-
'/',
|
|
96
|
-
asyncHandler(async (req, res) => {
|
|
97
|
-
const rawQs = req.originalUrl.split('?')[1] ?? '';
|
|
98
|
-
const query = parseRawQuery(rawQs);
|
|
99
|
-
const filterWhere = buildWhere(config.table, query.filters);
|
|
100
|
-
const searchWhere = buildSearchWhere(
|
|
101
|
-
config.table,
|
|
102
|
-
config.searchableFields ?? [],
|
|
103
|
-
query.search,
|
|
104
|
-
);
|
|
105
|
-
const where = combineWhere(filterWhere, searchWhere);
|
|
106
|
-
const order = buildOrderBy(config.table, query.order_by);
|
|
107
|
-
const offset = (query.page - 1) * query.page_size;
|
|
108
|
-
|
|
109
|
-
const baseSelect = db.select().from(config.table);
|
|
110
|
-
const baseCount = db
|
|
111
|
-
.select({ count: sql<number>`count(*)::int` })
|
|
112
|
-
.from(config.table);
|
|
113
|
-
|
|
114
|
-
const rows = await (where
|
|
115
|
-
? baseSelect
|
|
116
|
-
.where(where)
|
|
117
|
-
.orderBy(...order)
|
|
118
|
-
.limit(query.page_size)
|
|
119
|
-
.offset(offset)
|
|
120
|
-
: baseSelect
|
|
121
|
-
.orderBy(...order)
|
|
122
|
-
.limit(query.page_size)
|
|
123
|
-
.offset(offset));
|
|
124
|
-
const [{ count }] = await (where ? baseCount.where(where) : baseCount);
|
|
125
|
-
|
|
126
|
-
res.json({
|
|
127
|
-
data: rows,
|
|
128
|
-
pagination: buildPagination(query.page, query.page_size, Number(count)),
|
|
129
|
-
});
|
|
130
|
-
}),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
router.get(
|
|
134
|
-
'/:id',
|
|
135
|
-
asyncHandler(async (req, res) => {
|
|
136
|
-
const [record] = await db
|
|
137
|
-
.select()
|
|
138
|
-
.from(config.table)
|
|
139
|
-
.where(eq(pk, String(req.params.id)))
|
|
140
|
-
.limit(1);
|
|
141
|
-
if (!record) throw new ApiError(404, 'Not found', 'not_found');
|
|
142
|
-
res.json(record);
|
|
143
|
-
}),
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
if (config.readonly) return router;
|
|
147
|
-
|
|
148
|
-
router.post(
|
|
149
|
-
'/',
|
|
150
|
-
asyncHandler(async (req, res) => {
|
|
151
|
-
const data = req.body as Record<string, unknown>;
|
|
152
|
-
await config.beforeCreate?.(req, data);
|
|
153
|
-
const [record] = await db
|
|
154
|
-
.insert(config.table)
|
|
155
|
-
.values(data as never)
|
|
156
|
-
.returning();
|
|
157
|
-
if (config.afterCreate) {
|
|
158
|
-
try {
|
|
159
|
-
await config.afterCreate(req, record as Record<string, unknown>);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
req.log?.error?.(
|
|
162
|
-
{
|
|
163
|
-
err,
|
|
164
|
-
entity: config.name,
|
|
165
|
-
record_id: (record as { id?: string }).id,
|
|
166
|
-
},
|
|
167
|
-
'afterCreate hook failed (record persisted; hook is best-effort)',
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
res.status(201).json(record);
|
|
172
|
-
}),
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
router.patch(
|
|
176
|
-
'/:id',
|
|
177
|
-
asyncHandler(async (req, res) => {
|
|
178
|
-
const id = String(req.params.id);
|
|
179
|
-
const data = req.body as Record<string, unknown>;
|
|
180
|
-
if (!data || Object.keys(data).length === 0) {
|
|
181
|
-
throw new ApiError(400, 'Request body cannot be empty', 'empty_body');
|
|
182
|
-
}
|
|
183
|
-
if (config.beforeUpdate) {
|
|
184
|
-
await config.beforeUpdate(req, res, data);
|
|
185
|
-
if (res.headersSent) return;
|
|
186
|
-
}
|
|
187
|
-
let before: Record<string, unknown> | null = null;
|
|
188
|
-
if (config.afterUpdate) {
|
|
189
|
-
const [existing] = await db
|
|
190
|
-
.select()
|
|
191
|
-
.from(config.table)
|
|
192
|
-
.where(eq(pk, id))
|
|
193
|
-
.limit(1);
|
|
194
|
-
before = (existing as Record<string, unknown>) ?? null;
|
|
195
|
-
}
|
|
196
|
-
const [record] = await db
|
|
197
|
-
.update(config.table)
|
|
198
|
-
.set(data as never)
|
|
199
|
-
.where(eq(pk, id))
|
|
200
|
-
.returning();
|
|
201
|
-
if (!record) throw new ApiError(404, 'Not found', 'not_found');
|
|
202
|
-
if (config.afterUpdate && before) {
|
|
203
|
-
try {
|
|
204
|
-
await config.afterUpdate(
|
|
205
|
-
req,
|
|
206
|
-
before,
|
|
207
|
-
record as Record<string, unknown>,
|
|
208
|
-
);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
req.log?.error?.(
|
|
211
|
-
{ err, entity: config.name, record_id: id },
|
|
212
|
-
'afterUpdate hook failed (record persisted; hook is best-effort)',
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
res.json(record);
|
|
217
|
-
}),
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
router.delete(
|
|
221
|
-
'/:id',
|
|
222
|
-
asyncHandler(async (req, res) => {
|
|
223
|
-
const id = String(req.params.id);
|
|
224
|
-
if (config.beforeDelete) await config.beforeDelete(req, id);
|
|
225
|
-
const deleted = await db
|
|
226
|
-
.delete(config.table)
|
|
227
|
-
.where(eq(pk, id))
|
|
228
|
-
.returning();
|
|
229
|
-
if (deleted.length === 0)
|
|
230
|
-
throw new ApiError(404, 'Not found', 'not_found');
|
|
231
|
-
res.status(204).send();
|
|
232
|
-
}),
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
if (!config.bulkOperations) return router;
|
|
236
|
-
|
|
237
|
-
router.post(
|
|
238
|
-
'/bulk',
|
|
239
|
-
asyncHandler(async (req, res) => {
|
|
240
|
-
const { items } = req.body as { items: Record<string, unknown>[] };
|
|
241
|
-
if (!Array.isArray(items) || items.length === 0) {
|
|
242
|
-
throw new ApiError(
|
|
243
|
-
400,
|
|
244
|
-
'items must be a non-empty array',
|
|
245
|
-
'validation_error',
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
for (const item of items) {
|
|
249
|
-
await config.beforeCreate?.(req, item);
|
|
250
|
-
}
|
|
251
|
-
const rows = await db
|
|
252
|
-
.insert(config.table)
|
|
253
|
-
.values(items as never)
|
|
254
|
-
.returning();
|
|
255
|
-
res.status(201).json({ data: rows, count: rows.length });
|
|
256
|
-
}),
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
router.delete(
|
|
260
|
-
'/bulk',
|
|
261
|
-
asyncHandler(async (req, res) => {
|
|
262
|
-
const { ids } = req.body as { ids: string[] };
|
|
263
|
-
if (!Array.isArray(ids) || ids.length === 0) {
|
|
264
|
-
throw new ApiError(
|
|
265
|
-
400,
|
|
266
|
-
'ids must be a non-empty array',
|
|
267
|
-
'validation_error',
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
await db
|
|
271
|
-
.delete(config.table)
|
|
272
|
-
.where(inArray(pk as Parameters<typeof inArray>[0], ids));
|
|
273
|
-
res.status(204).send();
|
|
274
|
-
}),
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
return router;
|
|
278
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
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';
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { buildApp } from './app.js';
|
|
3
|
-
import { config } from './config.js';
|
|
4
|
-
import { closeDatabase } from './db/client.js';
|
|
5
|
-
|
|
6
|
-
const app = buildApp();
|
|
7
|
-
const server = createServer(app);
|
|
8
|
-
|
|
9
|
-
server.listen(config.PORT, config.HOST, () => {
|
|
10
|
-
console.log(`Express API listening on http://${config.HOST}:${config.PORT}`);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
function shutdown(signal: string): void {
|
|
14
|
-
console.log(`${signal} received, closing HTTP server`);
|
|
15
|
-
server.close((err) => {
|
|
16
|
-
closeDatabase()
|
|
17
|
-
.catch((closeErr: unknown) => {
|
|
18
|
-
console.error(closeErr);
|
|
19
|
-
})
|
|
20
|
-
.finally(() => {
|
|
21
|
-
if (err) {
|
|
22
|
-
console.error(err);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
process.exit(0);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
setTimeout(() => process.exit(1), 10_000).unref();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
process.on('SIGTERM', shutdown);
|
|
32
|
-
process.on('SIGINT', shutdown);
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import request from 'supertest';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { buildApp } from '../src/app.js';
|
|
4
|
-
|
|
5
|
-
describe('Express Drizzle app', () => {
|
|
6
|
-
it('exposes empty generated metadata until entities are added', async () => {
|
|
7
|
-
const res = await request(buildApp()).get('/api/v1/_meta');
|
|
8
|
-
|
|
9
|
-
expect(res.status).toBe(200);
|
|
10
|
-
expect(res.body).toEqual({ entities: [], orm: 'drizzle' });
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('returns structured errors with request id', async () => {
|
|
14
|
-
const res = await request(buildApp())
|
|
15
|
-
.get('/missing')
|
|
16
|
-
.set('x-request-id', 'req-missing');
|
|
17
|
-
|
|
18
|
-
expect(res.status).toBe(404);
|
|
19
|
-
expect(res.body.error).toMatchObject({
|
|
20
|
-
code: 'not_found',
|
|
21
|
-
request_id: 'req-missing',
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: 'node',
|
|
7
|
-
include: ['tests/**/*.test.ts'],
|
|
8
|
-
coverage: {
|
|
9
|
-
provider: 'v8',
|
|
10
|
-
include: ['src/**/*.ts'],
|
|
11
|
-
exclude: ['src/server.ts', 'src/config.ts'],
|
|
12
|
-
thresholds: {
|
|
13
|
-
statements: 80,
|
|
14
|
-
branches: 80,
|
|
15
|
-
functions: 80,
|
|
16
|
-
lines: 80,
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
});
|