@mtg-tracker/common 1.0.28 → 1.0.30
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/README.md +206 -0
- package/build/functions/runMigrations.js +28 -0
- package/build/index.js +4 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# @mtg-tracker/common
|
|
2
|
+
|
|
3
|
+
Shared utility library for the MTG Tracker microservices. This package provides common errors, middleware, logging utilities, and database migration tools used across all services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mtg-tracker/common
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Exports
|
|
12
|
+
|
|
13
|
+
### Functions
|
|
14
|
+
|
|
15
|
+
#### `runMigrations(pool, migrationsDir, service)`
|
|
16
|
+
Automatically runs SQL migrations for a service. Creates a service-specific migrations tracking table and executes all SQL files in the migrations directory that haven't been run yet.
|
|
17
|
+
|
|
18
|
+
**Parameters:**
|
|
19
|
+
- `pool` (mysql.Pool) - MySQL2 connection pool
|
|
20
|
+
- `migrationsDir` (string) - Absolute path to the migrations directory
|
|
21
|
+
- `service` (string) - Service name for tracking migrations
|
|
22
|
+
|
|
23
|
+
**Example:**
|
|
24
|
+
```typescript
|
|
25
|
+
import { runMigrations } from '@mtg-tracker/common';
|
|
26
|
+
import pool from './database';
|
|
27
|
+
|
|
28
|
+
await runMigrations(pool, path.join(__dirname, 'migrations'), 'auth');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Error Classes
|
|
32
|
+
|
|
33
|
+
All error classes extend `CustomError` and provide consistent error serialization across services.
|
|
34
|
+
|
|
35
|
+
#### `BadRequestError`
|
|
36
|
+
HTTP 400 error for invalid request data.
|
|
37
|
+
|
|
38
|
+
#### `CustomError` (abstract)
|
|
39
|
+
Base class for all custom errors. Provides:
|
|
40
|
+
- `statusCode` property
|
|
41
|
+
- `serializeErrors()` method for consistent error formatting
|
|
42
|
+
|
|
43
|
+
#### `DatabaseConnectionError`
|
|
44
|
+
HTTP 500 error for database connection failures.
|
|
45
|
+
|
|
46
|
+
#### `NotAuthorizedError`
|
|
47
|
+
HTTP 401 error for unauthorized access attempts.
|
|
48
|
+
|
|
49
|
+
#### `NotFoundError`
|
|
50
|
+
HTTP 404 error for missing resources.
|
|
51
|
+
|
|
52
|
+
#### `RequestValidationError`
|
|
53
|
+
HTTP 400 error for request validation failures. Integrates with `express-validator`.
|
|
54
|
+
|
|
55
|
+
### Middleware
|
|
56
|
+
|
|
57
|
+
#### `currentUser`
|
|
58
|
+
Extracts and verifies JWT from session, attaches user payload to `req.currentUser`.
|
|
59
|
+
|
|
60
|
+
**User Payload:**
|
|
61
|
+
```typescript
|
|
62
|
+
{
|
|
63
|
+
id: number;
|
|
64
|
+
email: string;
|
|
65
|
+
username: string;
|
|
66
|
+
role: 'user' | 'admin';
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Example:**
|
|
71
|
+
```typescript
|
|
72
|
+
import { currentUser } from '@mtg-tracker/common';
|
|
73
|
+
|
|
74
|
+
app.use(currentUser);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `requireAuth`
|
|
78
|
+
Ensures user is authenticated. Throws `NotAuthorizedError` if `req.currentUser` is not set.
|
|
79
|
+
|
|
80
|
+
**Example:**
|
|
81
|
+
```typescript
|
|
82
|
+
import { requireAuth } from '@mtg-tracker/common';
|
|
83
|
+
|
|
84
|
+
router.get('/protected', requireAuth, (req, res) => {
|
|
85
|
+
// User is guaranteed to be authenticated
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `requireAdmin`
|
|
90
|
+
Ensures user is authenticated AND has admin role. Throws `NotAuthorizedError` if user is not authenticated or not an admin.
|
|
91
|
+
|
|
92
|
+
**Example:**
|
|
93
|
+
```typescript
|
|
94
|
+
import { requireAdmin } from '@mtg-tracker/common';
|
|
95
|
+
|
|
96
|
+
router.delete('/admin/users/:id', requireAdmin, (req, res) => {
|
|
97
|
+
// User is guaranteed to be an admin
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### `validateRequest`
|
|
102
|
+
Validates request using express-validator results. Throws `RequestValidationError` if validation fails.
|
|
103
|
+
|
|
104
|
+
**Example:**
|
|
105
|
+
```typescript
|
|
106
|
+
import { body } from 'express-validator';
|
|
107
|
+
import { validateRequest } from '@mtg-tracker/common';
|
|
108
|
+
|
|
109
|
+
router.post(
|
|
110
|
+
'/users',
|
|
111
|
+
[
|
|
112
|
+
body('email').isEmail(),
|
|
113
|
+
body('password').isLength({ min: 6 })
|
|
114
|
+
],
|
|
115
|
+
validateRequest,
|
|
116
|
+
(req, res) => {
|
|
117
|
+
// Request is validated
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `errorHandler`
|
|
123
|
+
Global error handler middleware. Catches all errors and returns consistent JSON responses.
|
|
124
|
+
|
|
125
|
+
**Example:**
|
|
126
|
+
```typescript
|
|
127
|
+
import { errorHandler } from '@mtg-tracker/common';
|
|
128
|
+
|
|
129
|
+
// Add as last middleware
|
|
130
|
+
app.use(errorHandler);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Logging
|
|
134
|
+
|
|
135
|
+
#### `ServiceLogger`
|
|
136
|
+
Pino-based logger with custom log levels and pretty printing in development.
|
|
137
|
+
|
|
138
|
+
**Features:**
|
|
139
|
+
- Custom log levels: trace, debug, log, info, warn, error, fatal
|
|
140
|
+
- Color-coded output in development
|
|
141
|
+
- JSON output in production (for Loki/Grafana)
|
|
142
|
+
- Service name prefix on all logs
|
|
143
|
+
|
|
144
|
+
**Example:**
|
|
145
|
+
```typescript
|
|
146
|
+
import { ServiceLogger } from '@mtg-tracker/common';
|
|
147
|
+
|
|
148
|
+
const logger = new ServiceLogger('auth');
|
|
149
|
+
|
|
150
|
+
logger.log('Server starting...');
|
|
151
|
+
logger.info('User created', { userId: 123 });
|
|
152
|
+
logger.error('Database error', error);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Usage Pattern
|
|
156
|
+
|
|
157
|
+
Typical service setup:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import express from 'express';
|
|
161
|
+
import {
|
|
162
|
+
currentUser,
|
|
163
|
+
errorHandler,
|
|
164
|
+
requireAuth,
|
|
165
|
+
ServiceLogger,
|
|
166
|
+
runMigrations
|
|
167
|
+
} from '@mtg-tracker/common';
|
|
168
|
+
|
|
169
|
+
const app = express();
|
|
170
|
+
const logger = new ServiceLogger('my-service');
|
|
171
|
+
|
|
172
|
+
// Middleware
|
|
173
|
+
app.use(express.json());
|
|
174
|
+
app.use(currentUser);
|
|
175
|
+
|
|
176
|
+
// Routes
|
|
177
|
+
app.get('/api/protected', requireAuth, (req, res) => {
|
|
178
|
+
res.json({ user: req.currentUser });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Error handling (must be last)
|
|
182
|
+
app.use(errorHandler);
|
|
183
|
+
|
|
184
|
+
// Database migrations
|
|
185
|
+
await runMigrations(pool, path.join(__dirname, 'migrations'), 'my-service');
|
|
186
|
+
|
|
187
|
+
// Start server
|
|
188
|
+
app.listen(3000, () => {
|
|
189
|
+
logger.log('Service started on port 3000');
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Dependencies
|
|
194
|
+
|
|
195
|
+
- `express` - Web framework
|
|
196
|
+
- `jsonwebtoken` - JWT verification
|
|
197
|
+
- `mysql2` - MySQL database driver
|
|
198
|
+
- `pino` - Logging library
|
|
199
|
+
- `pino-pretty` - Pretty log formatter for development
|
|
200
|
+
- `express-validator` - Request validation
|
|
201
|
+
|
|
202
|
+
## Environment Variables
|
|
203
|
+
|
|
204
|
+
- `JWT_KEY` - Secret key for JWT verification (required)
|
|
205
|
+
- `LOG_LEVEL` - Pino log level (default: 'trace')
|
|
206
|
+
- `NODE_ENV` - Set to 'production' for JSON logging
|
|
@@ -60,6 +60,34 @@ function runMigrations(pool, migrationsDir, service) {
|
|
|
60
60
|
logger.log(`Migration ${file} completed successfully`);
|
|
61
61
|
}
|
|
62
62
|
catch (error) {
|
|
63
|
+
// Handle duplicate index errors gracefully (idempotency across environments)
|
|
64
|
+
const isDuplicateIndexError = error && (error.errno === 1061 || error.code === 'ER_DUP_KEYNAME' || (error.message && error.message.includes('Duplicate key name')));
|
|
65
|
+
if (isDuplicateIndexError) {
|
|
66
|
+
logger.warn(`Duplicate index error running migration ${file}, checking if index already exists`);
|
|
67
|
+
// Try to parse table and index name from the SQL so we can verify existence
|
|
68
|
+
const idxMatch = sql.match(/(?:ADD\s+INDEX|CREATE\s+INDEX)\s+`?([a-zA-Z0-9_]+)`?/i);
|
|
69
|
+
const tableMatch = sql.match(/ALTER\s+TABLE\s+`?([a-zA-Z0-9_]+)`?/i) || sql.match(/ON\s+`?([a-zA-Z0-9_]+)`?/i);
|
|
70
|
+
const indexName = idxMatch ? idxMatch[1] : null;
|
|
71
|
+
const tableName = tableMatch ? tableMatch[1] : null;
|
|
72
|
+
if (indexName && tableName) {
|
|
73
|
+
try {
|
|
74
|
+
const [rows] = yield pool.query(`SELECT COUNT(1) as cnt FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?`, [tableName, indexName]);
|
|
75
|
+
const cnt = rows && rows[0] && rows[0].cnt ? Number(rows[0].cnt) : 0;
|
|
76
|
+
if (cnt > 0) {
|
|
77
|
+
logger.warn(`Index ${indexName} on table ${tableName} already exists; marking migration ${file} as executed.`);
|
|
78
|
+
yield pool.query(`INSERT INTO ${migrationsTable} (filename) VALUES (?)`, [file]);
|
|
79
|
+
continue; // proceed to next migration
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (qerr) {
|
|
83
|
+
logger.error(`Error while checking if index exists for migration ${file}:`, qerr);
|
|
84
|
+
throw error; // rethrow original migration error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// If we couldn't verify the index, rethrow to prevent masking unknown issues
|
|
88
|
+
logger.error(`Unable to verify duplicate index for migration ${file}; rethrowing original error`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
63
91
|
logger.error(`Error running migration ${file}:`, error);
|
|
64
92
|
throw error;
|
|
65
93
|
}
|
package/build/index.js
CHANGED
|
@@ -14,16 +14,20 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
//Functions
|
|
17
18
|
__exportStar(require("./functions/runMigrations"), exports);
|
|
19
|
+
// Errors
|
|
18
20
|
__exportStar(require("./errors/bad-request-error"), exports);
|
|
19
21
|
__exportStar(require("./errors/custom-error"), exports);
|
|
20
22
|
__exportStar(require("./errors/database-connection-error"), exports);
|
|
21
23
|
__exportStar(require("./errors/request-validation-error"), exports);
|
|
22
24
|
__exportStar(require("./errors/not-found-error"), exports);
|
|
23
25
|
__exportStar(require("./errors/not-authorized-error"), exports);
|
|
26
|
+
// Middlewares
|
|
24
27
|
__exportStar(require("./middlewares/current-user"), exports);
|
|
25
28
|
__exportStar(require("./middlewares/error-handler"), exports);
|
|
26
29
|
__exportStar(require("./middlewares/require-auth"), exports);
|
|
27
30
|
__exportStar(require("./middlewares/validate-request"), exports);
|
|
28
31
|
__exportStar(require("./middlewares/require-admin"), exports);
|
|
32
|
+
// Logs
|
|
29
33
|
__exportStar(require("./logs/service-log"), exports);
|