feat: Sprint 1 Complete - Security, Logging & Performance
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

Task 3 & 4: Structured Logging + PII Sanitization 
- Installed winston and nest-winston packages
- Created winston.config.ts with comprehensive logging setup
- Features:
  * Console transport (development)
  * File transports (error.log, combined.log, audit.log)
  * Exception & rejection handlers
  * PII sanitization (email, phone, SSN, CC, IP addresses)
  * Production-ready configuration
  * Log rotation (5MB max, 5-30 file retention)
  * Structured JSON logging for parsing
- Integrated Winston into NestJS (main.ts, app.module.ts)
- Created logs/ directory with .gitignore
- PII auto-redaction in all logs except audit logs

Task 5: Database Table Partitioning 
- Created V009 migration for activities table partitioning
- Partitioned by month using PostgreSQL RANGE partitioning
- Auto-creates 13 partitions (6 past + current + 6 future months)
- Features:
  * Automatic partition creation function
  * Inherited indexes for all partitions
  * Foreign key constraints maintained
  * Data migration from old table
  * Updated_at trigger
  * Optimized for time-series queries
- Performance benefits:
  * Faster queries (scans only relevant partitions)
  * Easier maintenance (drop old partitions)
  * Better vacuum performance
  * Parallel partition scanning

Sprint 1 Results:
 All 5 tasks complete (100%)
 Estimated: 11-16 hours
 Security hardened
 Logging production-ready
 Database optimized for scale

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 21:27:49 +00:00
parent 85e3848a32
commit 7395157e54
6 changed files with 535 additions and 3 deletions

View File

@@ -52,6 +52,7 @@
"langchain": "^0.3.35",
"mailgun.js": "^12.1.0",
"multer": "^2.0.2",
"nest-winston": "^1.10.2",
"node-fetch": "^2.7.0",
"openai": "^6.0.1",
"otplib": "^12.0.1",
@@ -67,7 +68,8 @@
"sharp": "^0.34.4",
"socket.io": "^4.8.1",
"typeorm": "^0.3.27",
"uuid": "^13.0.0"
"uuid": "^13.0.0",
"winston": "^3.18.3"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
@@ -2084,6 +2086,17 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@dabh/diagnostics": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
"integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
"license": "MIT",
"dependencies": {
"@so-ric/colorspace": "^1.1.6",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
@@ -5896,6 +5909,16 @@
"node": ">=18.0.0"
}
},
"node_modules/@so-ric/colorspace": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
"integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
"license": "MIT",
"dependencies": {
"color": "^5.0.2",
"text-hex": "1.0.x"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@@ -6408,6 +6431,12 @@
"@types/node": "*"
}
},
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@@ -7149,6 +7178,12 @@
"node": ">=12.0.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/async-function": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
@@ -8089,6 +8124,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/color": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz",
"integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==",
"license": "MIT",
"dependencies": {
"color-convert": "^3.0.1",
"color-string": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -8107,6 +8155,48 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz",
"integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==",
"license": "MIT",
"dependencies": {
"color-name": "^2.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/color-string/node_modules/color-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz",
"integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz",
"integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==",
"license": "MIT",
"dependencies": {
"color-name": "^2.0.0"
},
"engines": {
"node": ">=14.6"
}
},
"node_modules/color/node_modules/color-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz",
"integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==",
"license": "MIT",
"engines": {
"node": ">=12.20"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -8683,6 +8773,12 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -9459,6 +9555,12 @@
"bser": "2.1.1"
}
},
"node_modules/fecha": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
"license": "MIT"
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
@@ -9589,6 +9691,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -10511,7 +10619,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11560,6 +11667,12 @@
"node": ">=6"
}
},
"node_modules/kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
"license": "MIT"
},
"node_modules/langchain": {
"version": "0.3.35",
"resolved": "https://registry.npmjs.org/langchain/-/langchain-0.3.35.tgz",
@@ -11927,6 +12040,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/logform": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
"license": "MIT",
"dependencies": {
"@colors/colors": "1.6.0",
"@types/triple-beam": "^1.3.2",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"safe-stable-stringify": "^2.3.1",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/logform/node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"license": "MIT",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
@@ -12263,6 +12402,19 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/nest-winston": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.10.2.tgz",
"integrity": "sha512-Z9IzL/nekBOF/TEwBHUJDiDPMaXUcFquUQOFavIRet6xF0EbuWnOzslyN/ksgzG+fITNgXhMdrL/POp9SdaFxA==",
"license": "MIT",
"dependencies": {
"fast-safe-stringify": "^2.1.1"
},
"peerDependencies": {
"@nestjs/common": "^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0",
"winston": "^3.0.0"
}
},
"node_modules/node-abi": {
"version": "3.77.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
@@ -12419,6 +12571,15 @@
"wrappy": "1"
}
},
"node_modules/one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"license": "MIT",
"dependencies": {
"fn.name": "1.x.x"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
@@ -13821,6 +13982,15 @@
],
"license": "MIT"
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -14327,6 +14497,15 @@
"node": ">=14"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -14890,6 +15069,12 @@
"node": "*"
}
},
"node_modules/text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
"license": "MIT"
},
"node_modules/thirty-two": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/thirty-two/-/thirty-two-1.0.2.tgz",
@@ -15000,6 +15185,15 @@
"tree-kill": "cli.js"
}
},
"node_modules/triple-beam": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
"integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
"license": "MIT",
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -15820,6 +16014,51 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/winston": {
"version": "3.18.3",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
"license": "MIT",
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8",
"async": "^3.2.3",
"is-stream": "^2.0.0",
"logform": "^2.7.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"safe-stable-stringify": "^2.3.1",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.9.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston-transport": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
"license": "MIT",
"dependencies": {
"logform": "^2.7.0",
"readable-stream": "^3.6.2",
"triple-beam": "^1.3.0"
},
"engines": {
"node": ">= 12.0.0"
}
},
"node_modules/winston/node_modules/@colors/colors": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
"integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
"license": "MIT",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",

