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.
- package/LICENSE +21 -21
- package/README.md +554 -328
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/crudora.d.ts +34 -9
- package/dist/core/crudora.d.ts.map +1 -1
- package/dist/core/crudora.js +254 -105
- package/dist/core/crudora.js.map +1 -1
- package/dist/core/crudoraServer.d.ts +64 -10
- package/dist/core/crudoraServer.d.ts.map +1 -1
- package/dist/core/crudoraServer.js +138 -19
- package/dist/core/crudoraServer.js.map +1 -1
- package/dist/core/drizzleTableBuilder.d.ts +6 -0
- package/dist/core/drizzleTableBuilder.d.ts.map +1 -0
- package/dist/core/drizzleTableBuilder.js +175 -0
- package/dist/core/drizzleTableBuilder.js.map +1 -0
- package/dist/core/model.d.ts +28 -9
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/model.js +33 -70
- package/dist/core/model.js.map +1 -1
- package/dist/core/repository.d.ts +98 -14
- package/dist/core/repository.d.ts.map +1 -1
- package/dist/core/repository.js +561 -103
- package/dist/core/repository.js.map +1 -1
- package/dist/core/schemaGenerator.d.ts +3 -3
- package/dist/core/schemaGenerator.d.ts.map +1 -1
- package/dist/core/schemaGenerator.js +237 -32
- package/dist/core/schemaGenerator.js.map +1 -1
- package/dist/decorators/model.d.ts +56 -1
- package/dist/decorators/model.d.ts.map +1 -1
- package/dist/decorators/model.js +92 -0
- package/dist/decorators/model.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/scripts/copy-assets.js +47 -47
- package/dist/scripts/postinstall.js +172 -136
- package/dist/templates/.env.example +13 -9
- package/dist/templates/drizzle.config.ts +10 -0
- package/dist/templates/schema.ts +23 -0
- package/dist/types/logger.type.d.ts +7 -0
- package/dist/types/logger.type.d.ts.map +1 -0
- package/dist/types/logger.type.js +3 -0
- package/dist/types/logger.type.js.map +1 -0
- package/dist/types/model.type.d.ts +30 -5
- package/dist/types/model.type.d.ts.map +1 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +91 -19
- package/dist/utils/validation.js.map +1 -1
- package/package.json +108 -94
- package/scripts/copy-assets.js +47 -47
- package/scripts/postinstall.js +172 -136
- package/templates/.env.example +13 -9
- package/templates/drizzle.config.ts +10 -0
- package/templates/schema.ts +23 -0
- package/dist/templates/schema.prisma +0 -22
- 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
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/core/crudora.d.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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,
|
|
18
|
-
post(path: string,
|
|
19
|
-
put(path: string,
|
|
20
|
-
delete(path: string,
|
|
21
|
-
patch(path: string,
|
|
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,
|
|
1
|
+
{"version":3,"file":"crudora.d.ts","sourceRoot":"","sources":["../../src/core/crudora.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAiB,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA0BrD,qBAAa,OAAO;IAClB,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,MAAM,CAA4C;IAC1D,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,YAAY,CAA2C;IAC/D,OAAO,CAAC,YAAY,CAIZ;gBAEI,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,aAAa;IAqB7D,aAAa,CAAC,GAAG,YAAY,EAAE,gBAAgB,EAAE,GAAG,IAAI;IAYxD;;;;;;;OAOG;IACH,aAAa,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAQjF,aAAa,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC;IAQ9E;;;;;;;;OAQG;IACH,QAAQ,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,GAAG;IAQ/D,qBAAqB,IAAI,MAAM;IAK/B,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;IAIzF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI;IAKrF,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI;IAKtF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI;IAKrF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI;IAKxF,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,KAAK,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,IAAI;IAKvF,sEAAsE;IAChE,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAI7D,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAe,GAAG,IAAI;CAwP9D"}
|
package/dist/core/crudora.js
CHANGED
|
@@ -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(
|
|
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 (!
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
const repository = new repository_1.Repository(modelClass, this.
|
|
24
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
46
|
-
|
|
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,
|
|
51
|
-
this.customRoutes.push({ method: 'POST', path,
|
|
113
|
+
post(path, ...handlers) {
|
|
114
|
+
this.customRoutes.push({ method: 'POST', path, handlers });
|
|
52
115
|
return this;
|
|
53
116
|
}
|
|
54
|
-
put(path,
|
|
55
|
-
this.customRoutes.push({ method: 'PUT', path,
|
|
117
|
+
put(path, ...handlers) {
|
|
118
|
+
this.customRoutes.push({ method: 'PUT', path, handlers });
|
|
56
119
|
return this;
|
|
57
120
|
}
|
|
58
|
-
delete(path,
|
|
59
|
-
this.customRoutes.push({ method: 'DELETE', path,
|
|
121
|
+
delete(path, ...handlers) {
|
|
122
|
+
this.customRoutes.push({ method: 'DELETE', path, handlers });
|
|
60
123
|
return this;
|
|
61
124
|
}
|
|
62
|
-
patch(path,
|
|
63
|
-
this.customRoutes.push({ method: 'PATCH', path,
|
|
125
|
+
patch(path, ...handlers) {
|
|
126
|
+
this.customRoutes.push({ method: 'PATCH', path, handlers });
|
|
64
127
|
return this;
|
|
65
128
|
}
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
70
|
-
app.get(basePath, (
|
|
134
|
+
// Route discovery endpoint
|
|
135
|
+
app.get(basePath, (_req, res) => {
|
|
71
136
|
const routes = [];
|
|
72
|
-
|
|
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
|
-
//
|
|
114
|
-
for (const [
|
|
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 /
|
|
157
|
+
// ── GET /resource ──────────────────────────────────────────────────────
|
|
119
158
|
app.get(routePath, async (req, res) => {
|
|
120
159
|
try {
|
|
121
|
-
const { page
|
|
122
|
-
|
|
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:
|
|
126
|
-
where:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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 (
|
|
140
|
-
|
|
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 /
|
|
234
|
+
// ── GET /resource/:id ──────────────────────────────────────────────────
|
|
144
235
|
app.get(`${routePath}/:id`, async (req, res) => {
|
|
145
236
|
try {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 (
|
|
153
|
-
|
|
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 /
|
|
263
|
+
// ── POST /resource ─────────────────────────────────────────────────────
|
|
157
264
|
app.post(routePath, async (req, res) => {
|
|
158
265
|
try {
|
|
159
|
-
|
|
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(
|
|
272
|
+
return res.status(422).json(fail('VALIDATION_ERROR', 'Validation failed', zodDetails(error.issues)));
|
|
167
273
|
}
|
|
168
|
-
|
|
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 /
|
|
282
|
+
// ── PUT /resource/:id ──────────────────────────────────────────────────
|
|
172
283
|
app.put(`${routePath}/:id`, async (req, res) => {
|
|
173
284
|
try {
|
|
174
|
-
const
|
|
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(
|
|
315
|
+
return res.status(422).json(fail('VALIDATION_ERROR', 'Validation failed', zodDetails(error.issues)));
|
|
182
316
|
}
|
|
183
|
-
|
|
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 /
|
|
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
|
-
|
|
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
|
-
//
|
|
345
|
+
// Custom routes
|
|
198
346
|
for (const route of this.customRoutes) {
|
|
199
347
|
const fullPath = `${basePath}${route.path}`;
|
|
200
|
-
|
|
348
|
+
const { method, handlers } = route;
|
|
349
|
+
switch (method.toLowerCase()) {
|
|
201
350
|
case 'get':
|
|
202
|
-
app.get(fullPath,
|
|
351
|
+
app.get(fullPath, ...handlers);
|
|
203
352
|
break;
|
|
204
353
|
case 'post':
|
|
205
|
-
app.post(fullPath,
|
|
354
|
+
app.post(fullPath, ...handlers);
|
|
206
355
|
break;
|
|
207
356
|
case 'put':
|
|
208
|
-
app.put(fullPath,
|
|
357
|
+
app.put(fullPath, ...handlers);
|
|
209
358
|
break;
|
|
210
359
|
case 'delete':
|
|
211
|
-
app.delete(fullPath,
|
|
360
|
+
app.delete(fullPath, ...handlers);
|
|
212
361
|
break;
|
|
213
362
|
case 'patch':
|
|
214
|
-
app.patch(fullPath,
|
|
363
|
+
app.patch(fullPath, ...handlers);
|
|
215
364
|
break;
|
|
216
365
|
}
|
|
217
366
|
}
|