crudora 0.2.0 → 0.3.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 (52) hide show
  1. package/dist/cli.js +55 -65
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.cjs +1732 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +438 -0
  6. package/dist/index.d.ts +438 -15
  7. package/dist/index.js +1725 -27
  8. package/dist/index.js.map +1 -1
  9. package/dist/scripts/copy-assets.js +0 -21
  10. package/package.json +5 -4
  11. package/scripts/copy-assets.js +0 -21
  12. package/dist/core/crudora.d.ts +0 -49
  13. package/dist/core/crudora.d.ts.map +0 -1
  14. package/dist/core/crudora.js +0 -370
  15. package/dist/core/crudora.js.map +0 -1
  16. package/dist/core/crudoraServer.d.ts +0 -84
  17. package/dist/core/crudoraServer.d.ts.map +0 -1
  18. package/dist/core/crudoraServer.js +0 -202
  19. package/dist/core/crudoraServer.js.map +0 -1
  20. package/dist/core/drizzleTableBuilder.d.ts +0 -6
  21. package/dist/core/drizzleTableBuilder.d.ts.map +0 -1
  22. package/dist/core/drizzleTableBuilder.js +0 -175
  23. package/dist/core/drizzleTableBuilder.js.map +0 -1
  24. package/dist/core/model.d.ts +0 -58
  25. package/dist/core/model.d.ts.map +0 -1
  26. package/dist/core/model.js +0 -64
  27. package/dist/core/model.js.map +0 -1
  28. package/dist/core/repository.d.ts +0 -106
  29. package/dist/core/repository.d.ts.map +0 -1
  30. package/dist/core/repository.js +0 -607
  31. package/dist/core/repository.js.map +0 -1
  32. package/dist/core/schemaGenerator.d.ts +0 -6
  33. package/dist/core/schemaGenerator.d.ts.map +0 -1
  34. package/dist/core/schemaGenerator.js +0 -248
  35. package/dist/core/schemaGenerator.js.map +0 -1
  36. package/dist/decorators/model.d.ts +0 -64
  37. package/dist/decorators/model.d.ts.map +0 -1
  38. package/dist/decorators/model.js +0 -138
  39. package/dist/decorators/model.js.map +0 -1
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/types/logger.type.d.ts +0 -7
  42. package/dist/types/logger.type.d.ts.map +0 -1
  43. package/dist/types/logger.type.js +0 -3
  44. package/dist/types/logger.type.js.map +0 -1
  45. package/dist/types/model.type.d.ts +0 -38
  46. package/dist/types/model.type.d.ts.map +0 -1
  47. package/dist/types/model.type.js +0 -3
  48. package/dist/types/model.type.js.map +0 -1
  49. package/dist/utils/validation.d.ts +0 -7
  50. package/dist/utils/validation.d.ts.map +0 -1
  51. package/dist/utils/validation.js +0 -107
  52. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,438 @@