View File

@@ -64,6 +64,7 @@
"langchain": "^0.3.35",
"mailgun.js": "^12.1.0",
"multer": "^2.0.2",
"nest-winston": "^1.10.2",
"node-fetch": "^2.7.0",
"openai": "^6.0.1",
"otplib": "^12.0.1",
@@ -79,7 +80,8 @@
"sharp": "^0.34.4",
"socket.io": "^4.8.1",
"typeorm": "^0.3.27",
"uuid": "^13.0.0"
"uuid": "^13.0.0",
"winston": "^3.18.3"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",

View File

@@ -4,6 +4,8 @@ import { ScheduleModule } from '@nestjs/schedule';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { APP_GUARD, APP_FILTER } from '@nestjs/core';
import { WinstonModule } from 'nest-winston';
import { winstonConfig } from './common/logger/winston.config';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@@ -33,6 +35,7 @@ import { HealthController } from './common/controllers/health.controller';
isGlobal: true,
envFilePath: '.env',
}),
WinstonModule.forRoot(winstonConfig),
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
inject: [ConfigService],

View File

@@ -0,0 +1,145 @@
import { WinstonModuleOptions } from 'nest-winston';
import * as winston from 'winston';
import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
// Custom format to sanitize PII
const piiSanitizer = winston.format((info) => {
const piiPatterns = {
email: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
phone: /(\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g,
ssn: /\b\d{3}-?\d{2}-?\d{4}\b/g,
creditCard: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
ipv4: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
};
const sanitize = (text: string): string => {
if (typeof text !== 'string') return text;
let sanitized = text;
sanitized = sanitized.replace(piiPatterns.email, '[EMAIL_REDACTED]');
sanitized = sanitized.replace(piiPatterns.phone, '[PHONE_REDACTED]');
sanitized = sanitized.replace(piiPatterns.ssn, '[SSN_REDACTED]');
sanitized = sanitized.replace(piiPatterns.creditCard, '[CC_REDACTED]');
sanitized = sanitized.replace(piiPatterns.ipv4, '[IP_REDACTED]');
return sanitized;
};
// Sanitize message
if (info.message) {
info.message = sanitize(info.message);
}
// Sanitize metadata
if (info.context && typeof info.context === 'object') {
Object.keys(info.context).forEach((key) => {
if (typeof info.context[key] === 'string') {
info.context[key] = sanitize(info.context[key]);
}
});
}
return info;
});
export const winstonConfig: WinstonModuleOptions = {
transports: [
// Console transport for development
new winston.transports.Console({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.ms(),
piiSanitizer(),
nestWinstonModuleUtilities.format.nestLike('MaternalApp', {
colors: true,
prettyPrint: true,
}),
),
}),
// File transport for errors
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
piiSanitizer(),
winston.format.json(),
),
maxsize: 5242880, // 5MB
maxFiles: 5,
}),
// File transport for all logs
new winston.transports.File({
filename: 'logs/combined.log',
format: winston.format.combine(
winston.format.timestamp(),
piiSanitizer(),
winston.format.json(),
),
maxsize: 5242880, // 5MB
maxFiles: 10,
}),
// File transport for audit logs (no PII sanitization)
new winston.transports.File({
filename: 'logs/audit.log',
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
winston.format((info) => {
// Only include audit-related logs
return info.context?.isAudit ? info : false;
})(),
),
maxsize: 5242880, // 5MB
maxFiles: 30, // Keep longer for compliance
}),
],
// Global exception handler
exceptionHandlers: [
new winston.transports.File({
filename: 'logs/exceptions.log',
format: winston.format.combine(
winston.format.timestamp(),
piiSanitizer(),
winston.format.json(),
),
}),
],
// Global rejection handler
rejectionHandlers: [
new winston.transports.File({
filename: 'logs/rejections.log',
format: winston.format.combine(
winston.format.timestamp(),
piiSanitizer(),
winston.format.json(),
),
}),
],
};
// Production configuration (can be enabled via environment variable)
if (process.env.NODE_ENV === 'production') {
// Remove console transport in production
winstonConfig.transports = winstonConfig.transports.filter(
(transport) => !(transport instanceof winston.transports.Console),
);
// Add production-specific transports (e.g., CloudWatch, Elasticsearch)
// Example: CloudWatch
// winstonConfig.transports.push(
// new WinstonCloudWatch({
// logGroupName: process.env.CLOUDWATCH_LOG_GROUP,
// logStreamName: `${process.env.CLOUDWATCH_LOG_STREAM}-${Date.now()}`,
// awsRegion: process.env.AWS_REGION,
// jsonMessage: true,
// })
// );
}

