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.
Files changed (76) hide show
  1. package/README.md +13 -35
  2. package/dist/{baseline-FHOZNS4D.js → baseline-ZPPJKHBN.js} +2 -2
  3. package/dist/{chunk-HAT7D4G2.js → chunk-FQPOK3QZ.js} +10 -3
  4. package/dist/{chunk-IMZKHDIL.js → chunk-XAYCVTHL.js} +15 -18
  5. package/dist/index.js +166 -385
  6. package/dist/{utils-BZGSJ7XZ.js → utils-MC7VKL2U.js} +1 -1
  7. package/package.json +2 -3
  8. package/src/templates/README.md.ejs +1 -1
  9. package/src/templates/ci.yml.ejs +14 -15
  10. package/src/templates/pre-commit.ejs +13 -1
  11. package/src/addons/orms/drizzle/express/src/app.ts +0 -81
  12. package/src/addons/orms/drizzle/express/src/modules/_base/auto-routes.ts +0 -278
  13. package/src/addons/orms/drizzle/express/src/modules/_base/index.ts +0 -20
  14. package/src/addons/orms/drizzle/express/src/server.ts +0 -32
  15. package/src/addons/orms/drizzle/express/tests/app.test.ts +0 -24
  16. package/src/addons/orms/drizzle/express/vitest.config.ts +0 -20
  17. package/src/addons/orms/drizzle/fastify/src/app.ts +0 -90
  18. package/src/addons/orms/drizzle/fastify/src/modules/_base/auto-routes.ts +0 -268
  19. package/src/addons/orms/drizzle/fastify/src/modules/_base/index.ts +0 -20
  20. package/src/addons/orms/drizzle/fastify/tests/modules/app.test.ts +0 -20
  21. package/src/addons/orms/drizzle/fastify/vitest.config.ts +0 -31
  22. package/src/addons/orms/drizzle/gen-entity/express-router.ts +0 -21
  23. package/src/addons/orms/drizzle/gen-entity/express-test.ts +0 -61
  24. package/src/addons/orms/drizzle/gen-entity/fastify-router.ts +0 -19
  25. package/src/addons/orms/drizzle/gen-entity/fastify-test.ts +0 -87
  26. package/src/addons/orms/drizzle/manifest.json +0 -52
  27. package/src/addons/orms/drizzle/shared/drizzle.config.ts +0 -12
  28. package/src/addons/orms/drizzle/shared/src/db/client.ts +0 -17
  29. package/src/addons/orms/drizzle/shared/src/db/schema.ts +0 -14
  30. package/src/addons/orms/drizzle/shared/src/modules/_base/query-engine.ts +0 -115
  31. package/src/addons/orms/drizzle/shared/src/modules/_base/registry.ts +0 -15
  32. package/src/addons/orms/sequelize/express/src/app.ts +0 -82
  33. package/src/addons/orms/sequelize/express/src/modules/_base/auto-routes.ts +0 -226
  34. package/src/addons/orms/sequelize/express/src/modules/_base/index.ts +0 -20
  35. package/src/addons/orms/sequelize/express/src/server.ts +0 -32
  36. package/src/addons/orms/sequelize/express/tests/app.test.ts +0 -24
  37. package/src/addons/orms/sequelize/express/vitest.config.ts +0 -20
  38. package/src/addons/orms/sequelize/fastify/src/app.ts +0 -83
  39. package/src/addons/orms/sequelize/fastify/src/modules/_base/auto-routes.ts +0 -216
  40. package/src/addons/orms/sequelize/fastify/src/modules/_base/index.ts +0 -20
  41. package/src/addons/orms/sequelize/fastify/tests/modules/app.test.ts +0 -20
  42. package/src/addons/orms/sequelize/fastify/vitest.config.ts +0 -31
  43. package/src/addons/orms/sequelize/gen-entity/express-router.ts +0 -17
  44. package/src/addons/orms/sequelize/gen-entity/express-test.ts +0 -65
  45. package/src/addons/orms/sequelize/gen-entity/fastify-router.ts +0 -19
  46. package/src/addons/orms/sequelize/gen-entity/fastify-test.ts +0 -89
  47. package/src/addons/orms/sequelize/gen-entity/model.ts +0 -21
  48. package/src/addons/orms/sequelize/manifest.json +0 -53
  49. package/src/addons/orms/sequelize/shared/scripts/db-sync.ts +0 -14
  50. package/src/addons/orms/sequelize/shared/src/db/client.ts +0 -19
  51. package/src/addons/orms/sequelize/shared/src/models/index.ts +0 -9
  52. package/src/addons/orms/sequelize/shared/src/modules/_base/query-engine.ts +0 -101
  53. package/src/addons/orms/sequelize/shared/src/modules/_base/registry.ts +0 -15
  54. package/src/addons/orms/typeorm/express/src/app.ts +0 -82
  55. package/src/addons/orms/typeorm/express/src/modules/_base/auto-routes.ts +0 -249
  56. package/src/addons/orms/typeorm/express/src/modules/_base/index.ts +0 -19
  57. package/src/addons/orms/typeorm/express/src/server.ts +0 -43
  58. package/src/addons/orms/typeorm/express/tests/app.test.ts +0 -24
  59. package/src/addons/orms/typeorm/express/vitest.config.ts +0 -20
  60. package/src/addons/orms/typeorm/fastify/src/app.ts +0 -86
  61. package/src/addons/orms/typeorm/fastify/src/modules/_base/auto-routes.ts +0 -239
  62. package/src/addons/orms/typeorm/fastify/src/modules/_base/index.ts +0 -19
  63. package/src/addons/orms/typeorm/fastify/tests/modules/app.test.ts +0 -20
  64. package/src/addons/orms/typeorm/fastify/vitest.config.ts +0 -31
  65. package/src/addons/orms/typeorm/gen-entity/entity.ts +0 -21
  66. package/src/addons/orms/typeorm/gen-entity/express-router.ts +0 -17
  67. package/src/addons/orms/typeorm/gen-entity/express-test.ts +0 -66
  68. package/src/addons/orms/typeorm/gen-entity/fastify-router.ts +0 -19
  69. package/src/addons/orms/typeorm/gen-entity/fastify-test.ts +0 -89
  70. package/src/addons/orms/typeorm/manifest.json +0 -53
  71. package/src/addons/orms/typeorm/shared/scripts/db-sync.ts +0 -14
  72. package/src/addons/orms/typeorm/shared/src/db/data-source.ts +0 -21
  73. package/src/addons/orms/typeorm/shared/src/entities/index.ts +0 -8
  74. package/src/addons/orms/typeorm/shared/src/modules/_base/query-engine.ts +0 -94
  75. package/src/addons/orms/typeorm/shared/src/modules/_base/registry.ts +0 -15
  76. package/src/addons/orms/typeorm/shared/tsconfig.json +0 -16
@@ -35,7 +35,7 @@ import {
35
35
  upsertComponentMarker,
36
36
  writeComponentMarker,
37
37
  writeProjxConfig
38
- } from "./chunk-HAT7D4G2.js";
38
+ } from "./chunk-FQPOK3QZ.js";
39
39
  export {
40
40
  COMPONENTS,
41
41
  COMPONENT_MARKER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-projx",
3
- "version": "1.7.0",
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, auto-entity CRUD' %> |
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 |
@@ -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 + test + audit)
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 "Formatting <%= inst.path %>..."
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
- });