crudora 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +554 -328
  3. package/dist/cli.js +72 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/core/crudora.d.ts +34 -9
  6. package/dist/core/crudora.d.ts.map +1 -1
  7. package/dist/core/crudora.js +254 -105
  8. package/dist/core/crudora.js.map +1 -1
  9. package/dist/core/crudoraServer.d.ts +64 -10
  10. package/dist/core/crudoraServer.d.ts.map +1 -1
  11. package/dist/core/crudoraServer.js +138 -19
  12. package/dist/core/crudoraServer.js.map +1 -1
  13. package/dist/core/drizzleTableBuilder.d.ts +6 -0
  14. package/dist/core/drizzleTableBuilder.d.ts.map +1 -0
  15. package/dist/core/drizzleTableBuilder.js +175 -0
  16. package/dist/core/drizzleTableBuilder.js.map +1 -0
  17. package/dist/core/model.d.ts +28 -9
  18. package/dist/core/model.d.ts.map +1 -1
  19. package/dist/core/model.js +33 -70
  20. package/dist/core/model.js.map +1 -1
  21. package/dist/core/repository.d.ts +98 -14
  22. package/dist/core/repository.d.ts.map +1 -1
  23. package/dist/core/repository.js +561 -103
  24. package/dist/core/repository.js.map +1 -1
  25. package/dist/core/schemaGenerator.d.ts +3 -3
  26. package/dist/core/schemaGenerator.d.ts.map +1 -1
  27. package/dist/core/schemaGenerator.js +237 -32
  28. package/dist/core/schemaGenerator.js.map +1 -1
  29. package/dist/decorators/model.d.ts +56 -1
  30. package/dist/decorators/model.d.ts.map +1 -1
  31. package/dist/decorators/model.js +92 -0
  32. package/dist/decorators/model.js.map +1 -1
  33. package/dist/index.d.ts +7 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +12 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/scripts/copy-assets.js +47 -47
  38. package/dist/scripts/postinstall.js +172 -136
  39. package/dist/templates/.env.example +13 -9
  40. package/dist/templates/drizzle.config.ts +10 -0
  41. package/dist/templates/schema.ts +23 -0
  42. package/dist/types/logger.type.d.ts +7 -0
  43. package/dist/types/logger.type.d.ts.map +1 -0
  44. package/dist/types/logger.type.js +3 -0
  45. package/dist/types/logger.type.js.map +1 -0
  46. package/dist/types/model.type.d.ts +30 -5
  47. package/dist/types/model.type.d.ts.map +1 -1
  48. package/dist/utils/validation.d.ts.map +1 -1
  49. package/dist/utils/validation.js +91 -19
  50. package/dist/utils/validation.js.map +1 -1
  51. package/package.json +108 -94
  52. package/scripts/copy-assets.js +47 -47
  53. package/scripts/postinstall.js +172 -136
  54. package/templates/.env.example +13 -9
  55. package/templates/drizzle.config.ts +10 -0
  56. package/templates/schema.ts +23 -0
  57. package/dist/templates/schema.prisma +0 -22
  58. package/templates/schema.prisma +0 -22
