@sundaysf/cli-v3 0.0.1
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 +302 -0
- package/dist/cli.js +1327 -0
- package/package.json +55 -0
- package/templates/api/.claude/agents/knex-table-implementer.md +36 -0
- package/templates/api/.claude/agents/sundays-backend-builder.md +32 -0
- package/templates/api/.env.example +25 -0
- package/templates/api/.github/workflows/ci.yaml +45 -0
- package/templates/api/.github/workflows/deploy.yaml +72 -0
- package/templates/api/.prettierignore +5 -0
- package/templates/api/.prettierrc +9 -0
- package/templates/api/.sundaysrc +8 -0
- package/templates/api/CLAUDE.md +81 -0
- package/templates/api/Dockerfile +17 -0
- package/templates/api/README.md +164 -0
- package/templates/api/_dockerignore +8 -0
- package/templates/api/_gitignore +23 -0
- package/templates/api/_package.json +58 -0
- package/templates/api/docker-compose.yml +21 -0
- package/templates/api/eslint.config.js +27 -0
- package/templates/api/jest.config.js +33 -0
- package/templates/api/jest.setup.js +25 -0
- package/templates/api/knexfile.ts +5 -0
- package/templates/api/src/app.ts +48 -0
- package/templates/api/src/common/__tests__/common.test.ts +116 -0
- package/templates/api/src/common/config/env.ts +53 -0
- package/templates/api/src/common/errors/http.error.ts +30 -0
- package/templates/api/src/common/logger/index.ts +25 -0
- package/templates/api/src/common/utils/environment.resolver.ts +7 -0
- package/templates/api/src/common/utils/pagination.ts +25 -0
- package/templates/api/src/common/utils/version.resolver.ts +24 -0
- package/templates/api/src/common/validation/parse-dto.ts +20 -0
- package/templates/api/src/controllers/health/__tests__/health.controller.test.ts +52 -0
- package/templates/api/src/controllers/health/health.controller.ts +26 -0
- package/templates/api/src/db/BaseDAO.ts +92 -0
- package/templates/api/src/db/KnexConnection.ts +59 -0
- package/templates/api/src/db/__tests__/base-dao.test.ts +73 -0
- package/templates/api/src/db/__tests__/index.barrel.test.ts +10 -0
- package/templates/api/src/db/__tests__/knex-connection.test.ts +90 -0
- package/templates/api/src/db/d.types.ts +42 -0
- package/templates/api/src/db/dao/sundays-package-version/sundays-package-version.dao.ts +12 -0
- package/templates/api/src/db/index.ts +17 -0
- package/templates/api/src/db/interfaces/sundays-package-version/sundays-package-version.interfaces.ts +5 -0
- package/templates/api/src/db/knex.config.ts +46 -0
- package/templates/api/src/dto/input/.gitkeep +0 -0
- package/templates/api/src/jobs/.gitkeep +0 -0
- package/templates/api/src/middlewares/error/__tests__/error.middleware.test.ts +117 -0
- package/templates/api/src/middlewares/error/error.middleware.ts +70 -0
- package/templates/api/src/middlewares/not-found/__tests__/not-found.middleware.test.ts +54 -0
- package/templates/api/src/middlewares/not-found/not-found.middleware.ts +51 -0
- package/templates/api/src/middlewares/request-id/__tests__/request-id.middleware.test.ts +31 -0
- package/templates/api/src/middlewares/request-id/request-id.middleware.ts +20 -0
- package/templates/api/src/migrations/20240101000000_create_sundays_package_version.ts +15 -0
- package/templates/api/src/routes/__tests__/index-router.test.ts +61 -0
- package/templates/api/src/routes/health/__tests__/health.routes.test.ts +22 -0
- package/templates/api/src/routes/health/health.router.ts +18 -0
- package/templates/api/src/routes/index.ts +77 -0
- package/templates/api/src/seeds/001_sundays_package_version.ts +14 -0
- package/templates/api/src/server.ts +56 -0
- package/templates/api/src/services/.gitkeep +0 -0
- package/templates/api/tsconfig.json +20 -0
- package/templates/api/tsconfig.spec.json +10 -0
- package/templates/api-auth/overlay.json +95 -0
- package/templates/api-auth/src/controllers/auth/__tests__/auth.controller.test.ts +194 -0
- package/templates/api-auth/src/controllers/auth/auth.controller.ts +109 -0
- package/templates/api-auth/src/db/dao/auth/auth.dao.ts +21 -0
- package/templates/api-auth/src/db/dao/user/user.dao.ts +24 -0
- package/templates/api-auth/src/db/interfaces/auth/auth.interfaces.ts +8 -0
- package/templates/api-auth/src/db/interfaces/user/user.interfaces.ts +11 -0
- package/templates/api-auth/src/dto/input/auth/auth.login.dto.ts +14 -0
- package/templates/api-auth/src/dto/input/auth/auth.register.dto.ts +19 -0
- package/templates/api-auth/src/middlewares/auth/__tests__/auth.middleware.test.ts +52 -0
- package/templates/api-auth/src/middlewares/auth/auth.middleware.ts +56 -0
- package/templates/api-auth/src/migrations/20240101000001_create_user.ts +18 -0
- package/templates/api-auth/src/migrations/20240101000002_create_auth.ts +24 -0
- package/templates/api-auth/src/routes/auth/__tests__/auth.routes.test.ts +83 -0
- package/templates/api-auth/src/routes/auth/auth.router.ts +28 -0
- package/templates/api-auth/src/services/jwt/__tests__/jwt.service.test.ts +32 -0
- package/templates/api-auth/src/services/jwt/jwt.service.ts +36 -0
- package/templates/api-auth/src/services/password/__tests__/password.service.test.ts +13 -0
- package/templates/api-auth/src/services/password/password.service.ts +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sundaysf/cli-v3",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Sundays Framework v3 CLI - scaffolds an Express 5 + Knex + Postgres API and generates entity verticals",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sundaysf": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsx src/cli.ts",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"test": "vitest run --exclude test/e2e",
|
|
22
|
+
"test:e2e": "SUNDAYS_E2E=1 vitest run test/e2e",
|
|
23
|
+
"lint": "eslint .",
|
|
24
|
+
"format": "prettier --write .",
|
|
25
|
+
"prepublishOnly": "npm run build && npm test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"sundays",
|
|
29
|
+
"scaffold",
|
|
30
|
+
"express",
|
|
31
|
+
"knex",
|
|
32
|
+
"cli"
|
|
33
|
+
],
|
|
34
|
+
"author": "Pablo Dominguez",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@clack/prompts": "^1.8.1",
|
|
38
|
+
"commander": "^15.0.0",
|
|
39
|
+
"execa": "^10.0.1",
|
|
40
|
+
"fs-extra": "^11.4.0",
|
|
41
|
+
"picocolors": "^1.1.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^10.0.0",
|
|
45
|
+
"@types/fs-extra": "^11.0.4",
|
|
46
|
+
"@types/node": "^24.0.0",
|
|
47
|
+
"eslint": "^10.11.0",
|
|
48
|
+
"prettier": "^3.9.8",
|
|
49
|
+
"tsup": "^8.5.1",
|
|
50
|
+
"tsx": "^4.23.15",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"typescript-eslint": "^8.70.0",
|
|
53
|
+
"vitest": "^5.0.1"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: knex-table-implementer
|
|
3
|
+
description: Use this agent to add or change database tables in this Sundays Framework v3 project - migrations, interfaces, DAOs and the src/db barrel - following the project's Knex conventions. Examples - "add an order_item table", "add a nullable deletedAt column to product", "write a DAO method that lists products with their category".
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: red
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a database-layer specialist for a Sundays Framework v3 API (Knex 3 + PostgreSQL).
|
|
9
|
+
Read `CLAUDE.md` first.
|
|
10
|
+
|
|
11
|
+
## New table
|
|
12
|
+
|
|
13
|
+
Run `sundaysf generate entity <name> <fields...>` (it writes the migration, interface, DAO, DTOs,
|
|
14
|
+
controller, router, tests and barrel exports). Then refine the generated migration if needed.
|
|
15
|
+
|
|
16
|
+
## Changing an existing table
|
|
17
|
+
|
|
18
|
+
1. `npm run db:make-migration -- <verb>_<table>` creates `src/migrations/<timestamp>_<verb>_<table>.ts`.
|
|
19
|
+
2. Implement `up` and `down` with the knex schema builder. Columns camelCase, tables snake_case,
|
|
20
|
+
foreign keys `.references('id').inTable('<table>').onDelete('CASCADE')` plus `.index()`.
|
|
21
|
+
3. Update `src/db/interfaces/<x>/<x>.interfaces.ts` (`interface IX extends IEntity`).
|
|
22
|
+
4. Add finders to `src/db/dao/<x>/<x>.dao.ts`. DAOs extend `BaseDAO<IX>`; use `this.q(trx)` for
|
|
23
|
+
the query builder and accept an optional `trx?: Knex.Transaction` on every method.
|
|
24
|
+
Relations: `leftJoin` + `this._knex.raw('to_jsonb(r.*) as related')`.
|
|
25
|
+
5. If you create a new DAO or interface by hand, export them in `src/db/index.ts` above the
|
|
26
|
+
`// @sundays:daos` / `// @sundays:interfaces` markers.
|
|
27
|
+
6. `npm run db:migrate`, then `npm run db:rollback && npm run db:migrate` to prove `down` works.
|
|
28
|
+
7. Add or update tests under `src/db/dao/<x>/__tests__/` (real Postgres, clean up in `afterAll`).
|
|
29
|
+
|
|
30
|
+
## Rules
|
|
31
|
+
|
|
32
|
+
- Never edit an applied migration; write a new one.
|
|
33
|
+
- Every table has `id` (increments), `uuid` (unique), `createdAt`, `updatedAt`
|
|
34
|
+
(`timestamp(...).notNullable().defaultTo(knex.fn.now())`).
|
|
35
|
+
- Return `null` for not-found, never throw from a DAO for missing rows.
|
|
36
|
+
- Keep `knexfile.ts` untouched: configuration lives in `src/db/knex.config.ts`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sundays-backend-builder
|
|
3
|
+
description: Use this agent to create or modify backend components (endpoints, controllers, routers, services, DTOs) in this Sundays Framework v3 project so they follow the project conventions exactly. Examples - "add a products API with CRUD", "add a POST /orders/:uuid/cancel endpoint", "this controller doesn't follow our standards, fix it".
|
|
4
|
+
model: sonnet
|
|
5
|
+
color: blue
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior backend developer working on a Sundays Framework v3 API (Express 5 + TypeScript +
|
|
9
|
+
Knex + PostgreSQL + Zod). Read `CLAUDE.md` first; it is the source of truth.
|
|
10
|
+
|
|
11
|
+
## How you work
|
|
12
|
+
|
|
13
|
+
1. **New entity?** Run `sundaysf generate entity <name> <fields...>` first, then adapt the output.
|
|
14
|
+
Never hand-write what the generator produces.
|
|
15
|
+
2. **New endpoint on an existing entity?** Add the route in `src/routes/<x>/<x>.router.ts`
|
|
16
|
+
(`.bind()` the handler), the method in `src/controllers/<x>/<x>.controller.ts`, a DTO in
|
|
17
|
+
`src/dto/input/<x>/` when it takes a body, and DAO methods in `src/db/dao/<x>/<x>.dao.ts`.
|
|
18
|
+
3. **Cross-cutting logic** (email, storage, external APIs) goes in `src/services/<x>/<x>.service.ts`.
|
|
19
|
+
4. Write tests next to the code (`__tests__/`): unit test for the controller (mock `'../../../db'`),
|
|
20
|
+
route test with supertest when the endpoint touches the database.
|
|
21
|
+
5. Finish with `npm run typecheck && npm run lint && npm test`.
|
|
22
|
+
|
|
23
|
+
## Rules
|
|
24
|
+
|
|
25
|
+
- Response envelope `{ success, data }` / `{ success, message, errors? }`; paginated lists return
|
|
26
|
+
`dao.getAll(page, limit)` directly.
|
|
27
|
+
- Validation through Zod DTOs (`validateXCreate(req.body)`), never manual `if (!body.x)` checks.
|
|
28
|
+
- Errors through `HttpError` helpers (`notFound`, `conflict`, `badRequest`, `unauthorized`), never
|
|
29
|
+
`res.status(500)` by hand; unexpected errors go to `next(err)`.
|
|
30
|
+
- Public ids are `uuid`; resolve to numeric `id` before DAO writes.
|
|
31
|
+
- Express 5 routing rules (no `*`, `{/:id}` for optional params).
|
|
32
|
+
- No new dependencies without saying why.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# ------------------------------------------------------------------------------
|
|
2
|
+
# __SF_PROJECT_NAME__ - environment variables
|
|
3
|
+
# Copy to .env (sundaysf new already did) and adjust. Never commit .env.
|
|
4
|
+
# ------------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
# Server
|
|
7
|
+
PORT=__SF_PORT__
|
|
8
|
+
NODE_ENV=development
|
|
9
|
+
# pino level: fatal | error | warn | info | debug | trace | silent
|
|
10
|
+
LOG_LEVEL=info
|
|
11
|
+
# Comma separated list of allowed origins, or * for any
|
|
12
|
+
CORS_ORIGINS=*
|
|
13
|
+
|
|
14
|
+
# Database (matches docker-compose.yml)
|
|
15
|
+
SQL_HOST=localhost
|
|
16
|
+
SQL_PORT=5432
|
|
17
|
+
SQL_USER=postgres
|
|
18
|
+
SQL_PASSWORD=postgres
|
|
19
|
+
SQL_DB_NAME=__SF_DB_NAME__
|
|
20
|
+
# Set to false to accept self-signed certificates on remote hosts (SSL is off for localhost)
|
|
21
|
+
SQL_REJECT_UNAUTHORIZED=true
|
|
22
|
+
# Run pending migrations on boot (useful in containers). Default: false
|
|
23
|
+
RUN_MIGRATIONS=false
|
|
24
|
+
|
|
25
|
+
# @sundays:env
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Typecheck, lint and test
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
services:
|
|
13
|
+
postgres:
|
|
14
|
+
image: postgres:16-alpine
|
|
15
|
+
env:
|
|
16
|
+
POSTGRES_USER: postgres
|
|
17
|
+
POSTGRES_PASSWORD: postgres
|
|
18
|
+
POSTGRES_DB: __SF_DB_NAME__
|
|
19
|
+
ports:
|
|
20
|
+
- 5432:5432
|
|
21
|
+
options: >-
|
|
22
|
+
--health-cmd "pg_isready -U postgres"
|
|
23
|
+
--health-interval 5s
|
|
24
|
+
--health-timeout 3s
|
|
25
|
+
--health-retries 10
|
|
26
|
+
env:
|
|
27
|
+
NODE_ENV: test
|
|
28
|
+
SQL_HOST: localhost
|
|
29
|
+
SQL_PORT: 5432
|
|
30
|
+
SQL_USER: postgres
|
|
31
|
+
SQL_PASSWORD: postgres
|
|
32
|
+
SQL_DB_NAME: __SF_DB_NAME__
|
|
33
|
+
# @sundays:ci-env
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v4
|
|
36
|
+
- uses: actions/setup-node@v4
|
|
37
|
+
with:
|
|
38
|
+
node-version: 24
|
|
39
|
+
cache: npm
|
|
40
|
+
- run: npm ci
|
|
41
|
+
- run: npm run typecheck
|
|
42
|
+
- run: npm run lint
|
|
43
|
+
- run: npm run db:migrate
|
|
44
|
+
- run: npm test
|
|
45
|
+
- run: npm run build
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: Deploy to AWS ECS
|
|
2
|
+
|
|
3
|
+
# Manual deploy. Requires in the GitHub repo:
|
|
4
|
+
# secrets.AWS_ROLE_ARN - IAM role assumed via OIDC (needs ECR push + ECS deploy)
|
|
5
|
+
# vars.AWS_REGION - e.g. us-east-1
|
|
6
|
+
# and, already created in AWS: an ECR repository, an ECS cluster/service and a task
|
|
7
|
+
# definition, all named after the project (change the env block below if they differ).
|
|
8
|
+
on:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
env:
|
|
12
|
+
AWS_REGION: ${{ vars.AWS_REGION || 'us-east-1' }}
|
|
13
|
+
ECR_REPOSITORY: __SF_PROJECT_SLUG__
|
|
14
|
+
ECS_CLUSTER: __SF_PROJECT_SLUG__
|
|
15
|
+
ECS_SERVICE: __SF_PROJECT_SLUG__
|
|
16
|
+
ECS_TASK_DEFINITION: __SF_PROJECT_SLUG__
|
|
17
|
+
CONTAINER_NAME: __SF_PROJECT_SLUG__
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
deploy:
|
|
21
|
+
name: Build, push and deploy
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
contents: read
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Read package version
|
|
30
|
+
id: pkg
|
|
31
|
+
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
|
|
32
|
+
|
|
33
|
+
- name: Configure AWS credentials
|
|
34
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
35
|
+
with:
|
|
36
|
+
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
|
|
37
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
38
|
+
|
|
39
|
+
- name: Login to Amazon ECR
|
|
40
|
+
id: ecr
|
|
41
|
+
uses: aws-actions/amazon-ecr-login@v2
|
|
42
|
+
|
|
43
|
+
- name: Build, tag and push image
|
|
44
|
+
id: image
|
|
45
|
+
env:
|
|
46
|
+
REGISTRY: ${{ steps.ecr.outputs.registry }}
|
|
47
|
+
TAG: ${{ steps.pkg.outputs.version }}
|
|
48
|
+
run: |
|
|
49
|
+
docker build -t "$REGISTRY/$ECR_REPOSITORY:$TAG" .
|
|
50
|
+
docker tag "$REGISTRY/$ECR_REPOSITORY:$TAG" "$REGISTRY/$ECR_REPOSITORY:latest"
|
|
51
|
+
docker push "$REGISTRY/$ECR_REPOSITORY:$TAG"
|
|
52
|
+
docker push "$REGISTRY/$ECR_REPOSITORY:latest"
|
|
53
|
+
echo "image=$REGISTRY/$ECR_REPOSITORY:$TAG" >> "$GITHUB_OUTPUT"
|
|
54
|
+
|
|
55
|
+
- name: Download current task definition
|
|
56
|
+
run: aws ecs describe-task-definition --task-definition "$ECS_TASK_DEFINITION" --query taskDefinition > task-definition.json
|
|
57
|
+
|
|
58
|
+
- name: Render new task definition
|
|
59
|
+
id: taskdef
|
|
60
|
+
uses: aws-actions/amazon-ecs-render-task-definition@v1
|
|
61
|
+
with:
|
|
62
|
+
task-definition: task-definition.json
|
|
63
|
+
container-name: ${{ env.CONTAINER_NAME }}
|
|
64
|
+
image: ${{ steps.image.outputs.image }}
|
|
65
|
+
|
|
66
|
+
- name: Deploy
|
|
67
|
+
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
|
|
68
|
+
with:
|
|
69
|
+
task-definition: ${{ steps.taskdef.outputs.task-definition }}
|
|
70
|
+
service: ${{ env.ECS_SERVICE }}
|
|
71
|
+
cluster: ${{ env.ECS_CLUSTER }}
|
|
72
|
+
wait-for-service-stability: true
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for Claude Code when working in this repository (an API generated by Sundays
|
|
4
|
+
Framework v__SF_CLI_VERSION__).
|
|
5
|
+
|
|
6
|
+
## Commands
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm run start:dev # dev server with reload (tsx watch, no type-check)
|
|
10
|
+
npm run typecheck # tsc --noEmit (sources + tests)
|
|
11
|
+
npm test # jest with coverage; routes/ and db/ suites need Postgres (docker compose up -d)
|
|
12
|
+
npm run test:unit # jest without database suites
|
|
13
|
+
npm run lint # eslint
|
|
14
|
+
npm run format # prettier
|
|
15
|
+
npm run db:migrate # apply src/migrations
|
|
16
|
+
npm run db:rollback # undo last batch
|
|
17
|
+
npm run db:make-migration -- <name>
|
|
18
|
+
npm run db:seed
|
|
19
|
+
sundaysf generate entity <name> <fields...> # scaffold a full entity (see below)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Before finishing any task run `npm run typecheck && npm run lint && npm test`.
|
|
23
|
+
|
|
24
|
+
## Architecture
|
|
25
|
+
|
|
26
|
+
Express 5 + TypeScript (CommonJS, ES2022) + Knex 3 + PostgreSQL. Layers, top to bottom:
|
|
27
|
+
|
|
28
|
+
1. `src/routes/<x>/<x>.router.ts` — class with `public router: Router`; auto-mounted at
|
|
29
|
+
`/api/<x>` by `src/routes/index.ts` (folder name = mount path, no manual registration).
|
|
30
|
+
2. `src/controllers/<x>/<x>.controller.ts` — class; one `async method(req, res, next)` per
|
|
31
|
+
endpoint wrapped in `try { } catch (err) { next(err) }`; DAOs as `private _xDAO = new XDAO()`.
|
|
32
|
+
3. `src/dto/input/<x>/<x>.{create,update}.dto.ts` — Zod schema + `validateXCreate(input)` which
|
|
33
|
+
throws `HttpError(400)` with field errors.
|
|
34
|
+
4. `src/services/<x>/<x>.service.ts` — cross-cutting logic (email, storage, tokens...).
|
|
35
|
+
5. `src/db/dao/<x>/<x>.dao.ts` — `class XDAO extends BaseDAO<IX> { protected readonly table = 'x' }`
|
|
36
|
+
plus custom finders; `src/db/interfaces/<x>/<x>.interfaces.ts` — `interface IX extends IEntity`.
|
|
37
|
+
6. `src/db/index.ts` — barrel. Import DAOs/types from `'../../db'`. Keep the
|
|
38
|
+
`// @sundays:interfaces` and `// @sundays:daos` markers: generators append above them.
|
|
39
|
+
|
|
40
|
+
Boot: `src/server.ts` validates env (`src/common/config/env.ts`), connects `KnexManager`,
|
|
41
|
+
optionally runs migrations (`RUN_MIGRATIONS=true`), then imports `src/app.ts` and listens.
|
|
42
|
+
SIGTERM/SIGINT close the server and the pool.
|
|
43
|
+
|
|
44
|
+
## Conventions (do not deviate)
|
|
45
|
+
|
|
46
|
+
- Response envelope: `{ success: true, data }` / `{ success: false, message, errors? }`.
|
|
47
|
+
Paginated lists return `IDataPaginator` from `dao.getAll(page, limit)` as-is (already has `success`).
|
|
48
|
+
- Public identifier is `uuid`; numeric `id` never leaves the API. Routes use `/:uuid`.
|
|
49
|
+
- Table names snake_case, columns camelCase, every table has `id`, `uuid`, `createdAt`, `updatedAt`.
|
|
50
|
+
- Migrations live in `src/migrations/` (TypeScript), named `<timestamp>_<verb>_<table>.ts`.
|
|
51
|
+
- Pagination params via `getPagination(req.query)` (`src/common/utils/pagination.ts`).
|
|
52
|
+
- Errors: `throw notFound('X not found')`, `conflict(...)`, `badRequest(...)`, `unauthorized(...)`
|
|
53
|
+
from `src/common/errors/http.error.ts`. Never `res.status(500)` by hand.
|
|
54
|
+
- Logging: `req.log` inside handlers, `logger` (`src/common/logger`) elsewhere. No `console.*`.
|
|
55
|
+
- New env vars go in the Zod schema in `src/common/config/env.ts`, `.env.example` and README.
|
|
56
|
+
- Express 5 routing: no `*` (use `/*splat`), optional params `{/:id}`, no regex strings,
|
|
57
|
+
`req.query` is read-only, `req.body` is `undefined` when no parser matched.
|
|
58
|
+
- Tests co-located in `__tests__/`: controller unit tests mock `'../../../db'` and use a
|
|
59
|
+
`mockRes()` helper; route tests use supertest + real Postgres and clean up in `afterAll`.
|
|
60
|
+
- Keep coverage above the threshold in `jest.config.js`.
|
|
61
|
+
|
|
62
|
+
## Adding an entity
|
|
63
|
+
|
|
64
|
+
Always start with the generator, then adapt:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
sundaysf generate entity product name:string:unique price:decimal categoryId:category.id isActive:boolean=true
|
|
68
|
+
npm run db:migrate
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Field syntax `name:type[?][:unique][=default]`; types `string text integer decimal boolean date
|
|
72
|
+
datetime json uuid <entity>.id`. It creates migration, interface, DAO, DTOs, controller, router,
|
|
73
|
+
unit test and route test, and updates `src/db/index.ts`. Add custom endpoints to the generated
|
|
74
|
+
router/controller; add custom queries to the DAO using `this.q(trx)`.
|
|
75
|
+
|
|
76
|
+
Transactions: `KnexManager.getConnection().transaction(async (trx) => { await dao.create(x, trx); })`.
|
|
77
|
+
Relations: prefer `leftJoin` + `this._knex.raw('to_jsonb(r.*) as related')` inside the DAO.
|
|
78
|
+
|
|
79
|
+
## Features in this project
|
|
80
|
+
|
|
81
|
+
<!-- @sundays:features -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ---- build stage ------------------------------------------------------------
|
|
2
|
+
FROM node:24-alpine AS builder
|
|
3
|
+
WORKDIR /var/api
|
|
4
|
+
COPY package*.json ./
|
|
5
|
+
RUN npm ci
|
|
6
|
+
COPY . .
|
|
7
|
+
RUN npm run build
|
|
8
|
+
|
|
9
|
+
# ---- runtime stage ----------------------------------------------------------
|
|
10
|
+
FROM node:24-alpine
|
|
11
|
+
ENV NODE_ENV=production
|
|
12
|
+
WORKDIR /var/api
|
|
13
|
+
COPY package*.json ./
|
|
14
|
+
RUN npm ci --omit=dev
|
|
15
|
+
COPY --from=builder /var/api/dist ./dist
|
|
16
|
+
EXPOSE __SF_PORT__
|
|
17
|
+
CMD ["node", "dist/server.js"]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# **SF_PROJECT_NAME**
|
|
2
|
+
|
|
3
|
+
REST API built with [Sundays Framework](https://github.com/sundaysf) v__SF_CLI_VERSION__:
|
|
4
|
+
Express 5 · TypeScript · Knex · PostgreSQL · Zod · pino · Jest.
|
|
5
|
+
|
|
6
|
+
## Quick start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install # once
|
|
10
|
+
docker compose up -d # local PostgreSQL 16 on port 5432
|
|
11
|
+
npm run db:migrate # apply migrations
|
|
12
|
+
npm run start:dev # http://localhost:__SF_PORT__/api/health (reloads on change)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`sundaysf new` already created `.env` from `.env.example`. Add a new entity with:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
sundaysf generate entity product name:string:unique price:decimal isActive:boolean=true
|
|
19
|
+
npm run db:migrate
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Scripts
|
|
23
|
+
|
|
24
|
+
| Script | What it does |
|
|
25
|
+
| ------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `npm run start:dev` | Dev server with reload (`tsx watch`). Does not type-check: run `typecheck` or rely on the editor. |
|
|
27
|
+
| `npm run build` | Compile `src/` to `dist/` with `tsc`. |
|
|
28
|
+
| `npm start` | Run the compiled server (`node dist/server.js`). |
|
|
29
|
+
| `npm run typecheck` | `tsc --noEmit` over sources and tests. |
|
|
30
|
+
| `npm test` | Jest with coverage. Route and DB suites need the local Postgres. |
|
|
31
|
+
| `npm run test:unit` | Jest without the `routes/` and `db/` suites (no database needed). |
|
|
32
|
+
| `npm run test:watch` | Jest in watch mode. |
|
|
33
|
+
| `npm run lint` / `npm run format` | ESLint / Prettier. |
|
|
34
|
+
| `npm run db:migrate` | Apply pending migrations from `src/migrations/`. |
|
|
35
|
+
| `npm run db:rollback` | Roll back the last migration batch. |
|
|
36
|
+
| `npm run db:status` | List applied and pending migrations. |
|
|
37
|
+
| `npm run db:seed` | Run seeds from `src/seeds/`. |
|
|
38
|
+
| `npm run db:make-migration -- <name>` | Create `src/migrations/<timestamp>_<name>.ts`. |
|
|
39
|
+
| `npm run db:make-seed -- <name>` | Create `src/seeds/<name>.ts`. |
|
|
40
|
+
|
|
41
|
+
## Environment variables
|
|
42
|
+
|
|
43
|
+
Validated at boot by `src/common/config/env.ts` (Zod). A missing or malformed value stops the
|
|
44
|
+
process with a readable error. Add new variables to that schema, to `.env.example`, and here.
|
|
45
|
+
|
|
46
|
+
| Variable | Default | Description |
|
|
47
|
+
| ------------------------- | ------------- | ------------------------------------------------------------------------- |
|
|
48
|
+
| `PORT` | `__SF_PORT__` | HTTP port. |
|
|
49
|
+
| `NODE_ENV` | `development` | `development`, `test` or `production`. Pretty logs only in development. |
|
|
50
|
+
| `LOG_LEVEL` | `info` | pino level: `fatal`, `error`, `warn`, `info`, `debug`, `trace`, `silent`. |
|
|
51
|
+
| `CORS_ORIGINS` | `*` | Comma separated allowed origins, or `*`. |
|
|
52
|
+
| `SQL_HOST` | `localhost` | PostgreSQL host. SSL is disabled for localhost. |
|
|
53
|
+
| `SQL_PORT` | `5432` | PostgreSQL port. |
|
|
54
|
+
| `SQL_USER` | - | PostgreSQL user. |
|
|
55
|
+
| `SQL_PASSWORD` | - | PostgreSQL password. |
|
|
56
|
+
| `SQL_DB_NAME` | - | Database name. |
|
|
57
|
+
| `SQL_REJECT_UNAUTHORIZED` | `true` | Set `false` to accept self-signed certificates on remote hosts. |
|
|
58
|
+
| `RUN_MIGRATIONS` | `false` | Run pending migrations on boot (handy for containers). |
|
|
59
|
+
|
|
60
|
+
<!-- @sundays:readme-env -->
|
|
61
|
+
|
|
62
|
+
## Project layout
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
src/
|
|
66
|
+
server.ts boot: env -> db -> (migrations) -> app.listen, graceful shutdown
|
|
67
|
+
app.ts express: helmet, cors, request id, pino-http, parsers, /api, 404, errors
|
|
68
|
+
routes/
|
|
69
|
+
index.ts auto-discovery: routes/<x>/<x>.router.ts is mounted at /api/<x>
|
|
70
|
+
<entity>/<entity>.router.ts
|
|
71
|
+
controllers/<entity>/<entity>.controller.ts
|
|
72
|
+
dto/input/<entity>/<entity>.{create,update}.dto.ts Zod schemas + validate helpers
|
|
73
|
+
services/<name>/<name>.service.ts cross-cutting logic (email, s3, jwt...)
|
|
74
|
+
middlewares/<name>/<name>.middleware.ts
|
|
75
|
+
jobs/<name>.job.ts cron jobs (export run() and schedule(); register schedule() in server.ts)
|
|
76
|
+
db/
|
|
77
|
+
index.ts barrel with @sundays markers (generators append here)
|
|
78
|
+
BaseDAO.ts generic CRUD + pagination + optional transaction
|
|
79
|
+
KnexConnection.ts KnexManager singleton (connect / getConnection / disconnect)
|
|
80
|
+
knex.config.ts the one knex config (also used by knexfile.ts for the CLI)
|
|
81
|
+
dao/<entity>/<entity>.dao.ts
|
|
82
|
+
interfaces/<entity>/<entity>.interfaces.ts
|
|
83
|
+
migrations/ knex migrations (TypeScript, compiled with the app)
|
|
84
|
+
seeds/
|
|
85
|
+
common/
|
|
86
|
+
config/env.ts validated environment
|
|
87
|
+
logger/ pino
|
|
88
|
+
errors/http.error.ts HttpError + badRequest()/notFound()/... helpers
|
|
89
|
+
validation/parse-dto.ts parseDto(schema, input)
|
|
90
|
+
utils/ pagination, version/environment resolvers
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Every file has its tests next to it in a `__tests__/` folder.
|
|
94
|
+
|
|
95
|
+
## Conventions
|
|
96
|
+
|
|
97
|
+
- **Response envelope**: `{ success: true, data }` on success, `{ success: false, message, errors? }`
|
|
98
|
+
on failure. Paginated lists return `IDataPaginator`:
|
|
99
|
+
`{ success, data, page, limit, count, totalCount, totalPages }`.
|
|
100
|
+
- **Ids**: tables have a numeric `id` (internal) and a `uuid` (public). Routes take `/:uuid`;
|
|
101
|
+
controllers resolve the row and use `id` for DAO writes.
|
|
102
|
+
- **Columns** are camelCase (`createdAt`, `categoryId`); **tables** are snake_case (`product_category`).
|
|
103
|
+
- **Controllers** are classes with `private _xDAO = new XDAO()` members and
|
|
104
|
+
`async method(req, res, next)` handlers wrapped in `try { } catch (err) { next(err) }`.
|
|
105
|
+
- **Routers** are classes exposing `public router: Router` and bind handlers with `.bind()`.
|
|
106
|
+
- **Validation**: `const input = validateProductCreate(req.body)` throws `HttpError(400)` with
|
|
107
|
+
per-field errors; the error middleware renders it.
|
|
108
|
+
- **Errors**: throw `notFound('Product not found')`, `conflict(...)`, etc. from
|
|
109
|
+
`src/common/errors/http.error.ts`, or `next(err)` for anything else.
|
|
110
|
+
- **Express 5**: no `*` wildcards (`/*splat`), optional params are `{/:id}`, `req.query` is read-only.
|
|
111
|
+
|
|
112
|
+
## Adding an entity
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
sundaysf generate entity product \
|
|
116
|
+
name:string:unique price:decimal categoryId:category.id isActive:boolean=true description:text?
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
This writes the migration, interface, DAO, create/update DTOs, controller, router and tests, and
|
|
120
|
+
registers the DAO in `src/db/index.ts`. Then:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm run db:migrate
|
|
124
|
+
npm run start:dev
|
|
125
|
+
curl -X POST localhost:__SF_PORT__/api/product -H 'content-type: application/json' \
|
|
126
|
+
-d '{"name":"Widget","price":9.5,"categoryId":1}'
|
|
127
|
+
npm test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Field syntax: `name:type[?][:unique][=default]` with types `string`, `text`, `integer`,
|
|
131
|
+
`decimal`, `boolean`, `date`, `datetime`, `json`, `uuid` or `<entity>.id` (foreign key).
|
|
132
|
+
|
|
133
|
+
## Endpoints
|
|
134
|
+
|
|
135
|
+
### Health
|
|
136
|
+
|
|
137
|
+
| Method | Path | Response |
|
|
138
|
+
| ------ | ------------- | ------------------------------------------------------------- |
|
|
139
|
+
| GET | `/api/health` | `{ success, health, version, environment, database, uptime }` |
|
|
140
|
+
|
|
141
|
+
<!-- @sundays:readme-endpoints -->
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
- `npm test` runs everything with coverage (threshold in `jest.config.js`). Route and DB suites
|
|
146
|
+
connect to the `SQL_*` database from `.env` and clean up what they create.
|
|
147
|
+
- `npm run test:unit` skips them so you can test without Postgres.
|
|
148
|
+
- `jest.setup.js` refuses to run against a non-local `SQL_HOST` unless `ALLOW_REMOTE_DB_TESTS=1`.
|
|
149
|
+
- Controller unit tests mock the `src/db` barrel with `jest.mock('../../../db', ...)`; route tests
|
|
150
|
+
use `supertest` against `app` with a real database.
|
|
151
|
+
|
|
152
|
+
## Deploy
|
|
153
|
+
|
|
154
|
+
- `Dockerfile`: two-stage build on `node:24-alpine`, `EXPOSE __SF_PORT__`, runs `node dist/server.js`.
|
|
155
|
+
Set `RUN_MIGRATIONS=true` in the container to migrate on boot.
|
|
156
|
+
- `.github/workflows/ci.yaml`: on every PR and push to `main` runs typecheck, lint, migrations,
|
|
157
|
+
tests and build against a Postgres service.
|
|
158
|
+
- `.github/workflows/deploy.yaml`: manual deploy to AWS ECS (build → ECR → new task definition →
|
|
159
|
+
service update). Configure `secrets.AWS_ROLE_ARN` (OIDC) and `vars.AWS_REGION` in GitHub, and
|
|
160
|
+
adjust the resource names at the top of the file if they differ from `__SF_PROJECT_SLUG__`.
|
|
161
|
+
|
|
162
|
+
## Claude Code
|
|
163
|
+
|
|
164
|
+
`CLAUDE.md` and `.claude/agents/` describe the conventions above for AI-assisted development.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
|
|
4
|
+
# Build output
|
|
5
|
+
dist/
|
|
6
|
+
*.tsbuildinfo
|
|
7
|
+
|
|
8
|
+
# Environment (only .env.example is versioned)
|
|
9
|
+
.env
|
|
10
|
+
.env.*
|
|
11
|
+
!.env.example
|
|
12
|
+
|
|
13
|
+
# Tests
|
|
14
|
+
coverage/
|
|
15
|
+
src/routes/__tests__/_fixtures-*/
|
|
16
|
+
|
|
17
|
+
# IDE / OS
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
.DS_Store
|
|
21
|
+
|
|
22
|
+
# Logs
|
|
23
|
+
*.log
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__SF_PROJECT_SLUG__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__SF_PROJECT_NAME__ API - generated with Sundays Framework v__SF_CLI_VERSION__",
|
|
5
|
+
"private": true,
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start:dev": "tsx watch src/server.ts",
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"start": "node dist/server.js",
|
|
11
|
+
"typecheck": "tsc --noEmit -p tsconfig.spec.json",
|
|
12
|
+
"test": "jest",
|
|
13
|
+
"test:unit": "jest --testPathIgnorePatterns '/routes/' '/db/'",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"format": "prettier --write .",
|
|
17
|
+
"db:migrate": "tsx node_modules/knex/bin/cli.js migrate:latest",
|
|
18
|
+
"db:rollback": "tsx node_modules/knex/bin/cli.js migrate:rollback",
|
|
19
|
+
"db:status": "tsx node_modules/knex/bin/cli.js migrate:status",
|
|
20
|
+
"db:seed": "tsx node_modules/knex/bin/cli.js seed:run",
|
|
21
|
+
"db:make-migration": "tsx node_modules/knex/bin/cli.js migrate:make -x ts",
|
|
22
|
+
"db:make-seed": "tsx node_modules/knex/bin/cli.js seed:make -x ts"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"cors": "^2.8.5",
|
|
26
|
+
"dotenv": "^18.0.1",
|
|
27
|
+
"express": "^5.2.1",
|
|
28
|
+
"helmet": "^8.3.0",
|
|
29
|
+
"knex": "^3.3.0",
|
|
30
|
+
"pg": "^8.23.0",
|
|
31
|
+
"pino": "^10.3.1",
|
|
32
|
+
"pino-http": "^11.0.0",
|
|
33
|
+
"zod": "^4.6.5"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@eslint/js": "^10.0.0",
|
|
37
|
+
"@types/cors": "^2.8.19",
|
|
38
|
+
"@types/express": "^5.0.6",
|
|
39
|
+
"@types/jest": "^30.0.0",
|
|
40
|
+
"@types/node": "^24.0.0",
|
|
41
|
+
"@types/pg": "^8.15.0",
|
|
42
|
+
"@types/supertest": "^7.0.0",
|
|
43
|
+
"eslint": "^10.11.0",
|
|
44
|
+
"globals": "^16.0.0",
|
|
45
|
+
"jest": "^30.5.2",
|
|
46
|
+
"pino-pretty": "^13.1.3",
|
|
47
|
+
"prettier": "^3.9.8",
|
|
48
|
+
"supertest": "^7.2.2",
|
|
49
|
+
"ts-jest": "^29.4.12",
|
|
50
|
+
"tsx": "^4.23.15",
|
|
51
|
+
"typescript": "~5.9.3",
|
|
52
|
+
"typescript-eslint": "^8.70.0"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22"
|
|
56
|
+
},
|
|
57
|
+
"license": "MIT"
|
|
58
|
+
}
|