@aruvili/api 0.1.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/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +34 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +3 -0
- package/dist/context.js.map +1 -0
- package/dist/controllers/index.d.ts +39 -0
- package/dist/controllers/index.d.ts.map +1 -0
- package/dist/controllers/index.js +39 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/db.d.ts +6 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +74 -0
- package/dist/db.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +15 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +93 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/body-limit.d.ts +9 -0
- package/dist/middleware/body-limit.d.ts.map +1 -0
- package/dist/middleware/body-limit.js +15 -0
- package/dist/middleware/body-limit.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +6 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +40 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/rbac.d.ts +10 -0
- package/dist/middleware/rbac.d.ts.map +1 -0
- package/dist/middleware/rbac.js +61 -0
- package/dist/middleware/rbac.js.map +1 -0
- package/dist/middleware/tenant.d.ts +3 -0
- package/dist/middleware/tenant.d.ts.map +1 -0
- package/dist/middleware/tenant.js +19 -0
- package/dist/middleware/tenant.js.map +1 -0
- package/dist/registry.d.ts +26 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +112 -0
- package/dist/registry.js.map +1 -0
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +141 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/crud.d.ts +7 -0
- package/dist/routes/crud.d.ts.map +1 -0
- package/dist/routes/crud.js +845 -0
- package/dist/routes/crud.js.map +1 -0
- package/dist/routes/files.d.ts +7 -0
- package/dist/routes/files.d.ts.map +1 -0
- package/dist/routes/files.js +123 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/meta.d.ts +3 -0
- package/dist/routes/meta.d.ts.map +1 -0
- package/dist/routes/meta.js +352 -0
- package/dist/routes/meta.js.map +1 -0
- package/dist/scheduler.d.ts +33 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +97 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/utils/link-validator.d.ts +7 -0
- package/dist/utils/link-validator.d.ts.map +1 -0
- package/dist/utils/link-validator.js +33 -0
- package/dist/utils/link-validator.js.map +1 -0
- package/dist/utils/resolver.d.ts +5 -0
- package/dist/utils/resolver.d.ts.map +1 -0
- package/dist/utils/resolver.js +58 -0
- package/dist/utils/resolver.js.map +1 -0
- package/package.json +24 -0
- package/src/api.test.ts +362 -0
- package/src/config.d.ts +22 -0
- package/src/config.d.ts.map +1 -0
- package/src/config.js +34 -0
- package/src/config.js.map +1 -0
- package/src/config.ts +38 -0
- package/src/context.d.ts +7 -0
- package/src/context.d.ts.map +1 -0
- package/src/context.js +3 -0
- package/src/context.js.map +1 -0
- package/src/context.ts +8 -0
- package/src/controllers/index.d.ts +39 -0
- package/src/controllers/index.d.ts.map +1 -0
- package/src/controllers/index.js +39 -0
- package/src/controllers/index.js.map +1 -0
- package/src/controllers/index.ts +51 -0
- package/src/db.d.ts +6 -0
- package/src/db.d.ts.map +1 -0
- package/src/db.js +74 -0
- package/src/db.js.map +1 -0
- package/src/db.ts +73 -0
- package/src/index.ts +178 -0
- package/src/integration.test.ts +453 -0
- package/src/middleware/auth.d.ts +15 -0
- package/src/middleware/auth.d.ts.map +1 -0
- package/src/middleware/auth.js +93 -0
- package/src/middleware/auth.js.map +1 -0
- package/src/middleware/auth.ts +109 -0
- package/src/middleware/body-limit.d.ts +9 -0
- package/src/middleware/body-limit.d.ts.map +1 -0
- package/src/middleware/body-limit.js +15 -0
- package/src/middleware/body-limit.js.map +1 -0
- package/src/middleware/body-limit.ts +16 -0
- package/src/middleware/rate-limit.d.ts +6 -0
- package/src/middleware/rate-limit.d.ts.map +1 -0
- package/src/middleware/rate-limit.js +40 -0
- package/src/middleware/rate-limit.js.map +1 -0
- package/src/middleware/rate-limit.ts +47 -0
- package/src/middleware/rbac.d.ts +10 -0
- package/src/middleware/rbac.d.ts.map +1 -0
- package/src/middleware/rbac.js +61 -0
- package/src/middleware/rbac.js.map +1 -0
- package/src/middleware/rbac.ts +71 -0
- package/src/middleware/tenant.d.ts +3 -0
- package/src/middleware/tenant.d.ts.map +1 -0
- package/src/middleware/tenant.js +19 -0
- package/src/middleware/tenant.js.map +1 -0
- package/src/middleware/tenant.ts +24 -0
- package/src/registry.d.ts +26 -0
- package/src/registry.d.ts.map +1 -0
- package/src/registry.js +112 -0
- package/src/registry.js.map +1 -0
- package/src/registry.ts +123 -0
- package/src/routes/auth.d.ts +3 -0
- package/src/routes/auth.d.ts.map +1 -0
- package/src/routes/auth.js +141 -0
- package/src/routes/auth.js.map +1 -0
- package/src/routes/auth.ts +164 -0
- package/src/routes/crud.d.ts +7 -0
- package/src/routes/crud.d.ts.map +1 -0
- package/src/routes/crud.js +845 -0
- package/src/routes/crud.js.map +1 -0
- package/src/routes/crud.ts +1029 -0
- package/src/routes/files.d.ts +7 -0
- package/src/routes/files.d.ts.map +1 -0
- package/src/routes/files.js +123 -0
- package/src/routes/files.js.map +1 -0
- package/src/routes/files.ts +143 -0
- package/src/routes/meta.d.ts +3 -0
- package/src/routes/meta.d.ts.map +1 -0
- package/src/routes/meta.js +352 -0
- package/src/routes/meta.js.map +1 -0
- package/src/routes/meta.ts +448 -0
- package/src/scheduler.ts +118 -0
- package/src/utils/link-validator.d.ts +7 -0
- package/src/utils/link-validator.d.ts.map +1 -0
- package/src/utils/link-validator.js +33 -0
- package/src/utils/link-validator.js.map +1 -0
- package/src/utils/link-validator.ts +45 -0
- package/src/utils/resolver.d.ts +5 -0
- package/src/utils/resolver.d.ts.map +1 -0
- package/src/utils/resolver.js +58 -0
- package/src/utils/resolver.js.map +1 -0
- package/src/utils/resolver.ts +65 -0
- package/tsconfig.json +9 -0
package/src/config.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Framework - Centralized Configuration Module
|
|
3
|
+
* Validates all required environment variables at startup.
|
|
4
|
+
*/
|
|
5
|
+
export declare const config: {
|
|
6
|
+
port: number;
|
|
7
|
+
nodeEnv: string;
|
|
8
|
+
isProduction: boolean;
|
|
9
|
+
databaseUrl: string;
|
|
10
|
+
dbPoolMax: number;
|
|
11
|
+
dbIdleTimeout: number;
|
|
12
|
+
dbConnectTimeout: number;
|
|
13
|
+
dbStatementTimeout: string;
|
|
14
|
+
jwtSecret: string;
|
|
15
|
+
jwtExpiresIn: number;
|
|
16
|
+
bcryptRounds: number;
|
|
17
|
+
corsOrigins: string[];
|
|
18
|
+
rateLimitMax: number;
|
|
19
|
+
rateLimitWindowMs: number;
|
|
20
|
+
bodyLimitBytes: number;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;CAuBlB,CAAC"}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Framework - Centralized Configuration Module
|
|
3
|
+
* Validates all required environment variables at startup.
|
|
4
|
+
*/
|
|
5
|
+
function requireEnv(key, fallback) {
|
|
6
|
+
const value = process.env[key] || fallback;
|
|
7
|
+
if (!value && process.env.NODE_ENV === 'production') {
|
|
8
|
+
console.error(`[CONFIG FATAL] Required environment variable ${key} is not set.`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
return value || '';
|
|
12
|
+
}
|
|
13
|
+
export const config = {
|
|
14
|
+
// Server
|
|
15
|
+
port: parseInt(process.env.PORT || '3000'),
|
|
16
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
17
|
+
isProduction: process.env.NODE_ENV === 'production',
|
|
18
|
+
// Database
|
|
19
|
+
databaseUrl: requireEnv('DATABASE_URL', 'postgresql://postgres:postgres@localhost:5432/meta_db'),
|
|
20
|
+
dbPoolMax: parseInt(process.env.DB_POOL_MAX || '20'),
|
|
21
|
+
dbIdleTimeout: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'),
|
|
22
|
+
dbConnectTimeout: parseInt(process.env.DB_CONNECT_TIMEOUT || '5000'),
|
|
23
|
+
dbStatementTimeout: process.env.DB_STATEMENT_TIMEOUT || '30000',
|
|
24
|
+
// Auth
|
|
25
|
+
jwtSecret: requireEnv('JWT_SECRET', 'dev-secret-change-in-production'),
|
|
26
|
+
jwtExpiresIn: parseInt(process.env.JWT_EXPIRES_IN || '86400'), // 24 hours
|
|
27
|
+
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS || '12'),
|
|
28
|
+
// Security
|
|
29
|
+
corsOrigins: (process.env.CORS_ORIGINS || '*').split(',').map(s => s.trim()),
|
|
30
|
+
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '100'),
|
|
31
|
+
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'),
|
|
32
|
+
bodyLimitBytes: parseInt(process.env.BODY_LIMIT_BYTES || '1048576'), // 1MB
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,SAAS,UAAU,CAAC,GAAW,EAAE,QAAiB;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC;IAC3C,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;QACpD,OAAO,CAAC,KAAK,CAAC,gDAAgD,GAAG,cAAc,CAAC,CAAC;QACjF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,IAAI,EAAE,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,SAAS;IACT,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAC;IAC1C,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa;IAC9C,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;IAEnD,WAAW;IACX,WAAW,EAAE,UAAU,CAAC,cAAc,EAAE,uDAAuD,CAAC;IAChG,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;IACpD,aAAa,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC;IAC/D,gBAAgB,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,MAAM,CAAC;IACpE,kBAAkB,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO;IAE/D,OAAO;IACP,SAAS,EAAE,UAAU,CAAC,YAAY,EAAE,iCAAiC,CAAC;IACtE,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,EAAE,WAAW;IAC1E,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC;IAEzD,WAAW;IACX,WAAW,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5E,YAAY,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,KAAK,CAAC;IAC3D,iBAAiB,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO,CAAC;IACxE,cAAc,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,SAAS,CAAC,EAAE,MAAM;CAC5E,CAAC"}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta Framework - Centralized Configuration Module
|
|
3
|
+
* Validates all required environment variables at startup.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function requireEnv(key: string, fallback?: string): string {
|
|
7
|
+
const value = process.env[key] || fallback;
|
|
8
|
+
if (!value && process.env.NODE_ENV === 'production') {
|
|
9
|
+
console.error(`[CONFIG FATAL] Required environment variable ${key} is not set.`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
return value || '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const config = {
|
|
16
|
+
// Server
|
|
17
|
+
port: parseInt(process.env.PORT || '3000'),
|
|
18
|
+
nodeEnv: process.env.NODE_ENV || 'development',
|
|
19
|
+
isProduction: process.env.NODE_ENV === 'production',
|
|
20
|
+
|
|
21
|
+
// Database
|
|
22
|
+
databaseUrl: requireEnv('DATABASE_URL', 'postgresql://postgres:postgres@localhost:5432/meta_db'),
|
|
23
|
+
dbPoolMax: parseInt(process.env.DB_POOL_MAX || '20'),
|
|
24
|
+
dbIdleTimeout: parseInt(process.env.DB_IDLE_TIMEOUT || '30000'),
|
|
25
|
+
dbConnectTimeout: parseInt(process.env.DB_CONNECT_TIMEOUT || '5000'),
|
|
26
|
+
dbStatementTimeout: process.env.DB_STATEMENT_TIMEOUT || '30000',
|
|
27
|
+
|
|
28
|
+
// Auth
|
|
29
|
+
jwtSecret: requireEnv('JWT_SECRET', 'dev-secret-change-in-production'),
|
|
30
|
+
jwtExpiresIn: parseInt(process.env.JWT_EXPIRES_IN || '86400'), // 24 hours
|
|
31
|
+
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS || '12'),
|
|
32
|
+
|
|
33
|
+
// Security
|
|
34
|
+
corsOrigins: (process.env.CORS_ORIGINS || '*').split(',').map(s => s.trim()),
|
|
35
|
+
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '100'),
|
|
36
|
+
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '60000'),
|
|
37
|
+
bodyLimitBytes: parseInt(process.env.BODY_LIMIT_BYTES || '1048576'), // 1MB
|
|
38
|
+
};
|
package/src/context.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,eAAO,MAAM,cAAc,wCAA+C,CAAC"}
|
package/src/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOhD,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,iBAAiB,EAAuB,CAAC"}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
export interface ControllerContext {
|
|
3
|
+
db: pg.PoolClient;
|
|
4
|
+
user: {
|
|
5
|
+
email: string;
|
|
6
|
+
roles: string[];
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare abstract class DocTypeController {
|
|
10
|
+
before_insert(doc: any, ctx: ControllerContext): Promise<void>;
|
|
11
|
+
after_insert(doc: any, ctx: ControllerContext): Promise<void>;
|
|
12
|
+
before_save(doc: any, ctx: ControllerContext): Promise<void>;
|
|
13
|
+
on_update(doc: any, oldDoc: any, ctx: ControllerContext): Promise<void>;
|
|
14
|
+
before_delete(doc: any, ctx: ControllerContext): Promise<void>;
|
|
15
|
+
on_trash(doc: any, ctx: ControllerContext): Promise<void>;
|
|
16
|
+
before_submit(doc: any, ctx: ControllerContext): Promise<void>;
|
|
17
|
+
on_submit(doc: any, ctx: ControllerContext): Promise<void>;
|
|
18
|
+
before_cancel(doc: any, ctx: ControllerContext): Promise<void>;
|
|
19
|
+
on_cancel(doc: any, ctx: ControllerContext): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
declare class ControllerRegistry {
|
|
22
|
+
private controllers;
|
|
23
|
+
register(doctypeName: string, controller: DocTypeController): void;
|
|
24
|
+
get(doctypeName: string): DocTypeController | undefined;
|
|
25
|
+
private trigger;
|
|
26
|
+
triggerBeforeInsert(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
27
|
+
triggerAfterInsert(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
28
|
+
triggerBeforeSave(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
29
|
+
triggerOnUpdate(n: string, doc: any, old: any, ctx: ControllerContext): Promise<void>;
|
|
30
|
+
triggerBeforeDelete(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
31
|
+
triggerOnTrash(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
32
|
+
triggerBeforeSubmit(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
33
|
+
triggerOnSubmit(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
34
|
+
triggerBeforeCancel(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
35
|
+
triggerOnCancel(n: string, doc: any, ctx: ControllerContext): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
export declare const controllerRegistry: ControllerRegistry;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC;IAClB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;CAC1C;AAED,8BAAsB,iBAAiB;IAC/B,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9D,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC7D,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC5D,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IACvE,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9D,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IACzD,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9D,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAC9D,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;CACjE;AAED,cAAM,kBAAkB;IACtB,OAAO,CAAC,WAAW,CAAwC;IAEpD,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB;IAI3D,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;YAIhD,OAAO;IAOf,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC/D,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC9D,iBAAiB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC7D,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IACrE,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC/D,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC1D,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC/D,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC3D,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;IAC/D,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB;CAClE;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export class DocTypeController {
|
|
2
|
+
async before_insert(doc, ctx) { }
|
|
3
|
+
async after_insert(doc, ctx) { }
|
|
4
|
+
async before_save(doc, ctx) { }
|
|
5
|
+
async on_update(doc, oldDoc, ctx) { }
|
|
6
|
+
async before_delete(doc, ctx) { }
|
|
7
|
+
async on_trash(doc, ctx) { }
|
|
8
|
+
async before_submit(doc, ctx) { }
|
|
9
|
+
async on_submit(doc, ctx) { }
|
|
10
|
+
async before_cancel(doc, ctx) { }
|
|
11
|
+
async on_cancel(doc, ctx) { }
|
|
12
|
+
}
|
|
13
|
+
class ControllerRegistry {
|
|
14
|
+
controllers = new Map();
|
|
15
|
+
register(doctypeName, controller) {
|
|
16
|
+
this.controllers.set(doctypeName, controller);
|
|
17
|
+
}
|
|
18
|
+
get(doctypeName) {
|
|
19
|
+
return this.controllers.get(doctypeName);
|
|
20
|
+
}
|
|
21
|
+
async trigger(name, method, ...args) {
|
|
22
|
+
const ctrl = this.get(name);
|
|
23
|
+
if (ctrl && typeof ctrl[method] === 'function') {
|
|
24
|
+
await ctrl[method](...args);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async triggerBeforeInsert(n, doc, ctx) { await this.trigger(n, 'before_insert', doc, ctx); }
|
|
28
|
+
async triggerAfterInsert(n, doc, ctx) { await this.trigger(n, 'after_insert', doc, ctx); }
|
|
29
|
+
async triggerBeforeSave(n, doc, ctx) { await this.trigger(n, 'before_save', doc, ctx); }
|
|
30
|
+
async triggerOnUpdate(n, doc, old, ctx) { await this.trigger(n, 'on_update', doc, old, ctx); }
|
|
31
|
+
async triggerBeforeDelete(n, doc, ctx) { await this.trigger(n, 'before_delete', doc, ctx); }
|
|
32
|
+
async triggerOnTrash(n, doc, ctx) { await this.trigger(n, 'on_trash', doc, ctx); }
|
|
33
|
+
async triggerBeforeSubmit(n, doc, ctx) { await this.trigger(n, 'before_submit', doc, ctx); }
|
|
34
|
+
async triggerOnSubmit(n, doc, ctx) { await this.trigger(n, 'on_submit', doc, ctx); }
|
|
35
|
+
async triggerBeforeCancel(n, doc, ctx) { await this.trigger(n, 'before_cancel', doc, ctx); }
|
|
36
|
+
async triggerOnCancel(n, doc, ctx) { await this.trigger(n, 'on_cancel', doc, ctx); }
|
|
37
|
+
}
|
|
38
|
+
export const controllerRegistry = new ControllerRegistry();
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
|
|
3
|
+
export interface ControllerContext {
|
|
4
|
+
db: pg.PoolClient;
|
|
5
|
+
user: { email: string; roles: string[] };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export abstract class DocTypeController {
|
|
9
|
+
async before_insert(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
10
|
+
async after_insert(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
11
|
+
async before_save(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
12
|
+
async on_update(doc: any, oldDoc: any, ctx: ControllerContext): Promise<void> {}
|
|
13
|
+
async before_delete(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
14
|
+
async on_trash(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
15
|
+
async before_submit(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
16
|
+
async on_submit(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
17
|
+
async before_cancel(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
18
|
+
async on_cancel(doc: any, ctx: ControllerContext): Promise<void> {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class ControllerRegistry {
|
|
22
|
+
private controllers = new Map<string, DocTypeController>();
|
|
23
|
+
|
|
24
|
+
public register(doctypeName: string, controller: DocTypeController) {
|
|
25
|
+
this.controllers.set(doctypeName, controller);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public get(doctypeName: string): DocTypeController | undefined {
|
|
29
|
+
return this.controllers.get(doctypeName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async trigger(name: string, method: string, ...args: any[]) {
|
|
33
|
+
const ctrl = this.get(name);
|
|
34
|
+
if (ctrl && typeof (ctrl as any)[method] === 'function') {
|
|
35
|
+
await (ctrl as any)[method](...args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async triggerBeforeInsert(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'before_insert', doc, ctx); }
|
|
40
|
+
async triggerAfterInsert(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'after_insert', doc, ctx); }
|
|
41
|
+
async triggerBeforeSave(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'before_save', doc, ctx); }
|
|
42
|
+
async triggerOnUpdate(n: string, doc: any, old: any, ctx: ControllerContext) { await this.trigger(n, 'on_update', doc, old, ctx); }
|
|
43
|
+
async triggerBeforeDelete(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'before_delete', doc, ctx); }
|
|
44
|
+
async triggerOnTrash(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'on_trash', doc, ctx); }
|
|
45
|
+
async triggerBeforeSubmit(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'before_submit', doc, ctx); }
|
|
46
|
+
async triggerOnSubmit(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'on_submit', doc, ctx); }
|
|
47
|
+
async triggerBeforeCancel(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'before_cancel', doc, ctx); }
|
|
48
|
+
async triggerOnCancel(n: string, doc: any, ctx: ControllerContext) { await this.trigger(n, 'on_cancel', doc, ctx); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const controllerRegistry = new ControllerRegistry();
|
package/src/db.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
export declare const pool: import("pg").Pool;
|
|
3
|
+
export declare function query(text: string, params?: any[]): Promise<any>;
|
|
4
|
+
export declare function withTransaction<T>(callback: (client: pg.PoolClient) => Promise<T>): Promise<T>;
|
|
5
|
+
export declare function closePool(): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=db.d.ts.map
|
package/src/db.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,eAAO,MAAM,IAAI,mBAMf,CAAC;AAMH,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,gBA2BvD;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAuBpG;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C"}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
import { requestContext } from './context.js';
|
|
4
|
+
export const pool = new pg.Pool({
|
|
5
|
+
connectionString: config.databaseUrl,
|
|
6
|
+
max: config.dbPoolMax,
|
|
7
|
+
idleTimeoutMillis: config.dbIdleTimeout,
|
|
8
|
+
connectionTimeoutMillis: config.dbConnectTimeout,
|
|
9
|
+
allowExitOnIdle: false
|
|
10
|
+
});
|
|
11
|
+
pool.on('error', (err) => {
|
|
12
|
+
console.error('[DB POOL ERROR] Unexpected error on idle client:', err);
|
|
13
|
+
});
|
|
14
|
+
export async function query(text, params) {
|
|
15
|
+
if (globalThis.dbMock) {
|
|
16
|
+
return globalThis.dbMock.query(text, params);
|
|
17
|
+
}
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
const client = await pool.connect();
|
|
20
|
+
try {
|
|
21
|
+
const store = requestContext.getStore();
|
|
22
|
+
const tenantId = store?.tenantId || 'public';
|
|
23
|
+
await client.query(`SET search_path TO ${tenantId}, public`);
|
|
24
|
+
const result = await client.query(text, params);
|
|
25
|
+
const duration = Date.now() - start;
|
|
26
|
+
if (duration > 1000) {
|
|
27
|
+
console.warn(`[DB SLOW QUERY] ${duration}ms: ${text.substring(0, 120)}...`);
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const duration = Date.now() - start;
|
|
33
|
+
console.error(`[DB QUERY ERROR] ${duration}ms | ${text.substring(0, 120)} | ${error.message}`);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
try {
|
|
38
|
+
await client.query('RESET search_path');
|
|
39
|
+
}
|
|
40
|
+
catch (_) { }
|
|
41
|
+
client.release();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function withTransaction(callback) {
|
|
45
|
+
if (globalThis.dbMock) {
|
|
46
|
+
return globalThis.dbMock.withTransaction(callback);
|
|
47
|
+
}
|
|
48
|
+
const client = await pool.connect();
|
|
49
|
+
try {
|
|
50
|
+
const store = requestContext.getStore();
|
|
51
|
+
const tenantId = store?.tenantId || 'public';
|
|
52
|
+
await client.query('BEGIN');
|
|
53
|
+
await client.query(`SET search_path TO ${tenantId}, public`);
|
|
54
|
+
await client.query(`SET LOCAL statement_timeout = '${config.dbStatementTimeout}'`);
|
|
55
|
+
const result = await callback(client);
|
|
56
|
+
await client.query('COMMIT');
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
await client.query('ROLLBACK');
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
try {
|
|
65
|
+
await client.query('RESET search_path');
|
|
66
|
+
}
|
|
67
|
+
catch (_) { }
|
|
68
|
+
client.release();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export async function closePool() {
|
|
72
|
+
await pool.end();
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=db.js.map
|
package/src/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["db.ts"],"names":[],"mappings":"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"}
|
package/src/db.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
import { requestContext } from './context.js';
|
|
4
|
+
|
|
5
|
+
export const pool = new pg.Pool({
|
|
6
|
+
connectionString: config.databaseUrl,
|
|
7
|
+
max: config.dbPoolMax,
|
|
8
|
+
idleTimeoutMillis: config.dbIdleTimeout,
|
|
9
|
+
connectionTimeoutMillis: config.dbConnectTimeout,
|
|
10
|
+
allowExitOnIdle: false
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
pool.on('error', (err) => {
|
|
14
|
+
console.error('[DB POOL ERROR] Unexpected error on idle client:', err);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export async function query(text: string, params?: any[]) {
|
|
18
|
+
if ((globalThis as any).dbMock) {
|
|
19
|
+
return (globalThis as any).dbMock.query(text, params);
|
|
20
|
+
}
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
const client = await pool.connect();
|
|
23
|
+
try {
|
|
24
|
+
const store = requestContext.getStore();
|
|
25
|
+
const tenantId = store?.tenantId || 'public';
|
|
26
|
+
await client.query(`SET search_path TO ${tenantId}, public`);
|
|
27
|
+
|
|
28
|
+
const result = await client.query(text, params);
|
|
29
|
+
const duration = Date.now() - start;
|
|
30
|
+
if (duration > 1000) {
|
|
31
|
+
console.warn(`[DB SLOW QUERY] ${duration}ms: ${text.substring(0, 120)}...`);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
const duration = Date.now() - start;
|
|
36
|
+
console.error(`[DB QUERY ERROR] ${duration}ms | ${text.substring(0, 120)} | ${error.message}`);
|
|
37
|
+
throw error;
|
|
38
|
+
} finally {
|
|
39
|
+
try {
|
|
40
|
+
await client.query('RESET search_path');
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
client.release();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function withTransaction<T>(callback: (client: pg.PoolClient) => Promise<T>): Promise<T> {
|
|
47
|
+
if ((globalThis as any).dbMock) {
|
|
48
|
+
return (globalThis as any).dbMock.withTransaction(callback);
|
|
49
|
+
}
|
|
50
|
+
const client = await pool.connect();
|
|
51
|
+
try {
|
|
52
|
+
const store = requestContext.getStore();
|
|
53
|
+
const tenantId = store?.tenantId || 'public';
|
|
54
|
+
await client.query('BEGIN');
|
|
55
|
+
await client.query(`SET search_path TO ${tenantId}, public`);
|
|
56
|
+
await client.query(`SET LOCAL statement_timeout = '${config.dbStatementTimeout}'`);
|
|
57
|
+
const result = await callback(client);
|
|
58
|
+
await client.query('COMMIT');
|
|
59
|
+
return result;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
await client.query('ROLLBACK');
|
|
62
|
+
throw error;
|
|
63
|
+
} finally {
|
|
64
|
+
try {
|
|
65
|
+
await client.query('RESET search_path');
|
|
66
|
+
} catch (_) {}
|
|
67
|
+
client.release();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function closePool(): Promise<void> {
|
|
72
|
+
await pool.end();
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
import { config } from './config.js';
|
|
4
|
+
import { metaRouter } from './routes/meta.js';
|
|
5
|
+
import { crudRouter } from './routes/crud.js';
|
|
6
|
+
import { authRouter } from './routes/auth.js';
|
|
7
|
+
import { authMiddleware } from './middleware/auth.js';
|
|
8
|
+
import { rbacMiddleware } from './middleware/rbac.js';
|
|
9
|
+
import { rateLimitMiddleware } from './middleware/rate-limit.js';
|
|
10
|
+
import { bodyLimitMiddleware } from './middleware/body-limit.js';
|
|
11
|
+
import { query, closePool } from './db.js';
|
|
12
|
+
import { filesRouter } from './routes/files.js';
|
|
13
|
+
import { tenantMiddleware } from './middleware/tenant.js';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
|
|
16
|
+
const app = new Hono<{ Variables: { request_id: string; user?: any } }>();
|
|
17
|
+
|
|
18
|
+
// Global Middleware Stack
|
|
19
|
+
|
|
20
|
+
// CORS
|
|
21
|
+
app.use('*', cors({
|
|
22
|
+
origin: config.corsOrigins.includes('*') ? '*' : config.corsOrigins,
|
|
23
|
+
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
24
|
+
allowHeaders: ['Content-Type', 'Authorization'],
|
|
25
|
+
exposeHeaders: ['X-Request-ID', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
|
|
26
|
+
maxAge: 86400
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
// Request ID + structured logging
|
|
30
|
+
app.use('*', async (c, next) => {
|
|
31
|
+
const requestId = crypto.randomUUID();
|
|
32
|
+
c.set('request_id', requestId);
|
|
33
|
+
c.header('X-Request-ID', requestId);
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
await next();
|
|
36
|
+
console.log(JSON.stringify({
|
|
37
|
+
level: 'info', type: 'http', method: c.req.method, url: c.req.url,
|
|
38
|
+
status: c.res.status, duration_ms: Date.now() - start, request_id: requestId
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Tenant Isolation Context
|
|
43
|
+
app.use('*', tenantMiddleware);
|
|
44
|
+
|
|
45
|
+
// Body size limit
|
|
46
|
+
app.use('*', bodyLimitMiddleware);
|
|
47
|
+
|
|
48
|
+
// Rate limiting
|
|
49
|
+
app.use('*', rateLimitMiddleware);
|
|
50
|
+
|
|
51
|
+
// Error handler - hide details in production
|
|
52
|
+
app.onError((err, c) => {
|
|
53
|
+
const requestId = c.get('request_id') || crypto.randomUUID();
|
|
54
|
+
console.error(JSON.stringify({
|
|
55
|
+
level: 'error', type: 'exception', request_id: requestId,
|
|
56
|
+
message: err.message, stack: config.isProduction ? undefined : err.stack
|
|
57
|
+
}));
|
|
58
|
+
return c.json({
|
|
59
|
+
error: 'Internal Server Error', request_id: requestId,
|
|
60
|
+
...(config.isProduction ? {} : { message: err.message })
|
|
61
|
+
}, 500);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// === Public Routes (no auth required) ===
|
|
65
|
+
app.get('/api/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
66
|
+
app.route('/api/auth', authRouter);
|
|
67
|
+
app.route('/api/meta', metaRouter);
|
|
68
|
+
|
|
69
|
+
// === Protected Routes ===
|
|
70
|
+
app.use('/api/files/*', authMiddleware);
|
|
71
|
+
app.route('/api/files', filesRouter);
|
|
72
|
+
|
|
73
|
+
app.use('/api/doctype/*', authMiddleware, rbacMiddleware);
|
|
74
|
+
app.route('/api/doctype/:doctype', crudRouter);
|
|
75
|
+
|
|
76
|
+
// === Bootstrap ===
|
|
77
|
+
async function bootstrapDatabase() {
|
|
78
|
+
console.log('[BOOT] Initializing system database schemas...');
|
|
79
|
+
try {
|
|
80
|
+
// System tables
|
|
81
|
+
await query(`CREATE TABLE IF NOT EXISTS _tenants (
|
|
82
|
+
id VARCHAR(100) PRIMARY KEY,
|
|
83
|
+
name VARCHAR(255) NOT NULL,
|
|
84
|
+
domain VARCHAR(255),
|
|
85
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
86
|
+
);`);
|
|
87
|
+
|
|
88
|
+
await query(`CREATE TABLE IF NOT EXISTS _doctype_meta (
|
|
89
|
+
name VARCHAR(255) PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
90
|
+
definition JSONB NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
91
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());`);
|
|
92
|
+
|
|
93
|
+
await query(`CREATE TABLE IF NOT EXISTS _doctype_migration_history (
|
|
94
|
+
id SERIAL PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
95
|
+
doctype_name VARCHAR(255) NOT NULL, version INTEGER NOT NULL,
|
|
96
|
+
ddl_executed TEXT NOT NULL, executed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
97
|
+
executed_by VARCHAR(255));`);
|
|
98
|
+
|
|
99
|
+
await query(`CREATE TABLE IF NOT EXISTS _doctype_definition_history (
|
|
100
|
+
id SERIAL PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
101
|
+
doctype_name VARCHAR(255) REFERENCES _doctype_meta(name) ON DELETE CASCADE,
|
|
102
|
+
version INTEGER NOT NULL,
|
|
103
|
+
definition JSONB NOT NULL,
|
|
104
|
+
changed_by VARCHAR(255) NOT NULL,
|
|
105
|
+
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
106
|
+
ddl_applied TEXT,
|
|
107
|
+
notes TEXT);`);
|
|
108
|
+
|
|
109
|
+
await query(`CREATE TABLE IF NOT EXISTS _naming_series (
|
|
110
|
+
prefix VARCHAR(100) PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
111
|
+
current_value INTEGER NOT NULL DEFAULT 0);`);
|
|
112
|
+
|
|
113
|
+
await query(`CREATE TABLE IF NOT EXISTS _audit_log (
|
|
114
|
+
id BIGSERIAL PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
115
|
+
doctype_name VARCHAR(255) NOT NULL, docname VARCHAR(255) NOT NULL,
|
|
116
|
+
action VARCHAR(50) NOT NULL, changed_by VARCHAR(255) NOT NULL,
|
|
117
|
+
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), diff JSONB NOT NULL);`);
|
|
118
|
+
|
|
119
|
+
// User management table
|
|
120
|
+
await query(`CREATE TABLE IF NOT EXISTS _users (
|
|
121
|
+
name VARCHAR(255) PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
122
|
+
email VARCHAR(255) UNIQUE NOT NULL, full_name VARCHAR(255) DEFAULT '',
|
|
123
|
+
hashed_password TEXT NOT NULL, roles JSONB DEFAULT '["Guest"]',
|
|
124
|
+
enabled BOOLEAN DEFAULT true, last_login TIMESTAMP WITH TIME ZONE,
|
|
125
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
126
|
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());`);
|
|
127
|
+
|
|
128
|
+
// Files attachment tracking table
|
|
129
|
+
await query(`CREATE TABLE IF NOT EXISTS _files (
|
|
130
|
+
name VARCHAR(255) PRIMARY KEY, uuid UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
|
|
131
|
+
file_name VARCHAR(255) NOT NULL, file_url VARCHAR(255) NOT NULL,
|
|
132
|
+
file_size INTEGER NOT NULL, mime_type VARCHAR(100) NOT NULL,
|
|
133
|
+
attached_to_doctype VARCHAR(255), attached_to_name VARCHAR(255),
|
|
134
|
+
attached_to_field VARCHAR(255), created_by VARCHAR(255),
|
|
135
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());`);
|
|
136
|
+
|
|
137
|
+
// System indexes
|
|
138
|
+
await query(`CREATE INDEX IF NOT EXISTS idx_audit_log_doctype ON _audit_log (doctype_name, docname);`);
|
|
139
|
+
await query(`CREATE INDEX IF NOT EXISTS idx_audit_log_changed_at ON _audit_log (changed_at);`);
|
|
140
|
+
await query(`CREATE INDEX IF NOT EXISTS idx_users_email ON _users (email);`);
|
|
141
|
+
|
|
142
|
+
console.log('[BOOT] System database schemas initialized successfully.');
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error('[BOOT ERROR] Failed to initialize system schemas:', err);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bootstrapDatabase().then(() => {
|
|
150
|
+
console.log(`[BOOT] Server launching on http://localhost:${config.port}`);
|
|
151
|
+
if (!config.isProduction) {
|
|
152
|
+
console.log('[BOOT] Running in DEVELOPMENT mode. Set NODE_ENV=production for production.');
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Graceful shutdown
|
|
157
|
+
const shutdown = async () => {
|
|
158
|
+
console.log('[SHUTDOWN] Graceful shutdown initiated...');
|
|
159
|
+
await closePool();
|
|
160
|
+
process.exit(0);
|
|
161
|
+
};
|
|
162
|
+
process.on('SIGTERM', shutdown);
|
|
163
|
+
process.on('SIGINT', shutdown);
|
|
164
|
+
|
|
165
|
+
export default { port: config.port, fetch: app.fetch };
|
|
166
|
+
|
|
167
|
+
export { metaRouter } from './routes/meta.js';
|
|
168
|
+
export { crudRouter } from './routes/crud.js';
|
|
169
|
+
export { authRouter } from './routes/auth.js';
|
|
170
|
+
export { filesRouter } from './routes/files.js';
|
|
171
|
+
export { tenantMiddleware } from './middleware/tenant.js';
|
|
172
|
+
export { bodyLimitMiddleware } from './middleware/body-limit.js';
|
|
173
|
+
export { rateLimitMiddleware } from './middleware/rate-limit.js';
|
|
174
|
+
export { authMiddleware } from './middleware/auth.js';
|
|
175
|
+
export { rbacMiddleware } from './middleware/rbac.js';
|
|
176
|
+
export { query, withTransaction } from './db.js';
|
|
177
|
+
export { config } from './config.js';
|
|
178
|
+
|