@ratim818/allyve-wellness-backend 1.0.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/.env.example +49 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/publish.yml +81 -0
- package/README.md +140 -0
- package/docs/HIPAA_COMPLIANCE.md +141 -0
- package/docs/frontend-api-client.ts +259 -0
- package/package.json +60 -0
- package/src/config/database.ts +45 -0
- package/src/config/index.ts +52 -0
- package/src/middleware/auth.ts +167 -0
- package/src/middleware/security.ts +101 -0
- package/src/migrations/rollback.ts +17 -0
- package/src/migrations/run.ts +17 -0
- package/src/migrations/schema.ts +339 -0
- package/src/migrations/seed.ts +159 -0
- package/src/routes/appointments.ts +293 -0
- package/src/routes/audit.ts +29 -0
- package/src/routes/auth.ts +141 -0
- package/src/routes/health.ts +387 -0
- package/src/server.ts +124 -0
- package/src/services/audit.ts +117 -0
- package/src/services/auth.ts +293 -0
- package/src/services/encryption.ts +76 -0
- package/src/utils/logger.ts +57 -0
- package/tsconfig.json +24 -0
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ratim818/allyve-wellness-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "HIPAA-compliant backend for Allyve Wellness AI — Maternal Health Monitoring Platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "tsx watch src/server.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/server.js",
|
|
10
|
+
"migrate": "tsx src/migrations/run.ts",
|
|
11
|
+
"migrate:down": "tsx src/migrations/rollback.ts",
|
|
12
|
+
"seed": "tsx src/migrations/seed.ts",
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"lint": "eslint src/"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"express": "^4.21.0",
|
|
18
|
+
"cors": "^2.8.5",
|
|
19
|
+
"helmet": "^7.1.0",
|
|
20
|
+
"compression": "^1.7.4",
|
|
21
|
+
"express-rate-limit": "^7.4.0",
|
|
22
|
+
"pg": "^8.13.0",
|
|
23
|
+
"knex": "^3.1.0",
|
|
24
|
+
"bcryptjs": "^2.4.3",
|
|
25
|
+
"jsonwebtoken": "^9.0.2",
|
|
26
|
+
"uuid": "^10.0.0",
|
|
27
|
+
"zod": "^3.23.8",
|
|
28
|
+
"dotenv": "^16.4.5",
|
|
29
|
+
"winston": "^3.14.2",
|
|
30
|
+
"crypto-js": "^4.2.0",
|
|
31
|
+
"express-validator": "^7.2.0",
|
|
32
|
+
"cookie-parser": "^1.4.6",
|
|
33
|
+
"hpp": "^0.2.3",
|
|
34
|
+
"express-session": "^1.18.0",
|
|
35
|
+
"connect-pg-simple": "^9.0.1",
|
|
36
|
+
"cron": "^3.1.7"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.5.4",
|
|
40
|
+
"tsx": "^4.19.0",
|
|
41
|
+
"@types/express": "^4.17.21",
|
|
42
|
+
"@types/cors": "^2.8.17",
|
|
43
|
+
"@types/bcryptjs": "^2.4.6",
|
|
44
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
45
|
+
"@types/uuid": "^10.0.0",
|
|
46
|
+
"@types/compression": "^1.7.5",
|
|
47
|
+
"@types/cookie-parser": "^1.4.7",
|
|
48
|
+
"@types/hpp": "^0.2.6",
|
|
49
|
+
"@types/pg": "^8.11.6",
|
|
50
|
+
"@types/express-session": "^1.18.0",
|
|
51
|
+
"@types/connect-pg-simple": "^7.0.3",
|
|
52
|
+
"@types/crypto-js": "^4.2.2",
|
|
53
|
+
"vitest": "^2.0.5",
|
|
54
|
+
"eslint": "^9.9.0"
|
|
55
|
+
},
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/ratim818/allyve-backend.git"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import knex, { Knex } from "knex";
|
|
2
|
+
import { config } from "../config/index.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
const knexConfig: Knex.Config = {
|
|
6
|
+
client: "pg",
|
|
7
|
+
connection: {
|
|
8
|
+
connectionString: config.db.url,
|
|
9
|
+
ssl: config.db.ssl ? { rejectUnauthorized: true } : false,
|
|
10
|
+
},
|
|
11
|
+
pool: {
|
|
12
|
+
min: config.db.poolMin,
|
|
13
|
+
max: config.db.poolMax,
|
|
14
|
+
// Destroy connections after 30 min idle (security best practice)
|
|
15
|
+
idleTimeoutMillis: 30000,
|
|
16
|
+
},
|
|
17
|
+
migrations: {
|
|
18
|
+
directory: "./src/migrations",
|
|
19
|
+
extension: "ts",
|
|
20
|
+
},
|
|
21
|
+
// Log slow queries (>1s) for performance monitoring
|
|
22
|
+
log: {
|
|
23
|
+
warn(msg: string) { logger.warn(`[DB] ${msg}`); },
|
|
24
|
+
error(msg: string) { logger.error(`[DB] ${msg}`); },
|
|
25
|
+
debug(msg: string) { if (config.env === "development") logger.debug(`[DB] ${msg}`); },
|
|
26
|
+
deprecate(msg: string) { logger.warn(`[DB DEPRECATE] ${msg}`); },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const db: Knex = knex(knexConfig);
|
|
31
|
+
|
|
32
|
+
export async function testConnection(): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
await db.raw("SELECT 1");
|
|
35
|
+
logger.info("✅ Database connection established");
|
|
36
|
+
} catch (err) {
|
|
37
|
+
logger.error("❌ Database connection failed:", err);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function closeConnection(): Promise<void> {
|
|
43
|
+
await db.destroy();
|
|
44
|
+
logger.info("Database connection closed");
|
|
45
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
dotenv.config();
|
|
3
|
+
|
|
4
|
+
function requireEnv(key: string): string {
|
|
5
|
+
const val = process.env[key];
|
|
6
|
+
if (!val) throw new Error(`Missing required env var: ${key}`);
|
|
7
|
+
return val;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function optionalEnv(key: string, fallback: string): string {
|
|
11
|
+
return process.env[key] || fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const config = {
|
|
15
|
+
env: optionalEnv("NODE_ENV", "development"),
|
|
16
|
+
port: parseInt(optionalEnv("PORT", "3001")),
|
|
17
|
+
apiVersion: optionalEnv("API_VERSION", "v1"),
|
|
18
|
+
corsOrigin: optionalEnv("CORS_ORIGIN", "http://localhost:5173"),
|
|
19
|
+
|
|
20
|
+
db: {
|
|
21
|
+
url: requireEnv("DATABASE_URL"),
|
|
22
|
+
ssl: optionalEnv("DATABASE_SSL", "false") === "true",
|
|
23
|
+
poolMin: parseInt(optionalEnv("DATABASE_POOL_MIN", "2")),
|
|
24
|
+
poolMax: parseInt(optionalEnv("DATABASE_POOL_MAX", "10")),
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
encryption: {
|
|
28
|
+
key: requireEnv("ENCRYPTION_KEY"),
|
|
29
|
+
ivLength: parseInt(optionalEnv("ENCRYPTION_IV_LENGTH", "16")),
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
jwt: {
|
|
33
|
+
secret: requireEnv("JWT_SECRET"),
|
|
34
|
+
expiresIn: optionalEnv("JWT_EXPIRES_IN", "15m"),
|
|
35
|
+
refreshExpiresIn: optionalEnv("JWT_REFRESH_EXPIRES_IN", "7d"),
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
session: {
|
|
39
|
+
secret: requireEnv("SESSION_SECRET"),
|
|
40
|
+
maxAge: parseInt(optionalEnv("SESSION_MAX_AGE", "900000")), // 15 min
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
audit: {
|
|
44
|
+
retentionDays: parseInt(optionalEnv("AUDIT_LOG_RETENTION_DAYS", "2190")), // 6 years HIPAA
|
|
45
|
+
logLevel: optionalEnv("AUDIT_LOG_LEVEL", "info"),
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
rateLimit: {
|
|
49
|
+
windowMs: parseInt(optionalEnv("RATE_LIMIT_WINDOW_MS", "900000")), // 15 min
|
|
50
|
+
maxRequests: parseInt(optionalEnv("RATE_LIMIT_MAX_REQUESTS", "100")),
|
|
51
|
+
},
|
|
52
|
+
} as const;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import jwt from "jsonwebtoken";
|
|
3
|
+
import { config } from "../config/index.js";
|
|
4
|
+
import { db } from "../config/database.js";
|
|
5
|
+
import { writeAuditLog } from "../services/audit.js";
|
|
6
|
+
import type { TokenPayload } from "../services/auth.js";
|
|
7
|
+
|
|
8
|
+
// Extend Express Request to include authenticated user
|
|
9
|
+
declare global {
|
|
10
|
+
namespace Express {
|
|
11
|
+
interface Request {
|
|
12
|
+
user?: TokenPayload;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Verify JWT and attach user to request.
|
|
19
|
+
* HIPAA §164.312(d) — Person or entity authentication
|
|
20
|
+
*/
|
|
21
|
+
export function authenticate(req: Request, res: Response, next: NextFunction) {
|
|
22
|
+
const authHeader = req.headers.authorization;
|
|
23
|
+
|
|
24
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
25
|
+
res.status(401).json({ error: "Authentication required" });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const token = authHeader.split(" ")[1];
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const decoded = jwt.verify(token, config.jwt.secret, {
|
|
33
|
+
issuer: "allyve-wellness",
|
|
34
|
+
audience: "allyve-frontend",
|
|
35
|
+
}) as TokenPayload;
|
|
36
|
+
|
|
37
|
+
req.user = decoded;
|
|
38
|
+
next();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err instanceof jwt.TokenExpiredError) {
|
|
41
|
+
res.status(401).json({ error: "Token expired", code: "TOKEN_EXPIRED" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
res.status(401).json({ error: "Invalid token" });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Verify the session is still valid (not revoked or expired).
|
|
50
|
+
* HIPAA §164.312(a)(2)(iii) — Automatic logoff
|
|
51
|
+
*/
|
|
52
|
+
export async function validateSession(req: Request, res: Response, next: NextFunction) {
|
|
53
|
+
if (!req.user) {
|
|
54
|
+
res.status(401).json({ error: "Authentication required" });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const session = await db("sessions")
|
|
60
|
+
.where("id", req.user.sessionId)
|
|
61
|
+
.where("revoked", false)
|
|
62
|
+
.where("expires_at", ">", new Date())
|
|
63
|
+
.first();
|
|
64
|
+
|
|
65
|
+
if (!session) {
|
|
66
|
+
await writeAuditLog({
|
|
67
|
+
user_id: req.user.userId,
|
|
68
|
+
action: "SESSION_TIMEOUT",
|
|
69
|
+
resource_type: "session",
|
|
70
|
+
resource_id: req.user.sessionId,
|
|
71
|
+
ip_address: req.ip,
|
|
72
|
+
outcome: "denied",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
res.status(401).json({ error: "Session expired or revoked", code: "SESSION_INVALID" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Extend session on activity (sliding window)
|
|
80
|
+
await db("sessions")
|
|
81
|
+
.where("id", req.user.sessionId)
|
|
82
|
+
.update({ expires_at: new Date(Date.now() + config.session.maxAge) });
|
|
83
|
+
|
|
84
|
+
next();
|
|
85
|
+
} catch (err) {
|
|
86
|
+
res.status(500).json({ error: "Session validation failed" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Role-based access control.
|
|
92
|
+
* HIPAA §164.312(a)(1) — Access control
|
|
93
|
+
*/
|
|
94
|
+
export function requireRole(...roles: string[]) {
|
|
95
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
96
|
+
if (!req.user) {
|
|
97
|
+
res.status(401).json({ error: "Authentication required" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!roles.includes(req.user.role)) {
|
|
102
|
+
writeAuditLog({
|
|
103
|
+
user_id: req.user.userId,
|
|
104
|
+
action: "VIEW",
|
|
105
|
+
resource_type: "user",
|
|
106
|
+
ip_address: req.ip,
|
|
107
|
+
outcome: "denied",
|
|
108
|
+
details: { requiredRoles: roles, userRole: req.user.role },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
res.status(403).json({ error: "Insufficient permissions" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
next();
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Ensure users can only access their own data.
|
|
121
|
+
* HIPAA §164.312(a)(1) — Access control (minimum necessary)
|
|
122
|
+
*/
|
|
123
|
+
export function requireOwnership(paramName: string = "userId") {
|
|
124
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
125
|
+
if (!req.user) {
|
|
126
|
+
res.status(401).json({ error: "Authentication required" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const targetUserId = req.params[paramName];
|
|
131
|
+
|
|
132
|
+
// Admins and providers can access other users' data (with audit)
|
|
133
|
+
if (req.user.role === "admin" || req.user.role === "provider") {
|
|
134
|
+
if (targetUserId && targetUserId !== req.user.userId) {
|
|
135
|
+
writeAuditLog({
|
|
136
|
+
user_id: req.user.userId,
|
|
137
|
+
action: "VIEW",
|
|
138
|
+
resource_type: "user",
|
|
139
|
+
resource_id: targetUserId,
|
|
140
|
+
details: { accessType: "cross_user", role: req.user.role },
|
|
141
|
+
ip_address: req.ip,
|
|
142
|
+
outcome: "success",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
next();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Patients can only access their own data
|
|
150
|
+
if (targetUserId && targetUserId !== req.user.userId) {
|
|
151
|
+
writeAuditLog({
|
|
152
|
+
user_id: req.user.userId,
|
|
153
|
+
action: "VIEW",
|
|
154
|
+
resource_type: "user",
|
|
155
|
+
resource_id: targetUserId,
|
|
156
|
+
ip_address: req.ip,
|
|
157
|
+
outcome: "denied",
|
|
158
|
+
details: { reason: "ownership_violation" },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
res.status(403).json({ error: "Access denied" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
next();
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import express, { Express, Request, Response, NextFunction } from "express";
|
|
2
|
+
import helmet from "helmet";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import compression from "compression";
|
|
5
|
+
import rateLimit from "express-rate-limit";
|
|
6
|
+
import hpp from "hpp";
|
|
7
|
+
import cookieParser from "cookie-parser";
|
|
8
|
+
import { config } from "../config/index.js";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Apply all HIPAA-required security middleware.
|
|
13
|
+
* References: HIPAA §164.312(a-e) Technical Safeguards
|
|
14
|
+
*/
|
|
15
|
+
export function applySecurityMiddleware(app: Express): void {
|
|
16
|
+
|
|
17
|
+
// ── Helmet: HTTP security headers ─────────────────────
|
|
18
|
+
// §164.312(e)(1) — Transmission security
|
|
19
|
+
app.use(helmet({
|
|
20
|
+
contentSecurityPolicy: {
|
|
21
|
+
directives: {
|
|
22
|
+
defaultSrc: ["'self'"],
|
|
23
|
+
scriptSrc: ["'self'"],
|
|
24
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
25
|
+
imgSrc: ["'self'", "data:", "blob:"],
|
|
26
|
+
connectSrc: ["'self'", config.corsOrigin],
|
|
27
|
+
frameSrc: ["'none'"],
|
|
28
|
+
objectSrc: ["'none'"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
hsts: {
|
|
32
|
+
maxAge: 31536000, // 1 year
|
|
33
|
+
includeSubDomains: true,
|
|
34
|
+
preload: true,
|
|
35
|
+
},
|
|
36
|
+
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
|
|
37
|
+
noSniff: true,
|
|
38
|
+
xssFilter: true,
|
|
39
|
+
frameguard: { action: "deny" },
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// ── CORS: restrict to known frontend origin ───────────
|
|
43
|
+
app.use(cors({
|
|
44
|
+
origin: config.corsOrigin,
|
|
45
|
+
credentials: true,
|
|
46
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
47
|
+
allowedHeaders: ["Content-Type", "Authorization"],
|
|
48
|
+
maxAge: 600, // Preflight cache: 10 min
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// ── Rate Limiting: prevent brute force ────────────────
|
|
52
|
+
// §164.312(a)(1) — Access control
|
|
53
|
+
const limiter = rateLimit({
|
|
54
|
+
windowMs: config.rateLimit.windowMs,
|
|
55
|
+
max: config.rateLimit.maxRequests,
|
|
56
|
+
message: { error: "Too many requests. Please try again later." },
|
|
57
|
+
standardHeaders: true,
|
|
58
|
+
legacyHeaders: false,
|
|
59
|
+
handler: (req, res) => {
|
|
60
|
+
logger.warn(`Rate limit exceeded: ${req.ip}`);
|
|
61
|
+
res.status(429).json({ error: "Too many requests. Please try again later." });
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
app.use(limiter);
|
|
65
|
+
|
|
66
|
+
// Stricter rate limit for auth endpoints
|
|
67
|
+
const authLimiter = rateLimit({
|
|
68
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
69
|
+
max: 10, // 10 login attempts per 15 min
|
|
70
|
+
message: { error: "Too many login attempts. Please try again later." },
|
|
71
|
+
});
|
|
72
|
+
app.use("/api/v1/auth/login", authLimiter);
|
|
73
|
+
app.use("/api/v1/auth/register", authLimiter);
|
|
74
|
+
|
|
75
|
+
// ── HTTP Parameter Pollution protection ────────────────
|
|
76
|
+
app.use(hpp());
|
|
77
|
+
|
|
78
|
+
// ── Cookie Parser ──────────────────────────────────────
|
|
79
|
+
app.use(cookieParser(config.session.secret));
|
|
80
|
+
|
|
81
|
+
// ── Compression ────────────────────────────────────────
|
|
82
|
+
app.use(compression());
|
|
83
|
+
|
|
84
|
+
// ── Body parsers with size limits ──────────────────────
|
|
85
|
+
app.use(express.json({ limit: "1mb" }));
|
|
86
|
+
app.use(express.urlencoded({ extended: false, limit: "1mb" }));
|
|
87
|
+
|
|
88
|
+
// ── Request logging (no PHI) ───────────────────────────
|
|
89
|
+
app.use((req: Request, _res: Response, next: NextFunction) => {
|
|
90
|
+
logger.info(`${req.method} ${req.path}`, {
|
|
91
|
+
ip: req.ip,
|
|
92
|
+
userAgent: req.get("user-agent")?.substring(0, 100),
|
|
93
|
+
});
|
|
94
|
+
next();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Disable X-Powered-By ──────────────────────────────
|
|
98
|
+
app.disable("x-powered-by");
|
|
99
|
+
|
|
100
|
+
logger.info("✅ Security middleware applied");
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { testConnection, closeConnection } from "../config/database.js";
|
|
2
|
+
import { rollbackMigrations } from "./schema.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
try {
|
|
7
|
+
await testConnection();
|
|
8
|
+
await rollbackMigrations();
|
|
9
|
+
} catch (err) {
|
|
10
|
+
logger.error("Rollback failed:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
} finally {
|
|
13
|
+
await closeConnection();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { testConnection, closeConnection } from "../config/database.js";
|
|
2
|
+
import { runMigrations } from "./schema.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
try {
|
|
7
|
+
await testConnection();
|
|
8
|
+
await runMigrations();
|
|
9
|
+
} catch (err) {
|
|
10
|
+
logger.error("Migration failed:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
} finally {
|
|
13
|
+
await closeConnection();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
main();
|