@sundaysf/cli-v2 0.0.2 → 0.0.4
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/dist/bin/index.js +1 -1
- package/dist/bin/index.js.map +1 -1
- package/dist/templates/backend/.claude/agents/knex-table-implementer.md +113 -0
- package/dist/templates/backend-db-sql/.claude/agents/knex-table-implementer.md +114 -0
- package/dist/templates/backend-db-sql/.claude/agents/sundays-backend-builder.md +70 -0
- package/dist/templates/backend-db-sql/.claude/settings.local.json +19 -0
- package/dist/templates/backend-db-sql/.env.example +13 -0
- package/dist/templates/backend-db-sql/.prettierignore +3 -0
- package/dist/templates/backend-db-sql/.prettierrc +9 -0
- package/dist/templates/backend-db-sql/CLAUDE.md +374 -0
- package/dist/templates/backend-db-sql/Dockerfile +17 -0
- package/dist/templates/backend-db-sql/README.md +34 -0
- package/dist/templates/backend-db-sql/db/knexfile.ts +33 -0
- package/dist/templates/backend-db-sql/db/migrations/.gitkeep +0 -0
- package/dist/templates/backend-db-sql/db/migrations/001_create_sundays_package_version.ts +13 -0
- package/dist/templates/backend-db-sql/db/seeds/001_sundays_package_version_seed.ts +11 -0
- package/dist/templates/backend-db-sql/db/src/KnexConnection.ts +74 -0
- package/dist/templates/backend-db-sql/db/src/d.types.ts +18 -0
- package/dist/templates/backend-db-sql/db/src/dao/sundays-package-version/sundays-package-version.dao.ts +71 -0
- package/dist/templates/backend-db-sql/db/src/index.ts +9 -0
- package/dist/templates/backend-db-sql/db/src/interfaces/sundays-package-version/sundays-package-version.interfaces.ts +6 -0
- package/dist/templates/backend-db-sql/db/tsconfig.json +16 -0
- package/dist/templates/backend-db-sql/eslint.config.js +20 -0
- package/dist/templates/backend-db-sql/src/app.ts +35 -0
- package/dist/templates/backend-db-sql/src/common/config/origins/origins.config.ts +11 -0
- package/dist/templates/backend-db-sql/src/common/utils/environment.resolver.ts +4 -0
- package/dist/templates/backend-db-sql/src/common/utils/version.resolver.ts +5 -0
- package/dist/templates/backend-db-sql/src/controllers/health/health.controller.ts +24 -0
- package/dist/templates/backend-db-sql/src/middlewares/error/error.middleware.ts +21 -0
- package/dist/templates/backend-db-sql/src/routes/health/health.router.ts +17 -0
- package/dist/templates/backend-db-sql/src/routes/index.ts +57 -0
- package/dist/templates/backend-db-sql/src/server.ts +18 -0
- package/dist/templates/backend-db-sql/src/types.d.ts +10 -0
- package/dist/templates/backend-db-sql/tsconfig.json +16 -0
- package/dist/templates/db-sql/.claude/agents/sundays-backend-builder.md +70 -0
- package/dist/templates/module/.claude/agents/knex-table-implementer.md +113 -0
- package/dist/templates/module/.claude/agents/sundays-backend-builder.md +70 -0
- package/dist/templates/module/.claude/settings.local.json +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### Development
|
|
8
|
+
|
|
9
|
+
- `npm run start:dev` - Start development server with auto-reload (uses nodemon)
|
|
10
|
+
- `npm run build` - Compile TypeScript to JavaScript (output to dist/)
|
|
11
|
+
- `npm start` - Run production server from compiled dist/
|
|
12
|
+
|
|
13
|
+
### Code Quality
|
|
14
|
+
|
|
15
|
+
- `npm run format` - Format code with Prettier
|
|
16
|
+
- No linting command configured - consider using ESLint with the installed configuration
|
|
17
|
+
|
|
18
|
+
### Testing
|
|
19
|
+
|
|
20
|
+
- `npm test` - Run tests with Jest (no test files found yet)
|
|
21
|
+
|
|
22
|
+
### Utilities
|
|
23
|
+
|
|
24
|
+
- `npm run create:controller` - Generate new controller using Sundays Framework CLI
|
|
25
|
+
|
|
26
|
+
### Database
|
|
27
|
+
|
|
28
|
+
- `npm run migrate:create` - Create a new migration file in TypeScript
|
|
29
|
+
- `npm run migrate:deploy` - Apply pending migrations
|
|
30
|
+
- `npm run seed:create` - Create a new seed file in TypeScript
|
|
31
|
+
- `npm run seed:run` - Execute seed files
|
|
32
|
+
- `npm run db:build` - Compile the db module TypeScript to JavaScript
|
|
33
|
+
- `npm run db:publish` - Publish the db module as an independent npm package
|
|
34
|
+
|
|
35
|
+
## Architecture
|
|
36
|
+
|
|
37
|
+
This is a Sundays Framework backend built with Express.js and TypeScript following a modular MVC pattern, with an embedded Knex.js database module in `db/`.
|
|
38
|
+
|
|
39
|
+
### Core Structure
|
|
40
|
+
|
|
41
|
+
- **Entry Points**: `src/server.ts` initializes the server and connects to the database, `src/app.ts` configures Express middleware
|
|
42
|
+
- **Routing**: Dynamic route loading system in `src/routes/index.ts` that automatically discovers and mounts routers from subdirectories
|
|
43
|
+
- **Controllers**: Business logic separated into controller classes (e.g., `HealthController`)
|
|
44
|
+
- **Middleware**: Error handling middleware in `src/middlewares/error/`
|
|
45
|
+
- **Configuration**: CORS origins managed in `src/common/config/origins/`
|
|
46
|
+
- **Database**: `db/` contains the Knex.js module with DAOs, migrations, seeds, and interfaces
|
|
47
|
+
|
|
48
|
+
### Key Patterns
|
|
49
|
+
|
|
50
|
+
1. **Router Auto-Discovery**: The `IndexRouter` class scans the routes directory and automatically mounts any `*.router.ts` files found in subdirectories. Routes are mounted at `/{folder-name}`.
|
|
51
|
+
|
|
52
|
+
2. **Controller Pattern**: Each router has a corresponding controller class that handles the business logic. Controllers are bound to router methods using `.bind()` to maintain proper context.
|
|
53
|
+
|
|
54
|
+
3. **Environment Configuration**: Uses dotenv for environment variables. Server port defaults to 3005 if not specified in .env.
|
|
55
|
+
|
|
56
|
+
4. **CORS Configuration**: Dynamic CORS origin configuration via `getAllowedOrigins()` function.
|
|
57
|
+
|
|
58
|
+
5. **Database Connection**: `src/server.ts` calls `KnexManager.connect()` before starting the Express server. The KnexManager singleton manages connection pooling.
|
|
59
|
+
|
|
60
|
+
6. **Dependencies**:
|
|
61
|
+
- Sundays Framework utilities (`@sundaysf/utils`) for pagination and validation
|
|
62
|
+
- Standard Express middleware (cors, morgan, etc.)
|
|
63
|
+
- Knex.js + PostgreSQL for database access
|
|
64
|
+
|
|
65
|
+
### Database Module (`db/`)
|
|
66
|
+
|
|
67
|
+
The `db/` directory is an independently publishable Knex.js module:
|
|
68
|
+
|
|
69
|
+
- **KnexManager** (`db/src/KnexConnection.ts`): Singleton for managing database connections with pooling and SSL support
|
|
70
|
+
- **DAO Pattern**: Data Access Objects implement `IBaseDAO<T>` with CRUD + pagination
|
|
71
|
+
- **Migrations**: `db/migrations/` - Knex migration files in TypeScript
|
|
72
|
+
- **Seeds**: `db/seeds/` - Seed data files
|
|
73
|
+
- **Interfaces**: `db/src/interfaces/` - Entity type definitions
|
|
74
|
+
- **Configuration**: `db/knexfile.ts` - Knex configuration for all environments
|
|
75
|
+
|
|
76
|
+
#### File Structure Conventions
|
|
77
|
+
|
|
78
|
+
**Interfaces**: `db/src/interfaces/{entity}/{entity}.interfaces.ts`
|
|
79
|
+
**DAOs**: `db/src/dao/{entity}/{entity}.dao.ts`
|
|
80
|
+
|
|
81
|
+
#### Environment Variables (Database)
|
|
82
|
+
|
|
83
|
+
- `SQL_HOST` - PostgreSQL host
|
|
84
|
+
- `SQL_PORT` - Database port (defaults to 5432)
|
|
85
|
+
- `SQL_USER` - Database user
|
|
86
|
+
- `SQL_PASSWORD` - Database password
|
|
87
|
+
- `SQL_DB_NAME` - Database name
|
|
88
|
+
- `SQL_REJECT_UNAUTHORIZED` - Set to 'false' to disable SSL certificate verification
|
|
89
|
+
|
|
90
|
+
### TypeScript Configuration
|
|
91
|
+
|
|
92
|
+
- **Backend** (`tsconfig.json`): Strict mode, ES2022, CommonJS. Source in `./src`, output to `./dist`. Excludes `db/`.
|
|
93
|
+
- **Database** (`db/tsconfig.json`): Strict mode, ES2022, CommonJS with declarations. Source in `db/src`, output to `db/dist`.
|
|
94
|
+
|
|
95
|
+
## Controller Implementation Guide
|
|
96
|
+
|
|
97
|
+
When creating new controllers, follow the established pattern demonstrated in the CompanyController:
|
|
98
|
+
|
|
99
|
+
### 1. Controller Structure
|
|
100
|
+
```typescript
|
|
101
|
+
import { Request, Response, NextFunction } from "express";
|
|
102
|
+
import { IBaseController } from "../../types";
|
|
103
|
+
import { inputValidator, IInputValidator, paginationHelper } from "@sundaysf/utils";
|
|
104
|
+
import { [Entity]DAO, I[Entity], IDataPaginator } from "../../db/src";
|
|
105
|
+
import { [Entity]CreateInputDTO } from "../../dto/input/[entity]/[entity].create.dto";
|
|
106
|
+
import { [Entity]UpdateInputDTO } from "../../dto/input/[entity]/[entity].update.dto";
|
|
107
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
108
|
+
|
|
109
|
+
export class [Entity]Controller implements IBaseController {
|
|
110
|
+
private _[entity]DAO: [Entity]DAO = new [Entity]DAO();
|
|
111
|
+
|
|
112
|
+
// Implement CRUD methods
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. API Response Format
|
|
117
|
+
|
|
118
|
+
All API responses must follow a consistent format:
|
|
119
|
+
|
|
120
|
+
**Success Response**:
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"success": true,
|
|
124
|
+
"data": {...} // or "message": "Action completed successfully" for operations without data
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Error Response**:
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"success": false,
|
|
132
|
+
"message": "Error description"
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Paginated Response** (from IDataPaginator):
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"success": true,
|
|
140
|
+
"data": [...],
|
|
141
|
+
"page": 1,
|
|
142
|
+
"limit": 10,
|
|
143
|
+
"count": 10,
|
|
144
|
+
"totalCount": 100,
|
|
145
|
+
"totalPages": 10
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Note: The `IDataPaginator` interface already includes the standard response format with `success` and `data` fields, so responses from paginated endpoints should be returned directly without additional wrapping.
|
|
150
|
+
|
|
151
|
+
### 3. Standard CRUD Methods
|
|
152
|
+
|
|
153
|
+
**getAll** - List with pagination:
|
|
154
|
+
```typescript
|
|
155
|
+
public async getAll(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
const {page, limit} = paginationHelper(req);
|
|
158
|
+
const result: IDataPaginator<I[Entity]> = await this._[entity]DAO.getAll(page, limit);
|
|
159
|
+
res.status(200).json(result); // IDataPaginator already includes success and data fields
|
|
160
|
+
} catch (err: any) {
|
|
161
|
+
next(err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**getByUuid** - Get single resource:
|
|
167
|
+
```typescript
|
|
168
|
+
public async getByUuid(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
169
|
+
try {
|
|
170
|
+
const { uuid } = req.params;
|
|
171
|
+
const result = await this._[entity]DAO.getByUuid(uuid);
|
|
172
|
+
if (!result) {
|
|
173
|
+
res.status(404).json({ success: false, message: "[Entity] not found" });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
res.status(200).json({
|
|
177
|
+
success: true,
|
|
178
|
+
data: result
|
|
179
|
+
});
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
next(err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**create** - Create new resource with DTO validation:
|
|
187
|
+
```typescript
|
|
188
|
+
public async create(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
const data = req.body;
|
|
191
|
+
const inputDTO = new [Entity]CreateInputDTO(data).build();
|
|
192
|
+
const validation: IInputValidator = await inputValidator(inputDTO);
|
|
193
|
+
if (!validation.success) {
|
|
194
|
+
req.statusCode = 400;
|
|
195
|
+
return next(new Error(validation.message));
|
|
196
|
+
}
|
|
197
|
+
const dataToCreate = {...inputDTO, uuid: uuidv4()};
|
|
198
|
+
const result = await this._[entity]DAO.create(dataToCreate);
|
|
199
|
+
res.status(201).json({
|
|
200
|
+
success: true,
|
|
201
|
+
data: result
|
|
202
|
+
});
|
|
203
|
+
} catch (err: any) {
|
|
204
|
+
next(err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**update** - Update existing resource with DTO validation:
|
|
210
|
+
```typescript
|
|
211
|
+
public async update(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
212
|
+
try {
|
|
213
|
+
const { uuid } = req.params;
|
|
214
|
+
const data = req.body;
|
|
215
|
+
|
|
216
|
+
// First get the entity by UUID to find its ID
|
|
217
|
+
const existing = await this._[entity]DAO.getByUuid(uuid);
|
|
218
|
+
if (!existing || !existing.id) {
|
|
219
|
+
res.status(404).json({ success: false, message: "[Entity] not found" });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const inputDTO = new [Entity]UpdateInputDTO(data).build();
|
|
224
|
+
const validation: IInputValidator = await inputValidator(inputDTO);
|
|
225
|
+
if (!validation.success) {
|
|
226
|
+
req.statusCode = 400;
|
|
227
|
+
return next(new Error(validation.message));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const result = await this._[entity]DAO.update(existing.id, inputDTO);
|
|
231
|
+
res.status(200).json({
|
|
232
|
+
success: true,
|
|
233
|
+
data: result
|
|
234
|
+
});
|
|
235
|
+
} catch (err: any) {
|
|
236
|
+
next(err);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**delete** - Delete resource:
|
|
242
|
+
```typescript
|
|
243
|
+
public async delete(req: Request, res: Response, next: NextFunction): Promise<void> {
|
|
244
|
+
try {
|
|
245
|
+
const { uuid } = req.params;
|
|
246
|
+
|
|
247
|
+
// First get the entity by UUID to find its ID
|
|
248
|
+
const existing = await this._[entity]DAO.getByUuid(uuid);
|
|
249
|
+
if (!existing || !existing.id) {
|
|
250
|
+
res.status(404).json({ success: false, message: "[Entity] not found" });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = await this._[entity]DAO.delete(existing.id);
|
|
255
|
+
if (result) {
|
|
256
|
+
res.status(200).json({ success: true, message: "[Entity] deleted successfully" });
|
|
257
|
+
} else {
|
|
258
|
+
res.status(404).json({ success: false, message: "Failed to delete [entity]" });
|
|
259
|
+
}
|
|
260
|
+
} catch (err: any) {
|
|
261
|
+
next(err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 4. Router Implementation
|
|
267
|
+
Create a corresponding router in `src/routes/[entity]/[entity].router.ts`:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { Router } from "express";
|
|
271
|
+
import { [Entity]Controller } from "../../controllers/[entity]/[entity].controller";
|
|
272
|
+
|
|
273
|
+
export class [Entity]Router {
|
|
274
|
+
private _router: Router;
|
|
275
|
+
private _[entity]Controller = new [Entity]Controller();
|
|
276
|
+
|
|
277
|
+
constructor() {
|
|
278
|
+
this._router = Router();
|
|
279
|
+
this.initRoutes();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private initRoutes(): void {
|
|
283
|
+
this._router.get("/", this._[entity]Controller.getAll.bind(this._[entity]Controller));
|
|
284
|
+
this._router.get("/:uuid", this._[entity]Controller.getByUuid.bind(this._[entity]Controller));
|
|
285
|
+
this._router.post("/", this._[entity]Controller.create.bind(this._[entity]Controller));
|
|
286
|
+
this._router.put("/:uuid", this._[entity]Controller.update.bind(this._[entity]Controller));
|
|
287
|
+
this._router.delete("/:uuid", this._[entity]Controller.delete.bind(this._[entity]Controller));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
public get router(): Router {
|
|
291
|
+
return this._router;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 5. DTO Implementation
|
|
297
|
+
|
|
298
|
+
Create DTOs to validate and sanitize input data:
|
|
299
|
+
|
|
300
|
+
**Create DTO** (`src/dto/input/[entity]/[entity].create.dto.ts`):
|
|
301
|
+
```typescript
|
|
302
|
+
export class [Entity]CreateInputDTO {
|
|
303
|
+
// Define only allowed properties
|
|
304
|
+
property1: type;
|
|
305
|
+
property2: type;
|
|
306
|
+
|
|
307
|
+
constructor(data: any){
|
|
308
|
+
this.property1 = data.property1;
|
|
309
|
+
this.property2 = data.property2;
|
|
310
|
+
// Set defaults for optional properties
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public build(): this {
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**Update DTO** (`src/dto/input/[entity]/[entity].update.dto.ts`):
|
|
320
|
+
```typescript
|
|
321
|
+
export class [Entity]UpdateInputDTO {
|
|
322
|
+
// Define optional properties
|
|
323
|
+
property1?: type;
|
|
324
|
+
property2?: type;
|
|
325
|
+
|
|
326
|
+
constructor(data: any){
|
|
327
|
+
// Only set properties that are present in the input
|
|
328
|
+
if (data.property1 !== undefined) this.property1 = data.property1;
|
|
329
|
+
if (data.property2 !== undefined) this.property2 = data.property2;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public build(): this {
|
|
333
|
+
// Remove any properties that weren't set
|
|
334
|
+
const cleanData: any = {};
|
|
335
|
+
if (this.property1 !== undefined) cleanData.property1 = this.property1;
|
|
336
|
+
if (this.property2 !== undefined) cleanData.property2 = this.property2;
|
|
337
|
+
|
|
338
|
+
// Clear all properties and reassign only the allowed ones
|
|
339
|
+
Object.keys(this).forEach(key => delete (this as any)[key]);
|
|
340
|
+
Object.assign(this, cleanData);
|
|
341
|
+
|
|
342
|
+
return this;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 6. Working with Related Entities
|
|
348
|
+
|
|
349
|
+
When your entity has foreign key relationships, DAOs can include related entities using PostgreSQL's `to_jsonb()` function:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
async getById(id: number): Promise<IEntity | null> {
|
|
353
|
+
const result = await this._knex("entity as e")
|
|
354
|
+
.leftJoin("related as r", "e.relatedId", "r.id")
|
|
355
|
+
.select("e.*", this._knex.raw("to_jsonb(r.*) as related"))
|
|
356
|
+
.where("e.id", id)
|
|
357
|
+
.first();
|
|
358
|
+
return result || null;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### 7. Important Notes
|
|
363
|
+
- Always use `.bind()` when assigning controller methods to router to maintain proper context
|
|
364
|
+
- Use UUID for public-facing endpoints (params) but convert to ID for internal DAO operations
|
|
365
|
+
- Return appropriate HTTP status codes (200, 201, 404, etc.)
|
|
366
|
+
- Pass errors to the next() function for centralized error handling
|
|
367
|
+
- Use paginationHelper from @sundaysf/utils for consistent pagination
|
|
368
|
+
- Use DTOs to validate and sanitize input data, preventing unwanted fields from being processed
|
|
369
|
+
- DTOs ensure only allowed fields are passed to the DAO layer
|
|
370
|
+
- Update DTOs should handle partial updates properly
|
|
371
|
+
- Always generate UUID in the controller for new resources (not in DTO or client-side)
|
|
372
|
+
- All API responses must follow the standard format: `{success: boolean, data?: any, message?: string}`
|
|
373
|
+
- Use status 200 for successful DELETE operations (not 204) to include success message
|
|
374
|
+
- Import DAOs and interfaces from `../db/src` (relative path from controllers)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
FROM node:22-alpine AS builder
|
|
2
|
+
WORKDIR /var/api
|
|
3
|
+
COPY package*.json ./
|
|
4
|
+
RUN npm ci
|
|
5
|
+
COPY . .
|
|
6
|
+
RUN npm run build
|
|
7
|
+
RUN npm run db:build
|
|
8
|
+
|
|
9
|
+
FROM node:22-alpine
|
|
10
|
+
WORKDIR /var/api
|
|
11
|
+
COPY package*.json ./
|
|
12
|
+
RUN npm ci --omit=dev
|
|
13
|
+
COPY --from=builder /var/api/dist ./dist
|
|
14
|
+
COPY --from=builder /var/api/db/dist ./db/dist
|
|
15
|
+
COPY --from=builder /var/api/db/package.json ./db/package.json
|
|
16
|
+
EXPOSE 3098
|
|
17
|
+
CMD ["node", "dist/server.js"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Sundays Framework Project
|
|
2
|
+
|
|
3
|
+
This directory contains the starter backend with embedded database module generated by the CLI.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
1. Install dependencies:
|
|
8
|
+
```
|
|
9
|
+
npm install
|
|
10
|
+
```
|
|
11
|
+
2. Copy `.env.example` to `.env` and set your environment variables (especially the `SQL_*` database variables).
|
|
12
|
+
3. Run database migrations:
|
|
13
|
+
```
|
|
14
|
+
npm run migrate:deploy
|
|
15
|
+
```
|
|
16
|
+
4. Start the development server:
|
|
17
|
+
```
|
|
18
|
+
npm run start:dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The server will run on the port specified in your `.env` file.
|
|
22
|
+
|
|
23
|
+
## Database module
|
|
24
|
+
|
|
25
|
+
The `db/` directory contains an independently publishable Knex.js database module. It can be used as part of this backend or published separately as an npm package.
|
|
26
|
+
|
|
27
|
+
### Database commands
|
|
28
|
+
|
|
29
|
+
- `npm run migrate:create` - Create a new migration
|
|
30
|
+
- `npm run migrate:deploy` - Run pending migrations
|
|
31
|
+
- `npm run seed:create` - Create a new seed file
|
|
32
|
+
- `npm run seed:run` - Run seed files
|
|
33
|
+
- `npm run db:build` - Compile the database module
|
|
34
|
+
- `npm run db:publish` - Publish the database module to npm
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
const isLocalhost = process.env.SQL_HOST === 'localhost' || process.env.SQL_HOST === '127.0.0.1';
|
|
6
|
+
const rejectUnauthorized = process.env.SQL_REJECT_UNAUTHORIZED !== 'false';
|
|
7
|
+
|
|
8
|
+
const sharedConfig: Knex.Config = {
|
|
9
|
+
client: "postgresql",
|
|
10
|
+
connection: {
|
|
11
|
+
database: process.env.SQL_DB_NAME,
|
|
12
|
+
user: process.env.SQL_USER,
|
|
13
|
+
password: process.env.SQL_PASSWORD,
|
|
14
|
+
host: process.env.SQL_HOST,
|
|
15
|
+
port: process.env.SQL_PORT ? +process.env.SQL_PORT : 5432,
|
|
16
|
+
ssl: isLocalhost ? false : { rejectUnauthorized },
|
|
17
|
+
},
|
|
18
|
+
pool: {
|
|
19
|
+
min: 2,
|
|
20
|
+
max: 10,
|
|
21
|
+
},
|
|
22
|
+
migrations: {
|
|
23
|
+
tableName: "knex_migrations",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const config: { [key: string]: Knex.Config } = {
|
|
28
|
+
development: sharedConfig,
|
|
29
|
+
staging: sharedConfig,
|
|
30
|
+
production: sharedConfig,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default config;
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function up(knex: Knex): Promise<void> {
|
|
4
|
+
await knex.schema.createTable("sundays_package_version", (table) => {
|
|
5
|
+
table.increments("id").primary();
|
|
6
|
+
table.string("versionName").notNullable();
|
|
7
|
+
table.timestamps(true, true); // created_at, updated_at
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function down(knex: Knex): Promise<void> {
|
|
12
|
+
await knex.schema.dropTableIfExists("sundays_package_version");
|
|
13
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Knex } from "knex";
|
|
2
|
+
|
|
3
|
+
export async function seed(knex: Knex): Promise<void> {
|
|
4
|
+
// Deletes ALL existing entries
|
|
5
|
+
await knex("sundays_package_version").del();
|
|
6
|
+
|
|
7
|
+
// Inserts seed entries
|
|
8
|
+
await knex("sundays_package_version").insert([
|
|
9
|
+
{ versionName: "1.0.0" }
|
|
10
|
+
]);
|
|
11
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { knex, Knex } from 'knex';
|
|
2
|
+
|
|
3
|
+
class KnexManager {
|
|
4
|
+
private static knexInstance: Knex<any, unknown[]> | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Open a new connection. Reuse the already existing one if there's any.
|
|
8
|
+
*/
|
|
9
|
+
static async connect(
|
|
10
|
+
config?: Knex.Config,
|
|
11
|
+
connections?: number
|
|
12
|
+
): Promise<Knex<any, unknown[]>> {
|
|
13
|
+
if (!KnexManager.knexInstance) {
|
|
14
|
+
const isLocalhost = process.env.SQL_HOST === 'localhost' || process.env.SQL_HOST === '127.0.0.1';
|
|
15
|
+
const rejectUnauthorized = process.env.SQL_REJECT_UNAUTHORIZED !== 'false';
|
|
16
|
+
const defaultConfig = {
|
|
17
|
+
client: 'pg',
|
|
18
|
+
connection: {
|
|
19
|
+
host: process.env.SQL_HOST,
|
|
20
|
+
user: process.env.SQL_USER,
|
|
21
|
+
password: process.env.SQL_PASSWORD,
|
|
22
|
+
database: process.env.SQL_DB_NAME,
|
|
23
|
+
port: Number(process.env.SQL_PORT) || 5432,
|
|
24
|
+
ssl: isLocalhost ? false : { rejectUnauthorized },
|
|
25
|
+
},
|
|
26
|
+
pool: {
|
|
27
|
+
min: 1,
|
|
28
|
+
max: connections || 15,
|
|
29
|
+
idleTimeoutMillis: 20000,
|
|
30
|
+
acquireTimeoutMillis: 30000,
|
|
31
|
+
},
|
|
32
|
+
migrations: {
|
|
33
|
+
tableName: 'knex_migrations',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
KnexManager.knexInstance = knex(config || defaultConfig);
|
|
37
|
+
try {
|
|
38
|
+
await KnexManager.knexInstance.raw('SELECT 1');
|
|
39
|
+
console.info(`Knex connection established`);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Failed to establish Knex connection:`, error);
|
|
42
|
+
KnexManager.knexInstance = null;
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return KnexManager.knexInstance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns the active connection.
|
|
52
|
+
*/
|
|
53
|
+
static getConnection(): Knex<any, unknown[]> {
|
|
54
|
+
if (!KnexManager.knexInstance) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'Knex connection has not been established. Call connect() first.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return KnexManager.knexInstance;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Closes the connection and destroys the instance.
|
|
64
|
+
*/
|
|
65
|
+
static async disconnect(): Promise<void> {
|
|
66
|
+
if (KnexManager.knexInstance) {
|
|
67
|
+
await KnexManager.knexInstance.destroy();
|
|
68
|
+
KnexManager.knexInstance = null;
|
|
69
|
+
console.info(`Knex connection closed`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default KnexManager;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface IBaseDAO<T> {
|
|
2
|
+
create(item: T): Promise<T>;
|
|
3
|
+
getById(id: number): Promise<T | null>;
|
|
4
|
+
getByUuid(uuid: string): Promise<T | null>;
|
|
5
|
+
update(id: number, item: Partial<T>): Promise<T | null>;
|
|
6
|
+
delete(id: number): Promise<boolean>;
|
|
7
|
+
getAll(page: number, limit: number): Promise<IDataPaginator<T>>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IDataPaginator<T> {
|
|
11
|
+
success: boolean;
|
|
12
|
+
data: T[];
|
|
13
|
+
page: number;
|
|
14
|
+
limit: number;
|
|
15
|
+
count: number;
|
|
16
|
+
totalCount: number;
|
|
17
|
+
totalPages: number;
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Knex } from "knex";
|
|
2
|
+
import { IBaseDAO, IDataPaginator } from "../../d.types";
|
|
3
|
+
import { ISundaysPackageVersion } from "../../interfaces/sundays-package-version/sundays-package-version.interfaces";
|
|
4
|
+
import KnexManager from "../../KnexConnection";
|
|
5
|
+
|
|
6
|
+
export class SundaysPackageVersionDAO implements IBaseDAO<ISundaysPackageVersion> {
|
|
7
|
+
private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
|
|
8
|
+
|
|
9
|
+
async create(item: ISundaysPackageVersion): Promise<ISundaysPackageVersion> {
|
|
10
|
+
const [created] = await this._knex("sundays_package_version").insert(item).returning("*");
|
|
11
|
+
return created;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async getById(id: number): Promise<ISundaysPackageVersion | null> {
|
|
15
|
+
const result = await this._knex("sundays_package_version")
|
|
16
|
+
.select("*")
|
|
17
|
+
.where("id", id)
|
|
18
|
+
.first();
|
|
19
|
+
return result || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getByUuid(uuid: string): Promise<ISundaysPackageVersion | null> {
|
|
23
|
+
const result = await this._knex("sundays_package_version")
|
|
24
|
+
.select("*")
|
|
25
|
+
.where("uuid", uuid)
|
|
26
|
+
.first();
|
|
27
|
+
return result || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async update(id: number, item: Partial<ISundaysPackageVersion>): Promise<ISundaysPackageVersion | null> {
|
|
31
|
+
const [updated] = await this._knex("sundays_package_version")
|
|
32
|
+
.where({ id })
|
|
33
|
+
.update(item)
|
|
34
|
+
.returning("*");
|
|
35
|
+
return updated || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async delete(id: number): Promise<boolean> {
|
|
39
|
+
const result = await this._knex("sundays_package_version").where({ id }).del();
|
|
40
|
+
return result > 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getAll(page: number, limit: number): Promise<IDataPaginator<ISundaysPackageVersion>> {
|
|
44
|
+
const safeLimit = Math.max(limit, 1);
|
|
45
|
+
const offset = (page - 1) * safeLimit;
|
|
46
|
+
|
|
47
|
+
const query = this._knex("sundays_package_version").select("*");
|
|
48
|
+
|
|
49
|
+
const [countResult] = await query.clone().clearSelect().count("* as count");
|
|
50
|
+
const totalCount = +countResult.count;
|
|
51
|
+
const data = await query.clone().limit(safeLimit).offset(offset).orderBy("id", "desc");
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
data,
|
|
56
|
+
page,
|
|
57
|
+
limit: safeLimit,
|
|
58
|
+
count: data.length,
|
|
59
|
+
totalCount,
|
|
60
|
+
totalPages: Math.ceil(totalCount / safeLimit),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getLatestVersion(): Promise<ISundaysPackageVersion | null> {
|
|
65
|
+
const result = await this._knex("sundays_package_version")
|
|
66
|
+
.select("*")
|
|
67
|
+
.orderBy("id", "desc")
|
|
68
|
+
.first();
|
|
69
|
+
return result || null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// DAOs
|
|
2
|
+
export { SundaysPackageVersionDAO } from "./dao/sundays-package-version/sundays-package-version.dao";
|
|
3
|
+
|
|
4
|
+
// Interfaces
|
|
5
|
+
export { IDataPaginator } from "./d.types";
|
|
6
|
+
export { ISundaysPackageVersion } from "./interfaces/sundays-package-version/sundays-package-version.interfaces";
|
|
7
|
+
|
|
8
|
+
import KnexManager from './KnexConnection';
|
|
9
|
+
export { KnexManager };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "CommonJS",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"sourceMap": true,
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|