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/dist/cli.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import 'reflect-metadata';
3
+ import { Command } from 'commander';
4
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { fileURLToPath, pathToFileURL } from 'url';
7
+ import { createRequire } from 'module';
8
+ import path from 'path';
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const _require = createRequire(import.meta.url);
12
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'));
13
+ const program = new Command();
14
+ program
15
+ .name('crudora')
16
+ .description('Crudora CLI — tools for Crudora projects')
17
+ .version(pkg.version ?? '0.0.0');
18
+ program
19
+ .command('generate-schema')
20
+ .description('Generate a Drizzle TypeScript schema file from registered Crudora models')
21
+ .option('-e, --entry <file>', 'Entry file (JS/TS) that exports a CrudoraServer or Crudora instance', 'src/server.ts')
22
+ .option('-o, --output <file>', 'Output path for the generated schema', 'src/db/schema.ts')
23
+ .action(async (opts) => {
24
+ const entryPath = resolve(process.cwd(), opts.entry);
25
+ if (!existsSync(entryPath)) {
26
+ console.error(`Error: entry file not found — ${entryPath}`);
27
+ process.exit(1);
28
+ }
29
+ try {
30
+ let mod;
31
+ try {
32
+ // ESM dynamic import — works for compiled .js / .mjs entry files
33
+ mod = await import(pathToFileURL(entryPath).href);
34
+ }
35
+ catch {
36
+ // Fallback: CJS require — for pre-compiled CommonJS output
37
+ try {
38
+ _require('ts-node/register');
39
+ }
40
+ catch {
41
+ // ts-node not installed — entry must be pre-compiled JS
42
+ }
43
+ mod = _require(entryPath);
44
+ }
45
+ const instance = mod.default ?? mod.server ?? mod.crudora;
46
+ if (!instance) {
47
+ console.error('Error: entry file must export a CrudoraServer or Crudora instance as default, "server", or "crudora".');
48
+ process.exit(1);
49
+ }
50
+ const crudora = typeof instance.getCrudora === 'function' ? instance.getCrudora() : instance;
51
+ if (typeof crudora.generateDrizzleSchema !== 'function') {
52
+ console.error('Error: could not find generateDrizzleSchema() on the exported instance.');
53
+ process.exit(1);
54
+ }
55
+ const schema = crudora.generateDrizzleSchema();
56
+ const outputPath = resolve(process.cwd(), opts.output);
57
+ const cwd = process.cwd() + path.sep;
58
+ if (!outputPath.startsWith(cwd)) {
59
+ console.error('Error: output path must be within the project directory.');
60
+ process.exit(1);
61
+ }
62
+ writeFileSync(outputPath, schema, 'utf-8');
63
+ console.log(`Schema written to ${outputPath}`);
64
+ console.log('Run: npx drizzle-kit push — to apply the schema to your database.');
65
+ }
66
+ catch (err) {
67
+ console.error('Error generating schema:', err.message);
68
+ process.exit(1);
69
+ }
70
+ });
71
+ program.parse();
72
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -1,24 +1,49 @@
1
- import { PrismaClient } from '@prisma/client';
2
1
  import { Express } from 'express';
3
2
  import { z } from 'zod';
4
3
  import { Repository } from './repository';
5
4
  import { Model, ModelConstructor } from './model';