package/README.md CHANGED
@@ -1,328 +1,554 @@
1
- # Crudora
2
-
3
- Automatic CRUD API generator for TypeScript with Prisma - Build REST APIs in minutes, not hours.
4
-
5
- [![npm version](https://badge.fury.io/js/crudora.svg)](https://badge.fury.io/js/crudora)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
- [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
8
-
9
- ## Features
10
-
11
- - 🚀 **Zero Configuration**: Generate CRUD APIs instantly
12
- - 🎯 **Type-Safe**: Full TypeScript support with Prisma integration
13
- - 🔧 **Flexible**: Support for both decorator and inheritance patterns
14
- - 📊 **Auto Pagination**: Built-in pagination and filtering
15
- - 🛡️ **Validation**: Automatic request validation with Zod
16
- - 🔌 **Extensible**: Custom routes and middleware support
17
- - 📖 **Schema Generation**: Auto-generate Prisma schemas from models
18
- - 🗄️ **Repository Pattern**: Built-in repository for database operations
19
- - 🔄 **Lifecycle Hooks**: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeFind, afterFind
20
- - 🔒 **Field Security**: Hidden fields support with automatic filtering
21
- - 🎛️ **Dynamic Selection**: Smart field selection based on fillable and hidden properties
22
- - **Auto Setup**: Intelligent postinstall script for quick project initialization
23
- - 🔄 **TypeScript First**: Native TypeScript support with ESM modules
24
- - 🖥️ **CLI Tool**: Command-line interface for project initialization and scaffolding
25
-
26
- ## Installation
27
-
28
- ```bash
29
- npm install crudora prisma @prisma/client
30
- # or
31
- yarn add crudora prisma @prisma/client
32
- ```
33
-
34
- **Note**: After installation, Crudora automatically sets up your project with:
35
-
36
- - Prisma schema template
37
- - Environment configuration (.env)
38
- - Basic server setup (server.ts)
39
- - Useful npm scripts
40
-
41
- ## CLI Usage
42
-
43
- Crudora comes with a built-in CLI tool for quick project initialization and database management:
44
-
45
- ```bash
46
- # Initialize a new Crudora project in the current directory
47
- npx crudora init
48
-
49
- # Start Prisma Studio for database management
50
- npx crudora studio
51
-
52
- # Generate Prisma Client
53
- npx crudora generate
54
-
55
- # Push Prisma schema to database
56
- npx crudora push
57
-
58
- # Run Prisma migrations
59
- npx crudora migrate
60
- ```
61
-
62
- ### CLI Commands
63
-
64
- - `init` - Initialize a new Crudora project with necessary files (schema.prisma, .env, server.ts, tsconfig.json)
65
- - `studio` - Start Prisma Studio for visual database management
66
- - `generate` - Generate Prisma Client based on your schema
67
- - `push` - Push Prisma schema to your database without migrations
68
- - `migrate` - Run Prisma migrations for schema changes
69
-
70
- ## Quick Start
71
-
72
- ### Basic Usage
73
-
74
- ```typescript
75
- import { CrudoraServer, Model } from "crudora";
76
- import { PrismaClient } from "@prisma/client";
77
-
78
- const prisma = new PrismaClient();
79
-
80
- class User extends Model {
81
- static tableName = "users";
82
- static primaryKey = "id";
83
- static timestamps = true;
84
- static fillable = ["name", "email", "password"];
85
- static hidden = ["password"];
86
-
87
- // Lifecycle hooks
88
- static async beforeCreate(data: any): Promise<any> {
89
- data.password = await hashPassword(data.password);
90
- return data;
91
- }
92
-
93
- static async afterCreate(data: any, result: any): Promise<any> {
94
- console.log(`User created: ${result.email}`);
95
- return result;
96
- }
97
- }
98
-
99
- class Post extends Model {
100
- static tableName = "posts";
101
- static fillable = ["title", "content", "authorId"];
102
- static hidden = ["deletedAt"];
103
- }
104
-
105
- const server = new CrudoraServer({
106
- port: 3000,
107
- prisma: prisma,
108
- });
109
-
110
- server
111
- .registerModel(User, Post)
112
- .generateRoutes()
113
- .listen(() => {
114
- console.log("Server running on port 3000");
115
- });
116
- ```
117
-
118
- ## Generated API Endpoints
119
-
120
- For each registered model, Crudora automatically generates:
121
-
122
- - `GET /api/{tableName}` - List all records with pagination and filtering
123
- - `GET /api/{tableName}/:id` - Get record by ID
124
- - `POST /api/{tableName}` - Create new record
125
- - `PUT /api/{tableName}/:id` - Update record
126
- - `DELETE /api/{tableName}/:id` - Delete record
127
-
128
- ### Query Parameters
129
-
130
- - `skip` - Number of records to skip (pagination)
131
- - `take` - Number of records to take (pagination)
132
- - `orderBy` - Sort order (e.g., `createdAt:desc`)
133
- - `where` - Filter conditions
134
-
135
- ## Advanced Usage
136
-
137
- ### Lifecycle Hooks
138
-
139
- Crudora supports comprehensive lifecycle hooks for all CRUD operations:
140
-
141
- ```typescript
142
- class User extends Model {
143
- static tableName = "users";
144
- static fillable = ["name", "email", "password"];
145
- static hidden = ["password"];
146
-
147
- // Create hooks
148
- static async beforeCreate(data: any): Promise<any> {
149
- data.password = await hashPassword(data.password);
150
- data.createdAt = new Date();
151
- return data;
152
- }
153
-
154
- static async afterCreate(data: any, result: any): Promise<any> {
155
- await sendWelcomeEmail(result.email);
156
- await logUserCreation(result.id);
157
- return result;
158
- }
159
-
160
- // Update hooks
161
- static async beforeUpdate(id: string, data: any): Promise<any> {
162
- data.updatedAt = new Date();
163
- return data;
164
- }
165
-
166
- static async afterUpdate(id: string, data: any, result: any): Promise<any> {
167
- await logUserUpdate(id, data);
168
- return result;
169
- }
170
-
171
- // Delete hooks
172
- static async beforeDelete(id: string): Promise<void> {
173
- await archiveUserData(id);
174
- }
175
-
176
- static async afterDelete(id: string, result: any): Promise<any> {
177
- await logUserDeletion(id);
178
- return result;
179
- }
180
-
181
- // Find hooks
182
- static async beforeFind(options: any): Promise<any> {
183
- // Add default filters
184
- options.where = { ...options.where, active: true };
185
- return options;
186
- }
187
-
188
- static async afterFind(result: any): Promise<any> {
189
- // Transform result
190
- if (Array.isArray(result)) {
191
- return result.map((user) => ({ ...user, displayName: user.name }));
192
- }
193
- return { ...result, displayName: result.name };
194
- }
195
- }
196
- ```
197
-
198
- ### Field Security and Dynamic Selection
199
-
200
- Crudora automatically handles field security and dynamic selection:
201
-
202
- ```typescript
203
- class User extends Model {
204
- static tableName = "users";
205
- static fillable = ["name", "email", "bio"]; // Only these fields can be mass-assigned
206
- static hidden = ["password", "secret"]; // These fields are automatically excluded from responses
207
- }
208
-
209
- // API responses automatically exclude hidden fields
210
- // Only fillable fields are included in select queries for better performance
211
- ```
212
-
213
- ### Using Repositories
214
-
215
- ```typescript
216
- const crudora = server.getCrudora();
217
- const userRepo = crudora.getRepository(User);
218
-
219
- // Create user (triggers beforeCreate and afterCreate hooks)
220
- const user = await userRepo.create({
221
- name: "John Doe",
222
- email: "john@example.com",
223
- password: "plaintext", // Will be hashed by beforeCreate hook
224
- });
225
- // Response excludes password due to hidden field
226
-
227
- // Find users (triggers beforeFind and afterFind hooks)
228
- const users = await userRepo.findAll({
229
- skip: 0,
230
- take: 10,
231
- where: { active: true },
232
- orderBy: { createdAt: "desc" },
233
- });
234
-
235
- // Update user (triggers beforeUpdate and afterUpdate hooks)
236
- const updatedUser = await userRepo.update("user-id", {
237
- name: "John Updated",
238
- });
239
-
240
- // Count users
241
- const count = await userRepo.count({ active: true });
242
- ```
243
-
244
- ### Custom Routes
245
-
246
- ```typescript
247
- server
248
- .get("/health", (req, res) => {
249
- res.json({ status: "ok", timestamp: new Date() });
250
- })
251
- .post("/users/:id/activate", async (req, res) => {
252
- const userRepo = server.getCrudora().getRepository(User);
253
- const user = await userRepo.update(req.params.id, { active: true });
254
- res.json(user);
255
- });
256
- ```
257
-
258
- ### Schema Generation
259
-
260
- ```typescript
261
- // Generate Prisma schema from your models
262
- const schema = server.getCrudora().generatePrismaSchema("postgresql");
263
- console.log(schema);
264
- ```
265
-
266
- ### Validation
267
-
268
- Crudora automatically generates Zod validation schemas:
269
-
270
- ```typescript
271
- const crudora = server.getCrudora();
272
-
273
- // Get partial validation schema (for updates)
274
- const partialSchema = crudora.getValidationSchema(User);
275
-
276
- // Get strict validation schema (for creation)
277
- const strictSchema = crudora.getStrictValidationSchema(User);
278
- ```
279
-
280
- ## Project Setup
281
-
282
- After installing Crudora, run these commands to complete setup:
283
-
284
- ```bash
285
- # Install additional dependencies
286
- npm install @prisma/client prisma dotenv
287
-
288
- # Generate Prisma client
289
- npm run db:generate
290
-
291
- # Push database schema
292
- npm run db:push
293
-
294
- # Start development server
295
- npm run dev
296
- ```
297
-
298
- ## Available Scripts
299
-
300
- Crudora automatically adds these scripts to your package.json:
301
-
302
- - `npm run dev` - Start development server with ts-node
303
- - `npm run build` - Build TypeScript to JavaScript
304
- - `npm run start` - Start production server from built JavaScript
305
- - `npm run start:prod` - Build and start production server
306
- - `npm run db:generate` - Generate Prisma client
307
- - `npm run db:push` - Push schema to database
308
- - `npm run db:migrate` - Run database migrations
309
- - `npm run db:studio` - Open Prisma Studio
310
-
311
- ## Documentation
312
-
313
- - [API Reference](./docs/api.md)
314
- - [Model Definition Guide](./docs/models.md)
315
- - [Custom Routes](./docs/custom-routes.md)
316
- - [Deployment Guide](./docs/deployment.md)
317
-
318
- ## Contributing
319
-
320
- We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
321
-
322
- ## License
323
-
324
- MIT © [Crudora](https://github.com/suryamsj/crudora)
325
-
326
- ---
327
-
328
- **⚠️ Alpha Version - Not recommended for production use**
1
+ # Crudora
2
+
3
+ Automatic CRUD API generator for TypeScript with Drizzle ORM build REST APIs in minutes, not hours.
4
+
5
+ [![npm version](https://badge.fury.io/js/crudora.svg)](https://badge.fury.io/js/crudora)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
8
+
9
+ ## Features
10
+
11
+ - **Zero Configuration** generate CRUD APIs instantly from model classes
12
+ - **Drizzle ORM** type-safe queries with Drizzle under the hood
13
+ - **Multiple Schema Support** `pgSchema` / `mysqlSchema` per model via `static schema`
14
+ - **`@Field()` Decorators** define columns with types, constraints, and Drizzle table auto-generation
15
+ - **Rich Field Types** `uuid`, `string`, `text`, `integer`, `number`, `boolean`, `date`, `decimal`, `json`, `enum`, `bigint`, `serial`, `array`
16
+ - **Advanced Filtering** equality, range (`_gt`, `_gte`, `_lt`, `_lte`), negation (`_ne`), LIKE (`_like`), and IN (`_in`) operators via query params
17
+ - **Offset & Cursor Pagination** built-in offset pagination and efficient cursor-based pagination
18
+ - **Zod Validation** automatic request validation with length limits and enum constraints
19
+ - **Soft Delete** built-in soft-delete support with `restore()` and `hardDelete()`
20
+ - **Relations** `@HasMany`, `@HasOne`, `@BelongsTo`, `@BelongsToMany` with batch loading
21
+ - **Transactions** `repository.transaction()` and `crudora.transaction()`
22
+ - **Lifecycle Hooks** `beforeCreate`, `afterCreate`, `afterCreateMany`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, `beforeFind`, `afterFind`
23
+ - **Structured Logging** pluggable `CrudoraLogger` with correlation IDs per request; compatible with pino, winston
24
+ - **Field Security** `hidden` fields stripped at query time via `getTableColumns()`
25
+ - **Standardized Responses** — all endpoints return `{ success, data, meta?, error? }` OpenAPI-style envelope
26
+ - **Schema Generator** — auto-generate Drizzle TypeScript schema files from models
27
+ - **TypeScript First** — full type safety, ESM and CJS dual build
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ npm install crudora drizzle-orm
33
+ # PostgreSQL
34
+ npm install pg
35
+ # or MySQL
36
+ npm install mysql2
37
+ ```
38
+
39
+ After installation, Crudora sets up your project with:
40
+ - `drizzle.config.ts` template
41
+ - `src/db/schema.ts` template
42
+ - Environment configuration (`.env`)
43
+ - Basic server setup (`src/server.ts`)
44
+
45
+ Add these scripts to your `package.json`:
46
+
47
+ ```json
48
+ {
49
+ "scripts": {
50
+ "dev": "ts-node src/server.ts",
51
+ "build": "tsc",
52
+ "db:generate": "drizzle-kit generate",
53
+ "db:push": "drizzle-kit push",
54
+ "db:migrate": "drizzle-kit migrate",
55
+ "db:studio": "drizzle-kit studio"
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## Quick Start
61
+
62
+ ```typescript
63
+ import { CrudoraServer, Model, Field } from 'crudora';
64
+ import { drizzle } from 'drizzle-orm/node-postgres';
65
+ import { Pool } from 'pg';
66
+ import 'dotenv/config';
67
+
68
+ const db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }));
69
+
70
+ class User extends Model {
71
+ static schema = 'auth'; // PostgreSQL schema (optional)
72
+ static tableName = 'users';
73
+ static hidden = ['password'];
74
+
75
+ @Field({ type: 'uuid', primary: true })
76
+ id!: string;
77
+
78
+ @Field({ type: 'string', required: true, unique: true, length: 255 })
79
+ email!: string;
80
+
81
+ @Field({ type: 'string', required: true })
82
+ password!: string;
83
+
84
+ static async beforeCreate(data: any) {
85
+ data.password = await hashPassword(data.password);
86
+ return data;
87
+ }
88
+
89
+ static async afterCreate(_data: any, result: any) {
90
+ await sendWelcomeEmail(result.email);
91
+ return result;
92
+ }
93
+ }
94
+
95
+ const server = new CrudoraServer({ db, dialect: 'postgresql', port: 3000 });
96
+
97
+ server
98
+ .registerModel(User)
99
+ .generateRoutes()
100
+ .listen();
101
+ ```
102
+
103
+ ## Generated API Endpoints
104
+
105
+ For each registered model, Crudora automatically generates:
106
+
107
+ | Method | Path | Description |
108
+ |---|---|---|
109
+ | `GET` | `/api/{tableName}` | List all with offset or cursor pagination |
110
+ | `GET` | `/api/{tableName}/:id` | Get by ID |
111
+ | `POST` | `/api/{tableName}` | Create — returns `201` |
112
+ | `PUT` | `/api/{tableName}/:id` | Full replace — all required fields must be provided |
113
+ | `PATCH` | `/api/{tableName}/:id` | Partial update — any subset of fields |
114
+ | `DELETE` | `/api/{tableName}/:id` | Delete — returns `204 No Content` |
115
+
116
+ ### Response Envelope
117
+
118
+ All endpoints return a consistent JSON envelope:
119
+
120
+ ```json
121
+ // Success (list)
122
+ {
123
+ "success": true,
124
+ "data": [{ "id": "uuid", "email": "john@example.com" }],
125
+ "meta": {
126
+ "pagination": { "page": 1, "limit": 10, "total": 42, "pages": 5 }
127
+ }
128
+ }
129
+
130
+ // Success (single)
131
+ { "success": true, "data": { "id": "uuid", "email": "john@example.com" } }
132
+
133
+ // Error
134
+ {
135
+ "success": false,
136
+ "error": {
137
+ "code": "NOT_FOUND",
138
+ "message": "Resource not found"
139
+ }
140
+ }
141
+
142
+ // Validation error
143
+ {
144
+ "success": false,
145
+ "error": {
146
+ "code": "VALIDATION_ERROR",
147
+ "message": "Validation failed",
148
+ "details": [{ "field": "email", "message": "Invalid email" }]
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Query Parameters (GET list)
154
+
155
+ | Param | Example | Description |
156
+ |---|---|---|
157
+ | `page` | `?page=2` | Page number (offset pagination, default: `1`) |
158
+ | `limit` | `?limit=20` | Records per page (default: `10`, max: `1000`) |
159
+ | `orderBy` | `?orderBy=name,createdAt` | Sort fields (comma-separated) |
160
+ | `order` | `?order=asc,desc` | Sort directions (comma-separated) |
161
+ | `cursor` | `?cursor=base64...` | Cursor for cursor-based pagination |
162
+ | `cursorField` | `?cursorField=createdAt` | Field to use as cursor (default: primary key) |
163
+ | `select` | `?select=id,name,email` | Return only specified fields |
164
+ | `with` | `?with=posts,profile` | Load relations (max 5) |
165
+ | `withDeleted` | `?withDeleted=true` | Include soft-deleted records |
166
+ | `{field}` | `?name=John` | Equality filter |
167
+ | `{field}_gt` | `?age_gt=18` | Greater than |
168
+ | `{field}_gte` | `?age_gte=18` | Greater than or equal |
169
+ | `{field}_lt` | `?createdAt_lt=2025-01-01` | Less than |
170
+ | `{field}_lte` | `?createdAt_lte=2025-01-01` | Less than or equal |
171
+ | `{field}_ne` | `?status_ne=deleted` | Not equal |
172
+ | `{field}_like` | `?name_like=john` | LIKE `%john%` (case-sensitive) |
173
+ | `{field}_in` | `?status_in=active,pending` | IN list (comma-separated) |
174
+
175
+ ## Multiple Schema Support
176
+
177
+ ```typescript
178
+ class User extends Model {
179
+ static schema = 'auth'; // → pgSchema('auth').table('users', ...)
180
+ static tableName = 'users';
181
+ }
182
+
183
+ class AuditLog extends Model {
184
+ static schema = 'audit'; // → pgSchema('audit').table('audit_logs', ...)
185
+ static tableName = 'audit_logs';
186
+ }
187
+
188
+ class Post extends Model {
189
+ // No schema → pgTable('posts', ...) (default public schema)
190
+ static tableName = 'posts';
191
+ }
192
+ ```
193
+
194
+ ## `@Field()` Decorator
195
+
196
+ Columns are defined with `@Field()`. Crudora reads this metadata to auto-build Drizzle table objects at registration time.
197
+
198
+ ```typescript
199
+ import { Model, Field } from 'crudora';
200
+
201
+ class Product extends Model {
202
+ static tableName = 'products';
203
+
204
+ @Field({ type: 'serial', primary: true })
205
+ id!: number;
206
+
207
+ @Field({ type: 'string', required: true, length: 200 })
208
+ name!: string;
209
+
210
+ @Field({ type: 'enum', enumValues: ['draft', 'published', 'archived'] })
211
+ status!: string;
212
+
213
+ @Field({ type: 'decimal', precision: 10, scale: 2, required: true })
214
+ price!: string;
215
+
216
+ @Field({ type: 'integer' })
217
+ stock!: number;
218
+
219
+ @Field({ type: 'boolean' })
220
+ isActive!: boolean;
221
+
222
+ @Field({ type: 'json' })
223
+ metadata!: object;
224
+
225
+ @Field({ type: 'array' })
226
+ tags!: string[];
227
+ }
228
+ ```
229
+
230
+ ### Supported Field Types
231
+
232
+ | `type` | PostgreSQL | MySQL | Notes |
233
+ |---|---|---|---|
234
+ | `uuid` | `uuid` | `varchar(36)` | |
235
+ | `string` | `varchar(length)` | `varchar(length)` | Zod enforces `max(length)` |
236
+ | `text` | `text` | `text` | |
237
+ | `integer` | `integer` | `int` | |
238
+ | `number` | `doublePrecision` | `double` | |
239
+ | `boolean` | `boolean` | `boolean` | |
240
+ | `date` | `timestamp` | `datetime` | |
241
+ | `decimal` | `decimal(p, s)` | `decimal(p, s)` | |
242
+ | `json` | `json` | `json` | |
243
+ | `enum` | `text` + Zod enum | `mysqlEnum` | Requires `enumValues` |
244
+ | `bigint` | `bigint` (mode: number) | `bigint` (mode: number) | |
245
+ | `serial` | `serial` (auto-increment) | `int().autoincrement()` | DB-managed, skip in API |
246
+ | `array` | `text[]` | — (use `json`) | PostgreSQL only |
247
+
248
+ > **Note on `enum` in PostgreSQL:** The column is stored as `text`. Enum values are enforced by Zod at the API layer. MySQL uses a native `ENUM` column.
249
+
250
+ ### Field Options
251
+
252
+ | Option | Type | Description |
253
+ |---|---|---|
254
+ | `type` | `FieldType` | Column type (required) |
255
+ | `primary` | `boolean` | Primary key — excluded from API validation |
256
+ | `required` | `boolean` | NOT NULL constraint + required in Zod |
257
+ | `nullable` | `boolean` | Column allows NULL; Zod accepts `null` values |
258
+ | `unique` | `boolean` | UNIQUE constraint |
259
+ | `length` | `number` | Max length for `string` — enforced by Zod |
260
+ | `precision` | `number` | Decimal precision (default: 10) |
261
+ | `scale` | `number` | Decimal scale (default: 2) |
262
+ | `default` | `any` | Column default value |
263
+ | `enumValues` | `string[]` | Required for `enum` type |
264
+
265
+ ## Soft Delete
266
+
267
+ ```typescript
268
+ class Post extends Model {
269
+ static tableName = 'posts';
270
+ static softDelete = true; // adds deletedAt column
271
+
272
+ @Field({ type: 'uuid', primary: true }) id!: string;
273
+ @Field({ type: 'string' }) title!: string;
274
+ }
275
+
276
+ // DELETE /api/posts/:id sets deletedAt (soft delete)
277
+ // GET /api/posts → excludes soft-deleted records by default
278
+ // GET /api/posts?withDeleted=true → includes soft-deleted records
279
+
280
+ const repo = crudora.getRepository(Post);
281
+ await repo.restore('uuid'); // restore a soft-deleted record
282
+ await repo.hardDelete('uuid'); // permanently delete
283
+ ```
284
+
285
+ ## Relations
286
+
287
+ ```typescript
288
+ import { HasMany, BelongsTo } from 'crudora';
289
+
290
+ class User extends Model {
291
+ static tableName = 'users';
292
+
293
+ @Field({ type: 'uuid', primary: true }) id!: string;
294
+ @Field({ type: 'string' }) name!: string;
295
+
296
+ @HasMany(() => Post, 'authorId')
297
+ posts?: Post[];
298
+ }
299
+
300
+ class Post extends Model {
301
+ static tableName = 'posts';
302
+
303
+ @Field({ type: 'uuid', primary: true }) id!: string;
304
+ @Field({ type: 'string' }) title!: string;
305
+ @Field({ type: 'uuid' }) authorId!: string;
306
+
307
+ @BelongsTo(() => User, 'authorId')
308
+ author?: User;
309
+ }
310
+
311
+ // Load with relations
312
+ // GET /api/users?with=posts
313
+ // GET /api/posts?with=author
314
+
315
+ // Or via repository
316
+ const users = await userRepo.findAll({ with: ['posts'] });
317
+ const post = await postRepo.findById('uuid', { with: ['author'] });
318
+ ```
319
+
320
+ ## Lifecycle Hooks
321
+
322
+ ```typescript
323
+ class User extends Model {
324
+ static async beforeCreate(data: any) {
325
+ data.password = await hashPassword(data.password);
326
+ return data;
327
+ }
328
+
329
+ static async afterCreate(_data: any, result: any) {
330
+ await sendWelcomeEmail(result.email);
331
+ return result;
332
+ }
333
+
334
+ // Called once after createMany() — use for bulk side effects
335
+ static async afterCreateMany(records: any[]) {
336
+ await sendBulkWelcomeEmails(records.map(r => r.email));
337
+ return records;
338
+ }
339
+
340
+ static async beforeUpdate(_id: string, data: any) {
341
+ return data;
342
+ }
343
+
344
+ static async afterUpdate(id: string, _data: any, result: any) {
345
+ await auditLog('update', id);
346
+ return result;
347
+ }
348
+
349
+ static async beforeDelete(id: string) {
350
+ await archiveUserData(id);
351
+ }
352
+
353
+ static async afterDelete(id: string, result: any) {
354
+ await auditLog('delete', id);
355
+ return result;
356
+ }
357
+
358
+ static async beforeFind(options: any) {
359
+ return options;
360
+ }
361
+
362
+ static async afterFind(results: any[]) {
363
+ return results.map(u => ({ ...u, displayName: u.name }));
364
+ }
365
+ }
366
+ ```
367
+
368
+ > **`afterCreate` vs `afterCreateMany`:** `afterCreate` is called for each individual `create()`. `afterCreateMany` is called once with all records after `createMany()`. This is intentional — calling `afterCreate` N times in a batch defeats the performance benefit of bulk insert.
369
+
370
+ ## Using Repositories
371
+
372
+ ```typescript
373
+ const crudora = server.getCrudora();
374
+ const userRepo = crudora.getRepository(User);
375
+
376
+ // Create
377
+ const user = await userRepo.create({ email: 'john@example.com', password: 'plain' });
378
+
379
+ // Bulk insert
380
+ const users = await userRepo.createMany([
381
+ { email: 'alice@example.com', password: '...' },
382
+ { email: 'bob@example.com', password: '...' },
383
+ ]);
384
+
385
+ // Find
386
+ const user = await userRepo.findById('uuid');
387
+ const users = await userRepo.findAll({ skip: 0, take: 10, where: { isActive: 'true' } });
388
+ const first = await userRepo.findOne({ email: 'john@example.com' });
389
+ const exists = await userRepo.exists({ email: 'john@example.com' });
390
+ const total = await userRepo.count({ isActive: 'true' });
391
+
392
+ // Cursor pagination
393
+ const page1 = await userRepo.findWithCursor({ take: 10 });
394
+ const page2 = await userRepo.findWithCursor({ take: 10, cursor: page1.nextCursor });
395
+
396
+ // Update / Delete
397
+ const updated = await userRepo.update('uuid', { name: 'Jane' });
398
+ await userRepo.delete('uuid');
399
+
400
+ // Transactions
401
+ await userRepo.transaction(async (trx) => {
402
+ const user = await trx.create({ email: 'alice@example.com', password: '...' });
403
+ await postTrxRepo.create({ title: 'Hello', authorId: user.id });
404
+ });
405
+ ```
406
+
407
+ ## Logging
408
+
409
+ By default, Crudora writes structured JSON to the console for request errors:
410
+
411
+ ```json
412
+ {"level":"error","time":"2025-01-01T00:00:00.000Z","msg":"POST request failed","path":"/api/users","correlationId":"uuid-...","error":"Duplicate key"}
413
+ ```
414
+
415
+ Every request automatically gets a unique **correlation ID** (`req.correlationId`) that appears in all log entries for that request.
416
+
417
+ ### Custom Logger
418
+
419
+ Pass any object with `error`, `warn`, `info`, `debug` methods:
420
+
421
+ ```typescript
422
+ import pino from 'pino';
423
+
424
+ const logger = pino();
425
+
426
+ const server = new CrudoraServer({
427
+ db,
428
+ dialect: 'postgresql',
429
+ logger: {
430
+ error: (msg, ctx) => logger.error(ctx, msg),
431
+ warn: (msg, ctx) => logger.warn(ctx, msg),
432
+ info: (msg, ctx) => logger.info(ctx, msg),
433
+ debug: (msg, ctx) => logger.debug(ctx, msg),
434
+ },
435
+ });
436
+
437
+ // Or disable logging entirely
438
+ const server = new CrudoraServer({ db, dialect: 'postgresql', logger: false });
439
+ ```
440
+
441
+ ## Schema Generation
442
+
443
+ ```typescript
444
+ const schema = server.getCrudora().generateDrizzleSchema();
445
+ console.log(schema);
446
+ // → TypeScript file ready for drizzle-kit
447
+ ```
448
+
449
+ Example output:
450
+
451
+ ```typescript
452
+ // Auto-generated by Crudora — do not edit manually
453
+ import { pgTable, pgSchema, uuid, varchar, timestamp } from 'drizzle-orm/pg-core';
454
+
455
+ const authSchema = pgSchema('auth');
456
+
457
+ export const usersTable = authSchema.table('users', {
458
+ id: uuid('id').primaryKey(),
459
+ email: varchar('email', { length: 255 }).notNull().unique(),
460
+ password: varchar('password', { length: 255 }).notNull(),
461
+ createdAt: timestamp('createdAt', { mode: 'date' }).defaultNow().notNull(),
462
+ updatedAt: timestamp('updatedAt', { mode: 'date' }).defaultNow().notNull(),
463
+ });
464
+ ```
465
+
466
+ Or use the CLI:
467
+
468
+ ```bash
469
+ npx crudora generate-schema --entry src/server.ts --output src/db/schema.ts
470
+ ```
471
+
472
+ ## Custom Routes
473
+
474
+ ```typescript
475
+ server
476
+ .get('/health', (_req, res) => {
477
+ res.json({ success: true, data: { status: 'ok', timestamp: new Date() } });
478
+ })
479
+ .post('/auth/login', async (req, res) => {
480
+ const { email, password } = req.body;
481
+ const userRepo = server.getCrudora().getRepository(User);
482
+ // includeHidden: true bypasses static hidden so the password hash is readable
483
+ const row = await userRepo.findOne({ email }, { includeHidden: true });
484
+ if (!row || !verifyPassword(password, (row as any).password)) {
485
+ return res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } });
486
+ }
487
+ const { password: _pw, ...safeUser } = row as any;
488
+ res.json({ success: true, data: { token: generateJWT(safeUser) } });
489
+ });
490
+ ```
491
+
492
+ ## Authentication
493
+
494
+ Crudora has no built-in auth by design — strategies differ too much across projects. Use standard Express middleware instead.
495
+
496
+ **Protect all auto-generated routes:**
497
+
498
+ ```typescript
499
+ // Mount before generateRoutes() — every /api/* route will require a valid JWT
500
+ server.getApp().use('/api', verifyJWT);
501
+
502
+ server.registerModel(User, Post).generateRoutes().listen();
503
+ ```
504
+
505
+ **Add a login route** (note: `findOne()` strips `hidden` fields — pass `{ includeHidden: true }` to bypass):
506
+
507
+ ```typescript
508
+ server.post('/auth/login', async (req, res) => {
509
+ const userRepo = server.getCrudora().getRepository(User);
510
+
511
+ // includeHidden: true bypasses static hidden so the password hash is readable
512
+ const row = await userRepo.findOne({ email: req.body.email }, { includeHidden: true });
513
+
514
+ if (!row || !verifyPassword(req.body.password, (row as any).password)) {
515
+ return res.status(401).json({ success: false, error: 'Invalid credentials' });
516
+ }
517
+ const { password: _pw, ...safeUser } = row as any;
518
+ res.json({ success: true, data: { token: generateJWT(safeUser) } });
519
+ });
520
+ ```
521
+
522
+ See the [Authentication Guide](./docs/authentication.md) for register, per-route middleware, role guards, and a full JWT example.
523
+
524
+ ## Project Setup
525
+
526
+ ```bash
527
+ # Install dependencies
528
+ npm install drizzle-orm pg
529
+ npm install -D drizzle-kit typescript ts-node
530
+
531
+ # Update .env with your DATABASE_URL
532
+
533
+ # Push schema to database
534
+ npx drizzle-kit push
535
+
536
+ # Start development server
537
+ npx ts-node src/server.ts
538
+ ```
539
+
540
+ ## Documentation
541
+
542
+ - [API Reference](./docs/api.md)
543
+ - [Model Definition Guide](./docs/models.md)
544
+ - [Custom Routes](./docs/custom-routes.md)
545
+ - [Authentication Guide](./docs/authentication.md)
546
+ - [Deployment Guide](./docs/deployment.md)
547
+
548
+ ## Contributing
549
+
550
+ We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
551
+
552
+ ## License
553
+
554
+ MIT © [Muhammad Surya J](https://suryamsj.my.id)