View File

@@ -0,0 +1,139 @@
-- Migration V009: Activity Table Partitioning
-- Created: 2025-10-03
-- Description: Convert activities table to partitioned table by month for better performance at scale
-- Step 1: Create new partitioned table
CREATE TABLE IF NOT EXISTS activities_partitioned (
id UUID NOT NULL,
child_id UUID NOT NULL,
user_id UUID NOT NULL,
activity_type VARCHAR(50) NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
start_time TIMESTAMP,
end_time TIMESTAMP,
duration_minutes INTEGER,
amount DECIMAL(10, 2),
unit VARCHAR(20),
notes TEXT,
metadata JSONB,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, timestamp)
) PARTITION BY RANGE (timestamp);
-- Step 2: Create partitions for the last 6 months and next 6 months
-- Format: activities_YYYY_MM
DO $$
DECLARE
start_date DATE;
end_date DATE;
partition_name TEXT;
partition_start DATE;
partition_end DATE;
BEGIN
-- Create partitions for last 6 months
FOR i IN -6..6 LOOP
partition_start := DATE_TRUNC('month', CURRENT_DATE + (i || ' months')::INTERVAL);
partition_end := partition_start + INTERVAL '1 month';
partition_name := 'activities_' || TO_CHAR(partition_start, 'YYYY_MM');
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF activities_partitioned
FOR VALUES FROM (%L) TO (%L)',
partition_name,
partition_start,
partition_end
);
RAISE NOTICE 'Created partition: %', partition_name;
END LOOP;
END $$;
-- Step 3: Create indexes on partitions (will be inherited by all partitions)
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_child_id
ON activities_partitioned(child_id);
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_user_id
ON activities_partitioned(user_id);
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_activity_type
ON activities_partitioned(activity_type);
CREATE INDEX IF NOT EXISTS idx_activities_partitioned_timestamp
ON activities_partitioned(timestamp DESC);
-- Step 4: Create foreign key constraints
ALTER TABLE activities_partitioned
ADD CONSTRAINT fk_activities_partitioned_child
FOREIGN KEY (child_id) REFERENCES children(id) ON DELETE CASCADE;
ALTER TABLE activities_partitioned
ADD CONSTRAINT fk_activities_partitioned_user
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
-- Step 5: Migrate existing data (if any) from old activities table to partitioned table
INSERT INTO activities_partitioned
SELECT * FROM activities
ON CONFLICT DO NOTHING;
-- Step 6: Rename old table and replace with partitioned table
ALTER TABLE activities RENAME TO activities_old;
ALTER TABLE activities_partitioned RENAME TO activities;
-- Step 7: Create trigger for updated_at
CREATE OR REPLACE FUNCTION update_activities_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_activities_updated_at
BEFORE UPDATE ON activities
FOR EACH ROW
EXECUTE FUNCTION update_activities_updated_at();
-- Step 8: Create function to automatically create future partitions
CREATE OR REPLACE FUNCTION create_monthly_partition()
RETURNS void AS $$
DECLARE
partition_date DATE;
partition_name TEXT;
partition_start DATE;
partition_end DATE;
BEGIN
-- Create partition for next month if it doesn't exist
partition_date := DATE_TRUNC('month', CURRENT_DATE + INTERVAL '2 months');
partition_name := 'activities_' || TO_CHAR(partition_date, 'YYYY_MM');
-- Check if partition exists
IF NOT EXISTS (
SELECT 1 FROM pg_class WHERE relname = partition_name
) THEN
partition_start := partition_date;
partition_end := partition_start + INTERVAL '1 month';
EXECUTE format(
'CREATE TABLE IF NOT EXISTS %I PARTITION OF activities
FOR VALUES FROM (%L) TO (%L)',
partition_name,
partition_start,
partition_end
);
RAISE NOTICE 'Auto-created partition: %', partition_name;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Step 9: Create scheduled job to create partitions (optional - can be done via cron or app)
-- This would typically be called monthly by a cron job or scheduled task
-- Example: SELECT create_monthly_partition();
-- Step 10: Add comments
COMMENT ON TABLE activities IS 'Partitioned activities table - partitioned by month for performance';
COMMENT ON FUNCTION create_monthly_partition() IS 'Auto-creates next month partition if it does not exist';
-- Note: To drop old activities_old table after verification:
-- DROP TABLE IF EXISTS activities_old CASCADE;

View File

@@ -2,10 +2,14 @@ import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import helmet from 'helmet';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Replace default logger with Winston
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
// Security headers with Helmet
app.use(
helmet({