5
+ import { Dialect } from '../types/model.type';
6
+ import { CrudoraLogger } from '../types/logger.type';
6
7
  export declare class Crudora {
7
- private prisma;
8
+ private db;
9
+ private dialect;
10
+ private logger;
8
11
  private models;
12
+ private tables;
9
13
  private repositories;
10
14
  private customRoutes;
11
- constructor(prisma?: PrismaClient);
15
+ constructor(db: any, dialect: Dialect, logger?: CrudoraLogger);
12
16
  registerModel(...modelClasses: ModelConstructor[]): this;
17
+ /**
18
+ * Register a model against a pre-built Drizzle table object (e.g. from `drizzle-kit introspect`).
19
+ * Skips `DrizzleTableBuilder` — useful when the database already exists and the schema
20
+ * was generated via introspection rather than Crudora decorators.
21
+ *
22
+ * Validation works if the model defines `@Field()` decorators matching the table columns.
23
+ * Without decorators, POST/PUT bodies pass through without Zod validation.
24
+ */
25
+ registerTable<T extends Model>(modelClass: ModelConstructor<T>, table: any): this;
13
26
  getRepository<T extends Model>(modelClass: ModelConstructor<T>): Repository<T>;
14
- generatePrismaSchema(databaseProvider?: string): string;
27
+ /**
28
+ * Returns the Drizzle table object for a registered model.
29
+ * Useful for raw queries that need access to columns excluded by `hidden`
30
+ * (e.g. fetching a password hash for authentication).
31
+ *
32
+ * @example
33
+ * const usersTable = crudora.getTable(User);
34
+ * const [row] = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1);
35
+ */
36
+ getTable<T extends Model>(modelClass: ModelConstructor<T>): any;
37
+ generateDrizzleSchema(): string;
15
38
  getValidationSchema<T extends Model>(modelClass: ModelConstructor<T>): z.ZodType<Partial<T>>;
16
39
  getStrictValidationSchema<T extends Model>(modelClass: ModelConstructor<T>): z.ZodType<T>;
17
- get(path: string, handler: (req: any, res: any) => void): this;
18
- post(path: string, handler: (req: any, res: any) => void): this;
19
- put(path: string, handler: (req: any, res: any) => void): this;
20
- delete(path: string, handler: (req: any, res: any) => void): this;
21
- patch(path: string, handler: (req: any, res: any) => void): this;
40
+ get(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
41
+ post(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
42
+ put(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
43
+ delete(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
44
+ patch(path: string, ...handlers: Array<(req: any, res: any, next?: any) => void>): this;
45
+ /** Runs `fn` inside a database transaction and returns its result. */
46
+ transaction<R>(fn: (db: any) => Promise<R>): Promise<R>;
22
47
  generateRoutes(app: Express, basePath?: string): void;
23
48
  }
24
49
  //# sourceMappingURL=crudora.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crudora.d.ts","sourceRoot":"","sources":["../../src/core/crudora.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAElD,qBAAa,OAAO;IAClB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,MAAM,CAA4C;IAC1D,OAAO,CAAC,YAAY,CAA2C;IAC/D,OAAO,CAAC,YAAY,CAIZ;gBAEI,MAAM,CAAC,EAAE,YAAY;IAQjC,aAAa,CAAC,GAAG,YAAY,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAWxD,aAAa,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;IAQ9E,oBAAoB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM;IAKvD,mBAAmB,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAI5F,yBAAyB,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAKzF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAK9D,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAK/D,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAK9D,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAKjE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI;IAOhE,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAe,GAAG,IAAI;CAsK9D"}
1
+ {"version":3,"file":"crudora.d.ts","sourceRoot":"","sources":["../../src/core/crudora.ts"],"names":[],"mappings":"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"}
@@ -5,26 +5,74 @@ const zod_1 = require("zod");
5
5
  const repository_1 = require("./repository");
6
6
  const schemaGenerator_1 = require("./schemaGenerator");
7
7
  const validation_1 = require("../utils/validation");
8
+ const drizzleTableBuilder_1 = require("./drizzleTableBuilder");
9
+ const MAX_LIMIT = 1000;
10
+ const MAX_RELATIONS = 5;
11
+ /** Validates a query-param key: alphanumeric/underscore with optional operator suffix. */
12
+ const SAFE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]*(_gt|_gte|_lt|_lte|_ne|_like|_in)?$/;
13
+ // ─── Response helpers ─────────────────────────────────────────────────────────
14
+ function ok(data, meta) {
15
+ return meta !== undefined ? { success: true, data, meta } : { success: true, data };
16
+ }
17
+ function fail(code, message, details) {
18
+ const error = { code, message };
19
+ if (details !== undefined)
20
+ error.details = details;
21
+ return { success: false, error };
22
+ }
23
+ function zodDetails(issues) {
24
+ return issues.map((issue) => ({
25
+ field: issue.path.length ? issue.path.join('.') : '_root',
26
+ message: issue.message,
27
+ }));
28
+ }
8
29
  class Crudora {
9
- constructor(prisma) {
30
+ constructor(db, dialect, logger) {
10
31
  this.models = new Map();
32
+ this.tables = new Map();
11
33
  this.repositories = new Map();
12
34
  this.customRoutes = [];
13
- if (!prisma) {
14
- console.warn('⚠️ No Prisma client provided. Make sure to install @prisma/client and prisma, then initialize PrismaClient.');
15
- throw new Error('PrismaClient is required. Please provide a PrismaClient instance.');
35
+ if (!db) {
36
+ throw new Error('Crudora: db is required. Provide a Drizzle db instance:\n' +
37
+ ' import { drizzle } from "drizzle-orm/node-postgres";\n' +
38
+ ' import { Pool } from "pg";\n' +
39
+ ' const db = drizzle(new Pool({ connectionString: process.env.DATABASE_URL }));');
16
40
  }
17
- this.prisma = prisma;
41
+ if (typeof db.select !== 'function' || typeof db.insert !== 'function') {
42
+ throw new Error('Crudora: the provided db object does not look like a Drizzle ORM instance. ' +
43
+ 'Make sure drizzle-orm is installed (npm install drizzle-orm) and that you are ' +
44
+ 'passing the result of drizzle(pool) or drizzle(client), not a raw pool/connection.');
45
+ }
46
+ this.db = db;
47
+ this.dialect = dialect;
48
+ this.logger = logger;
18
49
  }
19
50
  registerModel(...modelClasses) {
20
51
  for (const modelClass of modelClasses) {
21
- const modelName = modelClass.name;
22
- this.models.set(modelName, modelClass);
23
- const repository = new repository_1.Repository(modelClass, this.prisma);
24
- this.repositories.set(modelName, repository);
52
+ const table = drizzleTableBuilder_1.DrizzleTableBuilder.build(modelClass, this.dialect);
53
+ // Pass the shared repositories Map so each repo can resolve siblings lazily
54
+ const repository = new repository_1.Repository(modelClass, this.db, table, this.repositories);
55
+ this.models.set(modelClass.name, modelClass);
56
+ this.tables.set(modelClass.name, table);
57
+ this.repositories.set(modelClass.name, repository);
25
58
  }
26
59
  return this;
27
60
  }
61
+ /**
62
+ * Register a model against a pre-built Drizzle table object (e.g. from `drizzle-kit introspect`).
63
+ * Skips `DrizzleTableBuilder` — useful when the database already exists and the schema
64
+ * was generated via introspection rather than Crudora decorators.
65
+ *
66
+ * Validation works if the model defines `@Field()` decorators matching the table columns.
67
+ * Without decorators, POST/PUT bodies pass through without Zod validation.
68
+ */
69
+ registerTable(modelClass, table) {
70
+ const repository = new repository_1.Repository(modelClass, this.db, table, this.repositories);
71
+ this.models.set(modelClass.name, modelClass);
72
+ this.tables.set(modelClass.name, table);
73
+ this.repositories.set(modelClass.name, repository);
74
+ return this;
75
+ }
28
76
  getRepository(modelClass) {
29
77
  const repository = this.repositories.get(modelClass.name);
30
78
  if (!repository) {
@@ -32,9 +80,25 @@ class Crudora {
32
80
  }
33
81
  return repository;
34
82
  }
35
- generatePrismaSchema(databaseProvider) {
83
+ /**
84
+ * Returns the Drizzle table object for a registered model.
85
+ * Useful for raw queries that need access to columns excluded by `hidden`
86
+ * (e.g. fetching a password hash for authentication).
87
+ *
88
+ * @example
89
+ * const usersTable = crudora.getTable(User);
90
+ * const [row] = await db.select().from(usersTable).where(eq(usersTable.email, email)).limit(1);
91
+ */
92
+ getTable(modelClass) {
93
+ const table = this.tables.get(modelClass.name);
94
+ if (!table) {
95
+ throw new Error(`Table for ${modelClass.name} not found. Did you register the model?`);
96
+ }
97
+ return table;
98
+ }
99
+ generateDrizzleSchema() {
36
100
  const modelClasses = Array.from(this.models.values());
37
- return schemaGenerator_1.SchemaGenerator.generatePrismaSchema(modelClasses, databaseProvider);
101
+ return schemaGenerator_1.SchemaGenerator.generateDrizzleSchema(modelClasses, this.dialect);
38
102
  }
39
103
  getValidationSchema(modelClass) {
40
104
  return validation_1.ValidationGenerator.generateZodSchema(modelClass);
@@ -42,176 +106,261 @@ class Crudora {
42
106
  getStrictValidationSchema(modelClass) {
43
107
  return validation_1.ValidationGenerator.generateStrictZodSchema(modelClass);
44
108
  }
45
- // Custom route methods
46
- get(path, handler) {
47
- this.customRoutes.push({ method: 'GET', path, handler });
109
+ get(path, ...handlers) {
110
+ this.customRoutes.push({ method: 'GET', path, handlers });
48
111
  return this;
49
112
  }
50
- post(path, handler) {
51
- this.customRoutes.push({ method: 'POST', path, handler });
113
+ post(path, ...handlers) {
114
+ this.customRoutes.push({ method: 'POST', path, handlers });
52
115
  return this;
53
116
  }
54
- put(path, handler) {
55
- this.customRoutes.push({ method: 'PUT', path, handler });
117
+ put(path, ...handlers) {
118
+ this.customRoutes.push({ method: 'PUT', path, handlers });
56
119
  return this;
57
120
  }
58
- delete(path, handler) {
59
- this.customRoutes.push({ method: 'DELETE', path, handler });
121
+ delete(path, ...handlers) {
122
+ this.customRoutes.push({ method: 'DELETE', path, handlers });
60
123
  return this;
61
124
  }
62
- patch(path, handler) {
63
- this.customRoutes.push({ method: 'PATCH', path, handler });
125
+ patch(path, ...handlers) {
126
+ this.customRoutes.push({ method: 'PATCH', path, handlers });
64
127
  return this;
65
128
  }
66
- // Auto-generate REST API routes
67
- // Auto-generate REST API routes
129
+ /** Runs `fn` inside a database transaction and returns its result. */
130
+ async transaction(fn) {
131
+ return this.db.transaction(fn);
132
+ }
68
133
  generateRoutes(app, basePath = '/api') {
69
- // Add API documentation endpoint
70
- app.get(basePath, (req, res) => {
134
+ // Route discovery endpoint
135
+ app.get(basePath, (_req, res) => {
71
136
  const routes = [];
72
- // Add CRUD routes for each model
73
- for (const [modelName, modelClass] of this.models) {
137
+ for (const [, modelClass] of this.models) {
74
138
  const routePath = `${basePath}/${modelClass.getTableName()}`;
75
- routes.push({
76
- method: 'GET',
77
- path: routePath,
78
- description: `List all ${modelClass.getTableName()}`,
79
- type: 'CRUD'
80
- }, {
81
- method: 'GET',
82
- path: `${routePath}/:id`,
83
- description: `Get ${modelClass.getTableName()} by ID`,
84
- type: 'CRUD'
85
- }, {
86
- method: 'POST',
87
- path: routePath,
88
- description: `Create new ${modelClass.getTableName()}`,
89
- type: 'CRUD'
90
- }, {
91
- method: 'PUT',
92
- path: `${routePath}/:id`,
93
- description: `Update ${modelClass.getTableName()} by ID`,
94
- type: 'CRUD'
95
- }, {
96
- method: 'DELETE',
97
- path: `${routePath}/:id`,
98
- description: `Delete ${modelClass.getTableName()} by ID`,
99
- type: 'CRUD'
100
- });
139
+ routes.push({ method: 'GET', path: routePath, description: `List all ${modelClass.getTableName()}`, type: 'CRUD' }, { method: 'GET', path: `${routePath}/:id`, description: `Get ${modelClass.getTableName()} by ID`, type: 'CRUD' }, { method: 'POST', path: routePath, description: `Create new ${modelClass.getTableName()}`, type: 'CRUD' }, { method: 'PUT', path: `${routePath}/:id`, description: `Replace ${modelClass.getTableName()} by ID`, type: 'CRUD' }, { method: 'PATCH', path: `${routePath}/:id`, description: `Partial update ${modelClass.getTableName()} by ID`, type: 'CRUD' }, { method: 'DELETE', path: `${routePath}/:id`, description: `Delete ${modelClass.getTableName()} by ID`, type: 'CRUD' });
101
140
  }
102
- // Add custom routes
103
141
  for (const route of this.customRoutes) {
104
142
  routes.push({
105
143
  method: route.method,
106
144
  path: `${basePath}${route.path}`,
107
145
  description: `Custom ${route.method} route`,
108
- type: 'Custom'
146
+ type: 'Custom',
109
147
  });
110
148
  }
111
- res.json({ routes });
149
+ res.json(ok({ routes }));
112
150
  });
113
- // Generate CRUD routes for models
114
- for (const [modelName, modelClass] of this.models) {
151
+ // CRUD routes per model
152
+ for (const [, modelClass] of this.models) {
115
153
  const repository = this.getRepository(modelClass);
116
154
  const validationSchema = this.getValidationSchema(modelClass);
155
+ const strictValidationSchema = this.getStrictValidationSchema(modelClass);
117
156
  const routePath = `${basePath}/${modelClass.getTableName()}`;
118
- // GET /api/model - List all
157
+ // ── GET /resource ──────────────────────────────────────────────────────
119
158
  app.get(routePath, async (req, res) => {
120
159
  try {
121
- const { page = 1, limit = 10, ...filters } = req.query;
122
- const skip = (Number(page) - 1) * Number(limit);
160
+ const { page, limit = '10', orderBy, order, cursor, // presence triggers cursor mode
161
+ cursorField, select, // comma-separated field names
162
+ with: withStr, // comma-separated relation names
163
+ withDeleted, ...filters } = req.query;
164
+ const selectFields = select
165
+ ? select.split(',').map((f) => f.trim()).filter((f) => SAFE_KEY_RE.test(f))
166
+ : undefined;
167
+ const withRelations = withStr
168
+ ? withStr.split(',').map((r) => r.trim()).filter((r) => SAFE_KEY_RE.test(r)).slice(0, MAX_RELATIONS)
169
+ : undefined;
170
+ // Sanitize filter keys to prevent prototype pollution
171
+ const safeFilters = {};
172
+ for (const [k, v] of Object.entries(filters)) {
173
+ if (SAFE_KEY_RE.test(k))
174
+ safeFilters[k] = v;
175
+ }
176
+ const whereFilters = Object.keys(safeFilters).length ? safeFilters : undefined;
177
+ // Clamp limit: reject NaN, 0, negatives; cap at MAX_LIMIT
178
+ const rawLimit = Number(limit);
179
+ const limitNum = Number.isFinite(rawLimit) && rawLimit > 0
180
+ ? Math.min(rawLimit, MAX_LIMIT)
181
+ : 10;
182
+ const includeDeleted = withDeleted === 'true';
183
+ // Multiple orderBy: ?orderBy=createdAt,name&order=desc,asc
184
+ const orderByFields = orderBy ? orderBy.split(',').map((f) => f.trim()).filter(Boolean) : undefined;
185
+ const orderValues = order
186
+ ? order.split(',').map((o) => o.trim()).filter(Boolean)
187
+ : undefined;
188
+ const orderByArg = orderByFields && orderByFields.length === 1 ? orderByFields[0] : orderByFields;
189
+ const orderArg = orderValues && orderValues.length === 1 ? orderValues[0] : orderValues;
190
+ if (cursor !== undefined) {
191
+ // ── Cursor-based pagination ────────────────────────────────────
192
+ const result = await repository.findWithCursor({
193
+ take: limitNum,
194
+ cursor: cursor || null,
195
+ cursorField,
196
+ order: (orderValues?.[0]) ?? (order === 'desc' ? 'desc' : 'asc'),
197
+ where: whereFilters,
198
+ select: selectFields,
199
+ with: withRelations,
200
+ withDeleted: includeDeleted,
201
+ });
202
+ return res.json(ok(result.data, {
203
+ cursor: { next: result.nextCursor, hasMore: result.hasMore },
204
+ }));
205
+ }
206
+ // ── Offset-based pagination ────────────────────────────────────
207
+ const rawPage = Number(page ?? 1);
208
+ const pageNum = Number.isFinite(rawPage) && rawPage > 0 ? Math.floor(rawPage) : 1;
209
+ const skip = (pageNum - 1) * limitNum;
123
210
  const items = await repository.findAll({
124
211
  skip,
125
- take: Number(limit),
126
- where: filters
127
- });
128
- const total = await repository.count(filters);
129
- res.json({
130
- data: items,
131
- pagination: {
132
- page: Number(page),
133
- limit: Number(limit),
134
- total,
135
- pages: Math.ceil(total / Number(limit))
136
- }
212
+ take: limitNum,
213
+ where: whereFilters,
214
+ orderBy: orderByArg,
215
+ order: orderArg,
216
+ select: selectFields,
217
+ with: withRelations,
218
+ withDeleted: includeDeleted,
137
219
  });
220
+ const total = await repository.count(whereFilters, includeDeleted);
221
+ return res.json(ok(items, {
222
+ pagination: { page: pageNum, limit: limitNum, total, pages: total === 0 ? 0 : Math.ceil(total / limitNum) },
223
+ }));
138
224
  }
139
- catch (error) {
140
- res.status(500).json({ error: 'Internal server error' });
225
+ catch (err) {
226
+ this.logger?.error('GET request failed', {
227
+ path: routePath,
228
+ correlationId: req.correlationId,
229
+ error: err instanceof Error ? err.message : String(err),
230
+ });
231
+ res.status(500).json(fail('INTERNAL_ERROR', 'Internal server error'));
141
232
  }
142
233
  });
143
- // GET /api/model/:id - Get by ID
234
+ // ── GET /resource/:id ──────────────────────────────────────────────────
144
235
  app.get(`${routePath}/:id`, async (req, res) => {
145
236
  try {
146
- const item = await repository.findById(req.params.id);
147
- if (!item) {
148
- return res.status(404).json({ error: 'Not found' });
149
- }
150
- res.json(item);
237
+ const { select, with: withStr, withDeleted } = req.query;
238
+ const selectFields = select
239
+ ? select.split(',').map((f) => f.trim()).filter((f) => SAFE_KEY_RE.test(f))
240
+ : undefined;
241
+ const withRelations = withStr
242
+ ? withStr.split(',').map((r) => r.trim()).filter((r) => SAFE_KEY_RE.test(r)).slice(0, MAX_RELATIONS)
243
+ : undefined;
244
+ const item = await repository.findById(req.params.id, {
245
+ select: selectFields,
246
+ with: withRelations,
247
+ withDeleted: withDeleted === 'true',
248
+ });
249
+ if (!item)
250
+ return res.status(404).json(fail('NOT_FOUND', 'Resource not found'));
251
+ return res.json(ok(item));
151
252
  }
152
- catch (error) {
153
- res.status(500).json({ error: 'Internal server error' });
253
+ catch (err) {
254
+ this.logger?.error('GET by ID request failed', {
255
+ path: `${routePath}/:id`,
256
+ id: req.params.id,
257
+ correlationId: req.correlationId,
258
+ error: err instanceof Error ? err.message : String(err),
259
+ });
260
+ res.status(500).json(fail('INTERNAL_ERROR', 'Internal server error'));
154
261
  }
155
262
  });
156
- // POST /api/model - Create
263
+ // ── POST /resource ─────────────────────────────────────────────────────
157
264
  app.post(routePath, async (req, res) => {
158
265
  try {
159
- // Gunakan partial validation schema instead of strict
160
- const validatedData = validationSchema.parse(req.body);
266
+ const validatedData = strictValidationSchema.parse(req.body);
161
267
  const item = await repository.create(validatedData);
162
- res.status(201).json(item);
268
+ return res.status(201).json(ok(item));
163
269
  }
164
270
  catch (error) {
165
271
  if (error instanceof zod_1.z.ZodError) {
166
- return res.status(400).json({ error: 'Validation error', details: error.issues });
272
+ return res.status(422).json(fail('VALIDATION_ERROR', 'Validation failed', zodDetails(error.issues)));
167
273
  }
168
- res.status(500).json({ error: 'Internal server error' });
274
+ this.logger?.error('POST request failed', {
275
+ path: routePath,
276
+ correlationId: req.correlationId,
277
+ error: error instanceof Error ? error.message : String(error),
278
+ });
279
+ res.status(500).json(fail('INTERNAL_ERROR', 'Internal server error'));
169
280
  }
170
281
  });
171
- // PUT /api/model/:id - Update (gunakan partial validation)
282
+ // ── PUT /resource/:id ──────────────────────────────────────────────────
172
283
  app.put(`${routePath}/:id`, async (req, res) => {
173
284
  try {
174
- const validationSchema = this.getValidationSchema(modelClass);
285
+ const validatedData = strictValidationSchema.parse(req.body);
286
+ const item = await repository.update(req.params.id, validatedData);
287
+ return res.json(ok(item));
288
+ }
289
+ catch (error) {
290
+ if (error instanceof repository_1.NotFoundError)
291
+ return res.status(404).json(fail('NOT_FOUND', error.message));
292
+ if (error instanceof zod_1.z.ZodError) {
293
+ return res.status(422).json(fail('VALIDATION_ERROR', 'Validation failed', zodDetails(error.issues)));
294
+ }
295
+ this.logger?.error('PUT request failed', {
296
+ path: `${routePath}/:id`,
297
+ id: req.params.id,
298
+ correlationId: req.correlationId,
299
+ error: error instanceof Error ? error.message : String(error),
300
+ });
301
+ res.status(500).json(fail('INTERNAL_ERROR', 'Internal server error'));
302
+ }
303
+ });
304
+ // ── PATCH /resource/:id ── partial update ─────────────────────────────
305
+ app.patch(`${routePath}/:id`, async (req, res) => {
306
+ try {
175
307
  const validatedData = validationSchema.parse(req.body);
176
308
  const item = await repository.update(req.params.id, validatedData);
177
- res.json(item);
309
+ return res.json(ok(item));
178
310
  }
179
311
  catch (error) {
312
+ if (error instanceof repository_1.NotFoundError)
313
+ return res.status(404).json(fail('NOT_FOUND', error.message));
180
314
  if (error instanceof zod_1.z.ZodError) {
181
- return res.status(400).json({ error: 'Validation error', details: error.issues });
315
+ return res.status(422).json(fail('VALIDATION_ERROR', 'Validation failed', zodDetails(error.issues)));
182
316
  }
183
- res.status(500).json({ error: 'Internal server error' });
317
+ this.logger?.error('PATCH request failed', {
318
+ path: `${routePath}/:id`,
319
+ id: req.params.id,
320
+ correlationId: req.correlationId,
321
+ error: error instanceof Error ? error.message : String(error),
322
+ });
323
+ res.status(500).json(fail('INTERNAL_ERROR', 'Internal server error'));
184
324
  }
185
325
  });
186
- // DELETE /api/model/:id - Delete
326
+ // ── DELETE /resource/:id ───────────────────────────────────────────────
187
327
  app.delete(`${routePath}/:id`, async (req, res) => {
188
328
  try {
189
329
  await repository.delete(req.params.id);
190
- res.status(204).send();
330
+ return res.status(204).send();
191
331
  }
192
332
  catch (error) {
193
- res.status(500).json({ error: 'Internal server error' });
333
+ if (error instanceof repository_1.NotFoundError)
334
+ return res.status(404).json(fail('NOT_FOUND', error.message));
335
+ this.logger?.error('DELETE request failed', {
336
+ path: `${routePath}/:id`,
337
+ id: req.params.id,
338
+ correlationId: req.correlationId,
339
+ error: error instanceof Error ? error.message : String(error),
340
+ });
341
+ res.status(500).json(fail('INTERNAL_ERROR', 'Internal server error'));
194
342
  }
195
343
  });
196
344
  }
197
- // Generate custom routes
345
+ // Custom routes
198
346
  for (const route of this.customRoutes) {
199
347
  const fullPath = `${basePath}${route.path}`;
200
- switch (route.method.toLowerCase()) {
348
+ const { method, handlers } = route;
349
+ switch (method.toLowerCase()) {
201
350
  case 'get':
202
- app.get(fullPath, route.handler);
351
+ app.get(fullPath, ...handlers);
203
352
  break;
204
353
  case 'post':
205
- app.post(fullPath, route.handler);
354
+ app.post(fullPath, ...handlers);
206
355
  break;
207
356
  case 'put':
208
- app.put(fullPath, route.handler);
357
+ app.put(fullPath, ...handlers);
209
358
  break;
210
359
  case 'delete':
211
- app.delete(fullPath, route.handler);
360
+ app.delete(fullPath, ...handlers);
212
361
  break;
213
362
  case 'patch':
214
- app.patch(fullPath, route.handler);
363
+ app.patch(fullPath, ...handlers);
215
364
  break;
216
365
  }
217
366
  }