1
+ import { Express } from 'express';
2
+ import { z } from 'zod';
3
+ import http from 'http';
4
+
5
+ /**
6
+ * Base class for all Crudora models. Extend this to define your table schema,
7
+ * configure lifecycle hooks, and control API behaviour via static properties.
8
+ *
9
+ * Use `@Field()` decorators on instance properties to describe columns.
10
+ * Use `@HasMany()`, `@HasOne()`, `@BelongsTo()`, `@BelongsToMany()` for relations.
11
+ *
12
+ * This is a plain class — **not** a decorator. Extend it:
13
+ * ```ts
14
+ * class User extends Model {
15
+ * static tableName = 'users';
16
+ * }
17
+ * ```
18
+ */
19
+ declare abstract class Model {
20
+ static tableName?: string;
21
+ static primaryKey?: string;
22
+ static timestamps?: boolean;
23
+ static softDelete?: boolean;
24
+ static fillable?: string[];
25
+ static hidden?: string[];
26
+ static schema?: string;
27
+ static getTableName(): string;
28
+ static getPrimaryKey(): string;
29
+ static beforeCreate?(_data: any): Promise<any>;
30
+ static afterCreate?(_data: any, result: any): Promise<any>;
31
+ static afterCreateMany?(_records: any[]): Promise<any[]>;
32
+ static beforeUpdate?(_id: string, _data: any): Promise<any>;
33
+ static afterUpdate?(_id: string, _data: any, result: any): Promise<any>;
34
+ static beforeDelete?(_id: string): Promise<void>;
35
+ static afterDelete?(_id: string, result: any): Promise<any>;
36
+ static beforeFind?(options?: any): Promise<any>;
37
+ static afterFind?(results: any[]): Promise<any[]>;
38
+ beforeSave?(): Promise<void>;
39
+ afterSave?(): Promise<void>;
40
+ }
41
+ type ModelConstructor<T extends Model = Model> = {
42
+ new (): T;
43
+ tableName?: string;
44
+ primaryKey?: string;
45
+ timestamps?: boolean;
46
+ softDelete?: boolean;
47
+ fillable?: string[];
48
+ hidden?: string[];
49
+ schema?: string;
50
+ getTableName(): string;
51
+ getPrimaryKey(): string;
52
+ beforeCreate?(data: any): Promise<any>;
53
+ afterCreate?(data: any, result: any): Promise<any>;
54
+ afterCreateMany?(records: any[]): Promise<any[]>;
55
+ beforeUpdate?(id: string, data: any): Promise<any>;
56
+ afterUpdate?(id: string, data: any, result: any): Promise<any>;
57
+ beforeDelete?(id: string): Promise<void>;
58
+ afterDelete?(id: string, result: any): Promise<any>;
59
+ beforeFind?(options?: any): Promise<any>;
60
+ afterFind?(results: any[]): Promise<any[]>;
61
+ } & typeof Model;
62
+
63
+ interface FindAllOptions {
64
+ skip?: number;
65
+ take?: number;
66
+ where?: Record<string, any>;
67
+ orderBy?: string | string[];
68
+ order?: 'asc' | 'desc' | Array<'asc' | 'desc'>;
69
+ select?: string[];
70
+ with?: string[];
71
+ withDeleted?: boolean;
72
+ /** When true, fields listed in `static hidden` are included in the result. */
73
+ includeHidden?: boolean;
74
+ }
75
+ interface FindByIdOptions {
76
+ select?: string[];
77
+ with?: string[];
78
+ withDeleted?: boolean;
79
+ /** When true, fields listed in `static hidden` are included in the result. */
80
+ includeHidden?: boolean;
81
+ }
82
+ interface CursorPaginationOptions {
83
+ take?: number;
84
+ cursor?: string | null;
85
+ cursorField?: string;
86
+ order?: 'asc' | 'desc';
87
+ where?: Record<string, any>;
88
+ select?: string[];
89
+ with?: string[];
90
+ withDeleted?: boolean;
91
+ /** When true, fields listed in `static hidden` are included in the result. */
92
+ includeHidden?: boolean;
93
+ }
94
+ interface CursorResult<T> {
95
+ data: T[];
96
+ nextCursor: string | null;
97
+ hasMore: boolean;
98
+ }
99
+ declare class NotFoundError extends Error {
100
+ readonly code = "NOT_FOUND";
101
+ constructor(message: string);
102
+ }
103
+ declare class Repository<T extends Model> {
104
+ private modelClass;
105
+ private db;
106
+ private table;
107
+ /** Shared registry — same Map instance as Crudora holds; populated lazily. */
108
+ private registry?;
109
+ constructor(modelClass: ModelConstructor<T>, db: any, table: any,
110
+ /** Shared registry — same Map instance as Crudora holds; populated lazily. */
111
+ registry?: Map<string, Repository<any>> | undefined);
112
+ /**
113
+ * Builds the Drizzle select-column object, merging hidden-field exclusion with
114
+ * an optional explicit field list. Returns `undefined` to select all columns.
115
+ */
116
+ private buildSelectCols;
117
+ private buildWhere;
118
+ private softDeleteClause;
119
+ private combineWhere;
120
+ /**
121
+ * Batch-loads relations for a list of records. Uses the _in operator so the
122
+ * total number of DB round-trips equals the number of requested relations.
123
+ */
124
+ private loadRelations;
125
+ create(data: Partial<T>): Promise<T>;
126
+ findById(id: string, opts?: FindByIdOptions): Promise<T | null>;
127
+ findAll(options?: FindAllOptions): Promise<T[]>;
128
+ /**
129
+ * Cursor-based pagination — more efficient than offset for large datasets.
130
+ *
131
+ * @example
132
+ * const page1 = await userRepo.findWithCursor({ take: 10 });
133
+ * const page2 = await userRepo.findWithCursor({ take: 10, cursor: page1.nextCursor });
134
+ */
135
+ findWithCursor(options: CursorPaginationOptions): Promise<CursorResult<T>>;
136
+ update(id: string, data: Partial<T>): Promise<T>;
137
+ delete(id: string): Promise<T>;
138
+ hardDelete(id: string): Promise<T>;
139
+ restore(id: string): Promise<T>;
140
+ count(where?: any, withDeleted?: boolean): Promise<number>;
141
+ /** Returns the first record matching `where`, or null. Builds a direct LIMIT 1 query — does not delegate to findAll. */
142
+ findOne(where?: Record<string, any>, opts?: FindByIdOptions): Promise<T | null>;
143
+ /**
144
+ * Returns true if at least one record matches `where`.
145
+ * Uses SELECT pk LIMIT 1 — more efficient than COUNT(*) because the DB stops at the first match.
146
+ */
147
+ exists(where?: Record<string, any>, withDeleted?: boolean): Promise<boolean>;
148
+ /**
149
+ * Inserts multiple records in a single INSERT statement.
150
+ * Calls `beforeCreate` for each row. Calls `afterCreateMany` once with all
151
+ * results — use this hook for batch-aware side effects (e.g. bulk notifications).
152
+ * Individual `afterCreate` is intentionally NOT called per row for performance.
153
+ */
154
+ createMany(data: Partial<T>[]): Promise<T[]>;
155
+ /**
156
+ * Runs `fn` inside a database transaction. The callback receives a new
157
+ * Repository bound to the transaction connection.
158
+ *
159
+ * @example
160
+ * await userRepo.transaction(async (trx) => {
161
+ * await trx.create({ name: 'Alice' });
162
+ * await trx.update(id, { name: 'Bob' });
163
+ * });
164
+ */
165
+ transaction<R>(fn: (trx: Repository<T>) => Promise<R>): Promise<R>;
166
+ }
167
+
168
+ type FieldType = 'string' | 'text' | 'integer' | 'number' | 'boolean' | 'date' | 'uuid' | 'decimal' | 'json' | 'enum' | 'bigint' | 'serial' | 'array';
169
+ interface FieldOptions {
170
+ type: FieldType;
171
+ required?: boolean;
172
+ unique?: boolean;
173
+ default?: any;
174
+ length?: number;
175
+ primary?: boolean;
176
+ nullable?: boolean;
177
+ precision?: number;
178
+ scale?: number;
179
+ /** Required when type is 'enum'. Lists the allowed values. */
180
+ enumValues?: string[];
181
+ }
182
+ interface ModelOptions {
183
+ tableName?: string;
184
+ timestamps?: boolean;
185
+ }
186
+ type Dialect = 'postgresql' | 'mysql';
187
+ type RelationType = 'hasOne' | 'hasMany' | 'belongsTo' | 'belongsToMany';
188
+ interface RelationDefinition {
189
+ type: RelationType;
190
+ /** Lazy model getter — avoids circular-import issues */
191
+ model: () => any;
192
+ /** The foreign-key column name */
193
+ foreignKey: string;
194
+ /**
195
+ * hasOne / hasMany: column on the owning (local) side. Defaults to the model's primary key.
196
+ * belongsTo : column on the related side (the "owner"). Defaults to the related model's primary key.
197
+ * belongsToMany: override for this model's local key (defaults to primary key).
198
+ */
199
+ relatedKey?: string;
200
+ /** Lazy getter returning the pivot/junction model constructor. */
201
+ pivotModel?: () => any;
202
+ /** Column on the pivot table that points to the **related** model. */
203
+ pivotRelatedKey?: string;
204
+ }
205
+
206
+ interface CrudoraLogger {
207
+ error(message: string, context?: Record<string, any>): void;
208
+ warn(message: string, context?: Record<string, any>): void;
209
+ info(message: string, context?: Record<string, any>): void;
210
+ debug(message: string, context?: Record<string, any>): void;
211
+ }
212
+
213
+ declare class Crudora {
214
+ private db;
215
+ private dialect;
216
+ private logger;
217
+ private models;
218
+ private tables;
219
+ private repositories;
220
+ private customRoutes;
221
+ constructor(db: any, dialect: Dialect, logger?: CrudoraLogger);
222
+ registerModel(...modelClasses: ModelConstructor[]): this;
223
+ /**
224
+ * Register a model against a pre-built Drizzle table object (e.g. from `drizzle-kit introspect`).
225
+ * Skips `DrizzleTableBuilder` — useful when the database already exists and the schema
226
+ * was generated via introspection rather than Crudora decorators.
227
+ *
228
+ * Validation works if the model defines `@Field()` decorators matching the table columns.
229
+ * Without decorators, POST/PUT bodies pass through without Zod validation.
230
+ */
231
+ registerTable<T extends Model>(modelClass: ModelConstructor<T>, table: any): this;
232
+ getRepository<T extends Model>(modelClass: ModelConstructor<T>): Repository<T>;
233
+ /**
234
+ * Returns the Drizzle table object for a registered model.
235
+ * Useful for raw queries that need access to columns excluded by `hidden`
236
+ * (e.g. fetching a password hash for authentication).
237
+ *
238
+ * @example
239
+ * const usersTable = crudora.getTable(User);
240
+ * const [row] = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1);
241
+ */
242
+ getTable<T extends Model>(modelClass: ModelConstructor<T>): any;
243
+ generateDrizzleSchema(): string;
244
+ getValidationSchema<T extends Model>(modelClass: ModelConstructor<T>): z.ZodType<Partial<T>>;
245
+ getStrictValidationSchema<T extends Model>(modelClass: ModelConstructor<T>): z.ZodType<T>;
246
+ get(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
247
+ post(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
248
+ put(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
249
+ delete(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
250
+ patch(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
251
+ /** Runs `fn` inside a database transaction and returns its result. */
252
+ transaction<R>(fn: (db: any) => Promise<R>): Promise<R>;
253
+ generateRoutes(app: Express, basePath?: string): void;
254
+ }
255
+
256
+ interface RateLimitConfig {
257
+ /** Sliding-window duration in milliseconds. Default: `60_000` (1 minute). */
258
+ windowMs?: number;
259
+ /** Maximum number of requests per key per window. Default: `100`. */
260
+ max?: number;
261
+ /** Message body returned with 429 responses. Default: `'Too many requests'`. */
262
+ message?: string;
263
+ /**
264
+ * Function that extracts the rate-limit key from a request.
265
+ * Default: `req.ip` (the direct connection IP, or `X-Forwarded-For` if `trust proxy` is set on Express).
266
+ */
267
+ keyGenerator?: (req: any) => string;
268
+ }
269
+ interface CrudoraServerConfig {
270
+ port?: number;
271
+ /**
272
+ * CORS configuration.
273
+ * - `true` (default): allow all origins (`*`)
274
+ * - `false`: disable CORS headers entirely
275
+ * - `string`: allow a single specific origin, e.g. `'https://app.example.com'`
276
+ * - `string[]`: allow a list of origins; the request's Origin is reflected if it matches
277
+ */
278
+ cors?: boolean | string | string[];
279
+ bodyParser?: boolean;
280
+ /**
281
+ * Maximum request body size accepted by the JSON and URL-encoded body parsers.
282
+ * Accepts a number (bytes) or a string with a unit suffix (`'100kb'`, `'5mb'`).
283
+ * Default: `'100kb'`.
284
+ */
285
+ bodyParserLimit?: string | number;
286
+ db: any;
287
+ dialect: Dialect;
288
+ basePath?: string;
289
+ /**
290
+ * Logger for request errors and lifecycle events.
291
+ * - Omit (default): structured JSON written to console (compatible with log aggregators).
292
+ * - Pass a pino/winston instance or any object with `error`, `warn`, `info`, `debug` methods.
293
+ * - Pass `false` to disable all Crudora logging.
294
+ */
295
+ logger?: CrudoraLogger | false;
296
+ /**
297
+ * Built-in sliding-window rate limiter applied to every route.
298
+ * - Omit / `undefined` (default): enabled with 100 requests per minute per IP.
299
+ * - Pass a `RateLimitConfig` object to customise the window, limit, or key function.
300
+ * - Pass `false` to disable rate limiting entirely (e.g. when using an external proxy-level limiter).
301
+ *
302
+ * **Important:** This limiter is in-memory and per-process. In multi-instance deployments
303
+ * (load balancer, Kubernetes replicas), each instance tracks its own counter independently —
304
+ * the effective limit per client is `max × instanceCount`. Use a Redis-backed limiter
305
+ * (e.g. `rate-limiter-flexible`) and pass `rateLimit: false` to disable this one.
306
+ */
307
+ rateLimit?: RateLimitConfig | false;
308
+ /**
309
+ * Socket-level request timeout in milliseconds.
310
+ * Requests that exceed this duration are terminated with a `503` response.
311
+ * - Omit / `0` (default): no timeout.
312
+ * - Recommended: `30_000` (30 s) for most APIs.
313
+ */
314
+ timeout?: number;
315
+ /**
316
+ * Built-in health check endpoint.
317
+ * - `true` (default): mounts `GET /health` returning `{ status: 'ok' }`.
318
+ * - `string`: custom path, e.g. `'/healthz'`.
319
+ * - `false`: disable entirely.
320
+ */
321
+ healthCheck?: boolean | string;
322
+ }
323
+ declare class CrudoraServer {
324
+ private app;
325
+ private crudora;
326
+ private config;
327
+ private httpServer;
328
+ constructor(config: CrudoraServerConfig);
329
+ private setupMiddleware;
330
+ registerModel(...modelClasses: ModelConstructor[]): this;
331
+ registerTable<T extends Model>(modelClass: ModelConstructor<T>, table: any): this;
332
+ generateRoutes(): this;
333
+ use(middleware: any): this;
334
+ /**
335
+ * Starts the HTTP server and returns the underlying `http.Server` instance.
336
+ * Use the returned server for graceful shutdown:
337
+ *
338
+ * @example
339
+ * const httpServer = server.listen();
340
+ * process.on('SIGTERM', () => httpServer.close(() => process.exit(0)));
341
+ */
342
+ listen(callback?: () => void): http.Server;
343
+ /**
344
+ * Returns the `http.Server` instance after `listen()` has been called, or `null` before.
345
+ * Useful when you need the server reference without calling listen again.
346
+ */
347
+ getHttpServer(): http.Server | null;
348
+ getApp(): Express;
349
+ getCrudora(): Crudora;
350
+ /**
351
+ * Returns the Drizzle table object for a registered model — delegates to `Crudora.getTable()`.
352
+ * Use this when you need raw DB access to columns excluded by `static hidden`
353
+ * (e.g. a login route that must read the password hash).
354
+ *
355
+ * @example
356
+ * const usersTable = server.getTable(User);
357
+ * const [row] = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1);
358
+ */
359
+ getTable<T extends Model>(modelClass: ModelConstructor<T>): any;
360
+ get(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
361
+ post(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
362
+ put(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
363
+ delete(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
364
+ patch(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
365
+ }
366
+
367
+ declare class SchemaGenerator {
368
+ static generateDrizzleSchema(models: ModelConstructor[], dialect: Dialect): string;
369
+ }
370
+
371
+ declare class DrizzleTableBuilder {
372
+ static build(modelClass: ModelConstructor, dialect: Dialect): any;
373
+ }
374
+
375
+ declare class ValidationGenerator {
376
+ static generateZodSchema<T extends Model>(modelClass: ModelConstructor<T>): z.ZodType<Partial<T>>;
377
+ static generateStrictZodSchema<T extends Model>(modelClass: ModelConstructor<T>): z.ZodType<any>;
378
+ }
379
+
380
+ declare function Field(options: FieldOptions): (target: any, context: any) => void;
381
+ declare function getFieldMetadata(target: any): any;
382
+ /**
383
+ * Declares a one-to-many relation.
384
+ * @param model Lazy getter returning the related model constructor.
385
+ * @param foreignKey Column on the **related** table that references this model.
386
+ * @param localKey Column on **this** table (defaults to this model's primary key).
387
+ *
388
+ * @example
389
+ * class User extends Model {
390
+ * @HasMany(() => Post, 'authorId')
391
+ * posts?: Post[];
392
+ * }
393
+ */
394
+ declare function HasMany(model: () => any, foreignKey: string, localKey?: string): (target: any, propertyKey: string) => void;
395
+ /**
396
+ * Declares a one-to-one relation where the FK lives on the related table.
397
+ * @param model Lazy getter returning the related model constructor.
398
+ * @param foreignKey Column on the **related** table that references this model.
399
+ * @param localKey Column on **this** table (defaults to primary key).
400
+ *
401
+ * @example
402
+ * class User extends Model {
403
+ * @HasOne(() => Profile, 'userId')
404
+ * profile?: Profile;
405
+ * }
406
+ */
407
+ declare function HasOne(model: () => any, foreignKey: string, localKey?: string): (target: any, propertyKey: string) => void;
408
+ /**
409
+ * Declares a many-to-one (or one-to-one) relation where the FK lives on **this** table.
410
+ * @param model Lazy getter returning the related model constructor.
411
+ * @param foreignKey Column on **this** table that holds the FK value.
412
+ * @param ownerKey Column on the **related** table being pointed to (defaults to its primary key).
413
+ *
414
+ * @example
415
+ * class Post extends Model {
416
+ * @BelongsTo(() => User, 'authorId')
417
+ * author?: User;
418
+ * }
419
+ */
420
+ declare function BelongsTo(model: () => any, foreignKey: string, ownerKey?: string): (target: any, propertyKey: string) => void;
421
+ /**
422
+ * Declares a many-to-many relation through a pivot/junction model.
423
+ * @param model Lazy getter returning the **related** model constructor.
424
+ * @param pivotModel Lazy getter returning the **pivot** model constructor.
425
+ * @param pivotForeignKey Column on the pivot table that references **this** model.
426
+ * @param pivotRelatedKey Column on the pivot table that references the **related** model.
427
+ * @param localKey Override for this model's local key (defaults to primary key).
428
+ *
429
+ * @example
430
+ * class User extends Model {
431
+ * @BelongsToMany(() => Role, () => UserRole, 'userId', 'roleId')
432
+ * roles?: Role[];
433
+ * }
434
+ */
435
+ declare function BelongsToMany(model: () => any, pivotModel: () => any, pivotForeignKey: string, pivotRelatedKey: string, localKey?: string): (target: any, propertyKey: string) => void;
436
+ declare function getRelationMetadata(target: any): Record<string, RelationDefinition>;
437
+
438
+ export { BelongsTo, BelongsToMany, Crudora, type CrudoraLogger, CrudoraServer, type CrudoraServerConfig, type CursorPaginationOptions, type CursorResult, type Dialect, DrizzleTableBuilder, Field, type FieldOptions, type FieldType, type FindAllOptions, type FindByIdOptions, HasMany, HasOne, Model, type ModelConstructor, type ModelOptions, NotFoundError, type RateLimitConfig, type RelationDefinition, type RelationType, Repository, SchemaGenerator, ValidationGenerator, getFieldMetadata, getRelationMetadata };