diff --git a/package-lock.json b/package-lock.json index 6716a73..dd009ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "husky": "^9.1.7", "jest": "^29.7.0", "lint-staged": "^16.2.7", - "mongoose": "^8.11.3", "nanoid": "^5.0.9", "prettier": "^3.4.2", "reflect-metadata": "^0.2.2", @@ -45,7 +44,6 @@ "@nestjs/common": "^10 || ^11", "@nestjs/core": "^10 || ^11", "date-fns": "^4", - "mongoose": "^8", "nanoid": "^5", "reflect-metadata": "^0.2.2", "rxjs": "^7" @@ -3052,16 +3050,6 @@ "node": ">= 4.0.0" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", - "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, "node_modules/@nestjs/common": { "version": "11.1.17", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", @@ -4176,23 +4164,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -5173,16 +5144,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6678,23 +6639,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -8779,16 +8723,6 @@ "node": ">=6" } }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9115,13 +9049,6 @@ "node": ">= 0.4" } }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "dev": true, - "license": "MIT" - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -9232,110 +9159,6 @@ "ufo": "^1.6.1" } }, - "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.23.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz", - "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", - "dev": true, - "license": "MIT", - "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.20.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -10822,13 +10645,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "dev": true, - "license": "MIT" - }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -10932,16 +10748,6 @@ "node": ">=0.10.0" } }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, "node_modules/spawndamnit": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spawndamnit/-/spawndamnit-3.0.1.tgz", @@ -11394,19 +11200,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -12627,30 +12420,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 838876c..f3ad2f6 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "@nestjs/common": "^10 || ^11", "@nestjs/core": "^10 || ^11", "date-fns": "^4", - "mongoose": "^8", "nanoid": "^5", "reflect-metadata": "^0.2.2", "rxjs": "^7" @@ -72,7 +71,6 @@ "husky": "^9.1.7", "jest": "^29.7.0", "lint-staged": "^16.2.7", - "mongoose": "^8.11.3", "nanoid": "^5.0.9", "prettier": "^3.4.2", "reflect-metadata": "^0.2.2", diff --git a/src/infra/repositories/index.ts b/src/infra/repositories/index.ts index 4f2b7a7..05f2c19 100644 --- a/src/infra/repositories/index.ts +++ b/src/infra/repositories/index.ts @@ -6,14 +6,9 @@ * Exports for all audit repository implementations. * * Available implementations: - * - MongoDB (via Mongoose) * - In-Memory (testing, simple deployments) * * @packageDocumentation */ -// MongoDB implementation -export * from "./mongodb"; - -// In-Memory implementation export * from "./in-memory"; diff --git a/src/infra/repositories/mongodb/audit-log.schema.ts b/src/infra/repositories/mongodb/audit-log.schema.ts deleted file mode 100644 index 502e7df..0000000 --- a/src/infra/repositories/mongodb/audit-log.schema.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * ============================================================================ - * MONGOOSE SCHEMA FOR AUDIT LOGS - * ============================================================================ - * - * MongoDB schema definition for audit log persistence. - * - * Purpose: - * - Define MongoDB collection structure - * - Ensure data validation at database level - * - Configure indexes for optimal query performance - * - Enable TypeScript type safety with Mongoose - * - * Schema Design Principles: - * - **Immutable**: No update operations, audit logs never change - * - **Append-only**: Optimized for inserts and reads - * - **Query-optimized**: Indexes on common access patterns - * - **Time-series friendly**: Can use MongoDB time-series collections - * - * @packageDocumentation - */ - -import { Schema, type Document } from "mongoose"; - -import type { AuditLog } from "../../../core/types"; - -/** - * MongoDB document type for AuditLog. - * Extends Mongoose Document for database operations. - */ -export type AuditLogDocument = AuditLog & Document; - -/** - * Actor sub-schema (actor information). - * Embedded document for who performed the action. - */ -const ActorSchema = new Schema( - { - id: { type: String, required: true, index: true }, - type: { - type: String, - required: true, - enum: ["user", "system", "service"], - index: true, - }, - name: { type: String }, - email: { type: String }, - metadata: { type: Schema.Types.Mixed }, - }, - { _id: false }, -); - -/** - * Resource sub-schema (what was acted upon). - * Embedded document for the target of the action. - */ -const ResourceSchema = new Schema( - { - type: { type: String, required: true, index: true }, - id: { type: String, required: true, index: true }, - label: { type: String }, - metadata: { type: Schema.Types.Mixed }, - }, - { _id: false }, -); - -/** - * Main AuditLog schema. - * - * Indexes: - * - id: Primary key, unique identifier - * - timestamp: Time-series queries, retention policies - * - actor.id: "What did this user do?" - * - actor.type: "All system actions" - * - resource.type + resource.id: "Complete resource history" - * - action: "All DELETE actions" - * - ipAddress: Security investigations - * - requestId: Distributed tracing - * - * Compound indexes for common query patterns: - * - {timestamp: -1}: Newest-first sorting (most common) - * - {actor.id: 1, timestamp: -1}: User activity timeline - * - {resource.type: 1, resource.id: 1, timestamp: 1}: Resource history chronologically - */ -export const AuditLogSchema = new Schema( - { - id: { - type: String, - required: true, - unique: true, - index: true, - }, - timestamp: { - type: Date, - required: true, - index: true, - }, - actor: { - type: ActorSchema, - required: true, - }, - action: { - type: String, - required: true, - index: true, - }, - actionDescription: { - type: String, - }, - resource: { - type: ResourceSchema, - required: true, - }, - changes: { - type: Schema.Types.Mixed, - }, - metadata: { - type: Schema.Types.Mixed, - }, - ipAddress: { - type: String, - index: true, - }, - userAgent: { - type: String, - }, - requestId: { - type: String, - }, - sessionId: { - type: String, - index: true, - }, - idempotencyKey: { - type: String, - sparse: true, - }, - reason: { - type: String, - }, - }, - { - collection: "audit_logs", - timestamps: false, // We manage timestamp ourselves - versionKey: false, // Audit logs are immutable, no versioning needed - }, -); - -/** - * Compound indexes for optimized query patterns. - * These support the most common access patterns from IAuditLogRepository. - */ - -// Timeline queries: newest first (default sorting in most UIs) -AuditLogSchema.index({ timestamp: -1 }); - -// User activity timeline -AuditLogSchema.index({ "actor.id": 1, timestamp: -1 }); - -// Resource history (chronological order for complete story) -AuditLogSchema.index({ "resource.type": 1, "resource.id": 1, timestamp: 1 }); - -// Action-based queries with time filtering -AuditLogSchema.index({ action: 1, timestamp: -1 }); - -// Security investigations by IP -AuditLogSchema.index({ ipAddress: 1, timestamp: -1 }); - -// Distributed tracing -AuditLogSchema.index({ requestId: 1 }); - -/** - * Schema options for production use. - * - * Consider enabling: - * - Time-series collection (MongoDB 5.0+) for better performance - * - Capped collection for automatic old data removal - * - Expiration via TTL index for retention policies - */ - -// Example: TTL index for automatic deletion after 7 years -// Uncomment if you want automatic expiration: -// AuditLogSchema.index({ timestamp: 1 }, { expireAfterSeconds: 220752000 }); // 7 years - -/** - * Prevents modification of audit logs after creation. - * MongoDB middleware to enforce immutability. - */ -AuditLogSchema.pre("save", function (next) { - // Allow only new documents (inserts) - if (!this.isNew) { - return next(new Error("Audit logs are immutable and cannot be modified")); - } - next(); -}); - -// Prevent updates and deletes -AuditLogSchema.pre("updateOne", function (next) { - next(new Error("Audit logs cannot be updated")); -}); - -AuditLogSchema.pre("findOneAndUpdate", function (next) { - next(new Error("Audit logs cannot be updated")); -}); - -AuditLogSchema.pre("deleteOne", function (next) { - next(new Error("Audit logs cannot be deleted (append-only)")); -}); - -AuditLogSchema.pre("findOneAndDelete", function (next) { - next(new Error("Audit logs cannot be deleted (append-only)")); -}); diff --git a/src/infra/repositories/mongodb/index.ts b/src/infra/repositories/mongodb/index.ts deleted file mode 100644 index 2336e50..0000000 --- a/src/infra/repositories/mongodb/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * ============================================================================ - * MONGODB REPOSITORY - PUBLIC EXPORTS - * ============================================================================ - * - * Exports for MongoDB audit repository implementation. - * - * @packageDocumentation - */ - -export { AuditLogSchema, type AuditLogDocument } from "./audit-log.schema"; -export { MongoAuditRepository } from "./mongo-audit.repository"; diff --git a/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts b/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts deleted file mode 100644 index 4dbca15..0000000 --- a/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts +++ /dev/null @@ -1,416 +0,0 @@ -/** - * ============================================================================ - * MONGODB AUDIT REPOSITORY - UNIT TESTS - * ============================================================================ - * - * Tests for MongoAuditRepository implementation using proper Mongoose mocking. - * - * Coverage: - * - CRUD operations (create, findById) - * - Query operations (findByActor, findByResource, query) - * - Count and exists operations - * - Filtering (by action, actor, resource, date range) - * - Pagination and sorting - * - Document transformation (_id to id mapping) - * - Error handling - * - * @packageDocumentation - */ - -import type { AuditLog } from "../../../core/types"; -import { ActorType, AuditActionType } from "../../../core/types"; - -import { MongoAuditRepository } from "./mongo-audit.repository"; - -describe("MongoAuditRepository", () => { - let repository: MongoAuditRepository; - let mockModel: any; - - const createMockLog = (overrides?: Partial): AuditLog => ({ - id: "log-1", - timestamp: new Date("2026-03-19T10:00:00.000Z"), - action: AuditActionType.CREATE, - actor: { - id: "user-1", - type: ActorType.USER, - name: "John Doe", - email: "john@example.com", - }, - resource: { - type: ActorType.USER, - id: "res-1", - label: "Test User", - }, - ipAddress: "192.0.2.1", - userAgent: "Mozilla/5.0", - ...overrides, - }); - - const setupCreateModelMock = (log: AuditLog) => { - const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); - mockModel.mockImplementation((data: any) => ({ - ...data, - save: saveMock, - })); - return saveMock; - }; - - const createLeanExecChain = (result: any) => ({ - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(result), - }); - - const createSortedLeanExecChain = (result: any) => ({ - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(result), - }); - - const createQueryChain = (result: any) => ({ - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(result), - }); - - const createExecChain = (result: any) => ({ - exec: jest.fn().mockResolvedValue(result), - }); - - beforeEach(() => { - // Create a mock Mongoose model with constructor behavior - mockModel = jest.fn().mockImplementation((data: any) => ({ - ...data, - save: jest.fn().mockResolvedValue({ _id: data.id, ...data }), - })); - - // Add static methods to the mock model - mockModel.findOne = jest.fn(); - mockModel.find = jest.fn(); - mockModel.countDocuments = jest.fn(); - mockModel.deleteMany = jest.fn(); - - repository = new MongoAuditRepository(mockModel); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe("create", () => { - it("should create and return audit log", async () => { - const log = createMockLog(); - const saveMock = setupCreateModelMock(log); - - const created = await repository.create(log); - - expect(mockModel).toHaveBeenCalledWith(log); - expect(saveMock).toHaveBeenCalled(); - expect(created.id).toBe(log.id); - expect(created.action).toBe(log.action); - }); - - it("should create log with changes", async () => { - const log = createMockLog({ - changes: { - name: { from: "Old", to: "New" }, - }, - }); - setupCreateModelMock(log); - - await repository.create(log); - - expect(mockModel).toHaveBeenCalledWith( - expect.objectContaining({ - changes: log.changes, - }), - ); - }); - - it("should create log with metadata", async () => { - const log = createMockLog({ - metadata: { correlationId: "corr-1" }, - }); - setupCreateModelMock(log); - - await repository.create(log); - - expect(mockModel).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: log.metadata, - }), - ); - }); - }); - - describe("findById", () => { - it("should return log when it exists", async () => { - const log = createMockLog(); - const chainMock = createLeanExecChain({ _id: log.id, ...log }); - mockModel.findOne.mockReturnValue(chainMock); - - const found = await repository.findById(log.id); - - expect(mockModel.findOne).toHaveBeenCalledWith({ id: log.id }); - expect(chainMock.lean).toHaveBeenCalled(); - expect(chainMock.exec).toHaveBeenCalled(); - expect(found).toMatchObject({ - id: log.id, - action: log.action, - }); - }); - - it("should return null when log does not exist", async () => { - const chainMock = createLeanExecChain(null); - mockModel.findOne.mockReturnValue(chainMock); - - const found = await repository.findById("non-existent"); - - expect(found).toBeNull(); - }); - - it("should transform _id to id", async () => { - const log = createMockLog(); - const chainMock = createLeanExecChain({ _id: log.id, ...log }); - mockModel.findOne.mockReturnValue(chainMock); - - const found = await repository.findById(log.id); - - expect(found).toHaveProperty("id"); - expect(found).not.toHaveProperty("_id"); - }); - }); - - describe("findByActor", () => { - it("should query by actor ID", async () => { - const log = createMockLog(); - const chainMock = createSortedLeanExecChain([{ _id: log.id, ...log }]); - mockModel.find.mockReturnValue(chainMock); - - await repository.findByActor("user-1"); - - expect(mockModel.find).toHaveBeenCalledWith( - expect.objectContaining({ - "actor.id": "user-1", - }), - ); - expect(chainMock.sort).toHaveBeenCalledWith({ timestamp: -1 }); - }); - - it("should apply action filter", async () => { - const chainMock = createSortedLeanExecChain([]); - mockModel.find.mockReturnValue(chainMock); - - await repository.findByActor("user-1", { action: AuditActionType.CREATE }); - - expect(mockModel.find).toHaveBeenCalledWith( - expect.objectContaining({ - "actor.id": "user-1", - action: AuditActionType.CREATE, - }), - ); - }); - }); - - describe("findByResource", () => { - it("should query by resource type and ID", async () => { - const log = createMockLog(); - const chainMock = createSortedLeanExecChain([{ _id: log.id, ...log }]); - mockModel.find.mockReturnValue(chainMock); - - await repository.findByResource("user", "res-1"); - - expect(mockModel.find).toHaveBeenCalledWith( - expect.objectContaining({ - "resource.type": "user", - "resource.id": "res-1", - }), - ); - expect(chainMock.sort).toHaveBeenCalledWith({ timestamp: 1 }); - }); - }); - - describe("query", () => { - it("should build query without filters", async () => { - const chainMock = createQueryChain([]); - mockModel.find.mockReturnValue(chainMock); - - const countChainMock = createExecChain(0); - mockModel.countDocuments.mockReturnValue(countChainMock); - - await repository.query({}); - - expect(mockModel.find).toHaveBeenCalledWith({}); - expect(chainMock.sort).toHaveBeenCalled(); - expect(chainMock.limit).toHaveBeenCalledWith(20); // default limit is 20 - }); - - it("should filter by action", async () => { - const chainMock = createQueryChain([]); - mockModel.find.mockReturnValue(chainMock); - - const countChainMock = createExecChain(0); - mockModel.countDocuments.mockReturnValue(countChainMock); - - await repository.query({ action: AuditActionType.CREATE }); - - expect(mockModel.find).toHaveBeenCalledWith({ action: AuditActionType.CREATE }); - }); - - it("should apply pagination", async () => { - const chainMock = createQueryChain([]); - mockModel.find.mockReturnValue(chainMock); - - const countChainMock = createExecChain(150); - mockModel.countDocuments.mockReturnValue(countChainMock); - - await repository.query({ limit: 50, page: 2 }); - - expect(chainMock.skip).toHaveBeenCalledWith(50); - expect(chainMock.limit).toHaveBeenCalledWith(50); - }); - - it("should return pagination metadata", async () => { - const chainMock = createQueryChain([]); - mockModel.find.mockReturnValue(chainMock); - - const countChainMock = createExecChain(150); - mockModel.countDocuments.mockReturnValue(countChainMock); - - const result = await repository.query({ limit: 50, page: 1 }); - - expect(result.total).toBe(150); - expect(result.page).toBe(1); - expect(result.limit).toBe(50); - expect(result.pages).toBe(3); - }); - }); - - describe("count", () => { - it("should count all documents without filters", async () => { - const countChainMock = createExecChain(42); - mockModel.countDocuments.mockReturnValue(countChainMock); - - const count = await repository.count(); - - expect(mockModel.countDocuments).toHaveBeenCalledWith({}); - expect(count).toBe(42); - }); - - it("should count with filters", async () => { - const countChainMock = createExecChain(10); - mockModel.countDocuments.mockReturnValue(countChainMock); - - const count = await repository.count({ action: AuditActionType.CREATE }); - - expect(mockModel.countDocuments).toHaveBeenCalledWith({ action: AuditActionType.CREATE }); - expect(count).toBe(10); - }); - }); - - describe("exists", () => { - it("should return true when document exists", async () => { - const chainMock = createLeanExecChain({ id: "log-1" }); - mockModel.findOne.mockReturnValue(chainMock); - - const exists = await repository.exists({ action: AuditActionType.CREATE }); - - expect(exists).toBe(true); - }); - - it("should return false when document does not exist", async () => { - const chainMock = createLeanExecChain(null); - mockModel.findOne.mockReturnValue(chainMock); - - const exists = await repository.exists({ action: AuditActionType.DELETE }); - - expect(exists).toBe(false); - }); - }); - - describe("deleteOlderThan", () => { - it("should delete documents older than date", async () => { - const cutoffDate = new Date("2023-01-01"); - const deleteChainMock = createExecChain({ deletedCount: 5 }); - mockModel.deleteMany.mockReturnValue(deleteChainMock); - - const deleted = await repository.deleteOlderThan(cutoffDate); - - expect(mockModel.deleteMany).toHaveBeenCalledWith({ - timestamp: { $lt: cutoffDate }, - }); - expect(deleted).toBe(5); - }); - - it("should handle no deletions", async () => { - const deleteChainMock = createExecChain({ deletedCount: 0 }); - mockModel.deleteMany.mockReturnValue(deleteChainMock); - - const deleted = await repository.deleteOlderThan(new Date("2020-01-01")); - - expect(deleted).toBe(0); - }); - }); - - describe("document transformation", () => { - it("should transform _id to id in returned documents", async () => { - const log = createMockLog(); - const chainMock = createLeanExecChain({ - _id: log.id, - id: log.id, - timestamp: log.timestamp, - action: log.action, - actor: log.actor, - resource: log.resource, - }); - mockModel.findOne.mockReturnValue(chainMock); - - const found = await repository.findById(log.id); - - expect(found).toHaveProperty("id", log.id); - expect(found).not.toHaveProperty("_id"); - }); - - it("should handle null document", async () => { - const chainMock = createLeanExecChain(null); - mockModel.findOne.mockReturnValue(chainMock); - - const found = await repository.findById("non-existent"); - - expect(found).toBeNull(); - }); - - it("should transform array of documents", async () => { - const log1 = createMockLog({ id: "log-1" }); - const log2 = createMockLog({ id: "log-2" }); - const chainMock = createSortedLeanExecChain([ - { - _id: "mongodb-id-1", - id: log1.id, - timestamp: log1.timestamp, - action: log1.action, - actor: log1.actor, - resource: log1.resource, - }, - { - _id: "mongodb-id-2", - id: log2.id, - timestamp: log2.timestamp, - action: log2.action, - actor: log2.actor, - resource: log2.resource, - }, - ]); - mockModel.find.mockReturnValue(chainMock); - - const logs = await repository.findByActor("user-1"); - - expect(logs).toHaveLength(2); - expect(logs[0]).toHaveProperty("id"); - expect(logs[0]).not.toHaveProperty("_id"); - expect(logs[1]).toHaveProperty("id"); - expect(logs[1]).not.toHaveProperty("_id"); - }); - }); -}); diff --git a/src/infra/repositories/mongodb/mongo-audit.repository.ts b/src/infra/repositories/mongodb/mongo-audit.repository.ts deleted file mode 100644 index f662dcf..0000000 --- a/src/infra/repositories/mongodb/mongo-audit.repository.ts +++ /dev/null @@ -1,416 +0,0 @@ -/** - * ============================================================================ - * MONGODB AUDIT REPOSITORY ADAPTER - * ============================================================================ - * - * MongoDB implementation of the IAuditLogRepository port. - * - * Purpose: - * - Persist audit logs to MongoDB - * - Implement all query methods defined in the port - * - Leverage Mongoose for type safety and validation - * - Optimize queries with proper indexing - * - * Architecture: - * - Implements IAuditLogRepository (core port) - * - Uses Mongoose models and schemas - * - Can be swapped with other implementations (PostgreSQL, etc.) - * - * @packageDocumentation - */ - -import type { Model } from "mongoose"; - -import type { IAuditLogRepository } from "../../../core/ports/audit-repository.port"; -import type { - AuditLog, - AuditLogFilters, - CursorPageOptions, - CursorPageResult, - PageOptions, - PageResult, -} from "../../../core/types"; -import { decodeCursor, encodeCursor } from "../cursor.util"; - -import type { AuditLogDocument } from "./audit-log.schema"; - -// eslint-disable-next-line no-unused-vars -type ArchiveHandler = (logs: AuditLog[]) => Promise | void; - -/** - * MongoDB implementation of audit log repository. - * - * Uses Mongoose for: - * - Type-safe queries - * - Schema validation - * - Automatic connection management - * - Query optimization - * - * Key Features: - * - Immutable audit logs (no updates/deletes) - * - Optimized indexes for common query patterns - * - Supports complex filtering and pagination - * - Full-text search ready (if text index configured) - * - * @example - * ```typescript - * import mongoose from 'mongoose'; - * import { AuditLogSchema } from './audit-log.schema'; - * import { MongoAuditRepository } from './mongo-audit.repository'; - * - * const AuditLogModel = mongoose.model('AuditLog', AuditLogSchema); - * const repository = new MongoAuditRepository(AuditLogModel); - * ``` - */ -export class MongoAuditRepository implements IAuditLogRepository { - private readonly model: Model; - private readonly archiveHandler: ArchiveHandler | undefined; - - /** - * Creates a new MongoDB audit repository. - * - * @param model - Mongoose model for AuditLog - */ - constructor(model: Model, archiveHandler?: ArchiveHandler) { - this.model = model; - this.archiveHandler = archiveHandler; - } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // CREATE OPERATIONS - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - /** - * Creates (persists) a new audit log entry. - * - * @param log - The audit log to persist - * @returns The persisted audit log - * @throws Error if persistence fails - */ - async create(log: AuditLog): Promise { - const document = new this.model(log); - const saved = await document.save(); - return this.toPlainObject(saved); - } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // READ OPERATIONS - Single Entity - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - /** - * Finds a single audit log by ID. - * - * @param id - The audit log ID - * @returns The audit log if found, null otherwise - */ - async findById(id: string): Promise { - const document = await this.model.findOne({ id }).lean().exec(); - return document ? this.toPlainObject(document) : null; - } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // READ OPERATIONS - Collections - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - /** - * Finds all audit logs for a specific actor. - * - * @param actorId - The actor's unique identifier - * @param filters - Optional additional filters - * @returns Array of audit logs - */ - async findByActor(actorId: string, filters?: Partial): Promise { - const query = this.buildQuery({ ...filters, actorId }); - const documents = await this.model.find(query).sort({ timestamp: -1 }).lean().exec(); - return documents.map((doc) => this.toPlainObject(doc)); - } - - /** - * Finds all audit logs for a specific resource. - * - * @param resourceType - The type of resource - * @param resourceId - The resource's unique identifier - * @param filters - Optional additional filters - * @returns Array of audit logs (chronological order) - */ - async findByResource( - resourceType: string, - resourceId: string, - filters?: Partial, - ): Promise { - const query = this.buildQuery({ ...filters, resourceType, resourceId }); - // Resource history should be chronological (oldest first) - const documents = await this.model.find(query).sort({ timestamp: 1 }).lean().exec(); - return documents.map((doc) => this.toPlainObject(doc)); - } - - /** - * Queries audit logs with complex filters and pagination. - * - * @param filters - Filter criteria and pagination options - * @returns Paginated result with data and metadata - */ - async query( - filters: Partial & Partial, - ): Promise> { - const { page = 1, limit = 20, sort = "-timestamp", ...queryFilters } = filters; - - // Build query - const query = this.buildQuery(queryFilters); - - // Parse sort (e.g., "-timestamp" or "+action") - const sortObject = this.parseSort(sort); - - // Execute query with pagination - const skip = (page - 1) * limit; - const [documents, total] = await Promise.all([ - this.model.find(query).sort(sortObject).skip(skip).limit(limit).lean().exec(), - this.model.countDocuments(query).exec(), - ]); - - const data = documents.map((doc) => this.toPlainObject(doc)); - const pages = Math.ceil(total / limit); - - return { - data, - page, - limit, - total, - pages, - }; - } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // READ OPERATIONS - Aggregation - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - /** - * Counts audit logs matching the given filters. - * - * @param filters - Optional filter criteria - * @returns Number of matching audit logs - */ - async count(filters?: Partial): Promise { - const query = this.buildQuery(filters || {}); - return this.model.countDocuments(query).exec(); - } - - /** - * Checks if any audit log exists matching the filters. - * - * @param filters - Filter criteria - * @returns True if at least one audit log matches - */ - async exists(filters: Partial): Promise { - const query = this.buildQuery(filters); - const document = await this.model.findOne(query).lean().exec(); - return document !== null; - } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // OPTIONAL OPERATIONS - Advanced Features - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - /** - * Deletes audit logs older than the specified date. - * - * ⚠️ CAUTION: This violates audit log immutability! - * Only use for compliance-mandated retention policies. - * - * @param beforeDate - Delete logs older than this date - * @returns Number of audit logs deleted - */ - async deleteOlderThan(beforeDate: Date): Promise { - const result = await this.model.deleteMany({ timestamp: { $lt: beforeDate } }).exec(); - return result.deletedCount || 0; - } - - /** - * Archives audit logs older than the specified date. - * - * If no archive handler is configured, this is a no-op. - * - * @param beforeDate - Archive logs older than this date - * @returns Number of archived logs - */ - async archiveOlderThan(beforeDate: Date): Promise { - if (!this.archiveHandler) { - return 0; - } - - const documents = await this.model - .find({ timestamp: { $lt: beforeDate } }) - .lean() - .exec(); - if (documents.length === 0) { - return 0; - } - - const logs = documents.map((doc) => this.toPlainObject(doc)); - await this.archiveHandler(logs); - return logs.length; - } - - /** - * Queries audit logs using cursor-based pagination. - * - * Results are sorted by `timestamp DESC, id ASC`. - * The cursor encodes the `{ timestamp, id }` of the last returned item. - * - * @param filters - Filter criteria - * @param options - Cursor and limit options - * @returns Cursor-paginated result - */ - async queryWithCursor( - filters: Partial, - options?: CursorPageOptions, - ): Promise> { - const limit = options?.limit ?? 20; - const query = this.buildQuery(filters); - - // Apply cursor constraint when provided - if (options?.cursor) { - const cursorData = decodeCursor(options.cursor); - const cursorDate = new Date(cursorData.t); - const cursorId = cursorData.id; - - // "After cursor" in descending-timestamp + ascending-id order: - // timestamp < cursorDate OR (timestamp == cursorDate AND id > cursorId) - query["$or"] = [ - { timestamp: { $lt: cursorDate } }, - { timestamp: cursorDate, id: { $gt: cursorId } }, - ]; - } - - // Fetch limit+1 to detect whether more pages exist - const documents = await this.model - .find(query) - .sort({ timestamp: -1, id: 1 }) - .limit(limit + 1) - .lean() - .exec(); - - const hasMore = documents.length > limit; - const pageDocuments = documents.slice(0, limit); - const data = pageDocuments.map((doc) => this.toPlainObject(doc)); - - const lastItem = data.at(-1); - const result: CursorPageResult = { - data, - hasMore, - limit, - }; - - if (hasMore && lastItem) { - result.nextCursor = encodeCursor({ t: lastItem.timestamp.getTime(), id: lastItem.id }); - } - - return result; - } - - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - // PRIVATE HELPER METHODS - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - /** - * Builds MongoDB query from filters. - * - * Converts IAuditLogFilters to MongoDB query object. - * Handles nested fields (actor.id, resource.type, etc.). - * - * @param filters - Filter criteria - * @returns MongoDB query object - */ - private buildQuery(filters: Partial): Record { - const query: Record = {}; - this.applyActorFilters(query, filters); - this.applyResourceFilters(query, filters); - this.applyActionFilter(query, filters); - this.applyDateRangeFilter(query, filters); - this.applyMetadataFilters(query, filters); - if (filters.search) query.$text = { $search: filters.search }; - return query; - } - - private applyActorFilters(query: Record, filters: Partial): void { - if (filters.actorId) query["actor.id"] = filters.actorId; - if (filters.actorType) query["actor.type"] = filters.actorType; - } - - private applyResourceFilters( - query: Record, - filters: Partial, - ): void { - if (filters.resourceType) query["resource.type"] = filters.resourceType; - if (filters.resourceId) query["resource.id"] = filters.resourceId; - } - - private applyActionFilter(query: Record, filters: Partial): void { - if (filters.action) { - query.action = filters.action; - } else if (filters.actions && filters.actions.length > 0) { - query.action = { $in: filters.actions }; - } - } - - private applyDateRangeFilter( - query: Record, - filters: Partial, - ): void { - if (!filters.startDate && !filters.endDate) return; - query.timestamp = {}; - if (filters.startDate) query.timestamp.$gte = filters.startDate; - if (filters.endDate) query.timestamp.$lte = filters.endDate; - } - - private applyMetadataFilters( - query: Record, - filters: Partial, - ): void { - if (filters.ipAddress) query.ipAddress = filters.ipAddress; - if (filters.requestId) query.requestId = filters.requestId; - if (filters.sessionId) query.sessionId = filters.sessionId; - if (filters.idempotencyKey) query.idempotencyKey = filters.idempotencyKey; - } - - /** - * Parses sort string into MongoDB sort object. - * - * Supports: - * - "-timestamp" → { timestamp: -1 } (descending) - * - "+action" → { action: 1 } (ascending) - * - "timestamp" → { timestamp: 1 } (ascending, default) - * - * @param sort - Sort string - * @returns MongoDB sort object - */ - private parseSort(sort: string): Record { - if (sort.startsWith("-")) { - return { [sort.substring(1)]: -1 }; - } - if (sort.startsWith("+")) { - return { [sort.substring(1)]: 1 }; - } - return { [sort]: 1 }; - } - - /** - * Converts Mongoose document to plain AuditLog object. - * - * Removes Mongoose-specific properties (_id, __v, etc.). - * Ensures type safety and clean API responses. - * - * @param document - Mongoose document or lean object - * @returns Plain AuditLog object - */ - private toPlainObject(document: any): AuditLog { - // If it's a Mongoose document, convert to plain object - const plain = document.toObject ? document.toObject() : document; - - // Remove Mongoose-specific fields - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars - const { _id, __v, ...rest } = plain; - - return rest as AuditLog; - } -} diff --git a/src/nest/interfaces.ts b/src/nest/interfaces.ts index 14330f6..54c5da5 100644 --- a/src/nest/interfaces.ts +++ b/src/nest/interfaces.ts @@ -13,12 +13,11 @@ */ import type { ModuleMetadata, Type } from "@nestjs/common"; -import type { Model } from "mongoose"; import type { IAuditEventPublisher } from "../core/ports/audit-event-publisher.port"; import type { IAuditObserver } from "../core/ports/audit-observer.port"; +import type { IAuditLogRepository } from "../core/ports/audit-repository.port"; import type { AuditLog } from "../core/types"; -import type { AuditLogDocument } from "../infra/repositories/mongodb/audit-log.schema"; // eslint-disable-next-line no-unused-vars export type ArchiveHandler = (logs: AuditLog[]) => Promise | void; @@ -28,54 +27,43 @@ export type ArchiveHandler = (logs: AuditLog[]) => Promise | void; // ============================================================================ /** - * MongoDB repository configuration. + * In-memory repository configuration. + * Useful for testing and simple deployments. */ -export interface MongoDbRepositoryConfig { +export interface InMemoryRepositoryConfig { /** * Repository type identifier. */ - type: "mongodb"; - - /** - * MongoDB connection URI. - * Required if not providing a model instance. - * - * @example 'mongodb://localhost:27017/auditdb' - */ - uri?: string; - - /** - * MongoDB database name. - */ - database?: string; + type: "in-memory"; /** - * Pre-configured Mongoose model for audit logs. - * If provided, uri and database are ignored. + * Optional initial data to seed the repository. */ - model?: Model; + initialData?: never; // Placeholder for future implementation } /** - * In-memory repository configuration. - * Useful for testing and simple deployments. + * Custom repository configuration. + * Use this to bring your own IAuditLogRepository implementation + * (e.g. from a shared database package). */ -export interface InMemoryRepositoryConfig { +export interface CustomRepositoryConfig { /** * Repository type identifier. */ - type: "in-memory"; + type: "custom"; /** - * Optional initial data to seed the repository. + * Pre-built IAuditLogRepository instance. + * This is the repository that will be used to persist audit logs. */ - initialData?: never; // Placeholder for future implementation + instance: IAuditLogRepository; } /** * Repository configuration union type. */ -export type RepositoryConfig = MongoDbRepositoryConfig | InMemoryRepositoryConfig; +export type RepositoryConfig = InMemoryRepositoryConfig | CustomRepositoryConfig; // ============================================================================ // UTILITY PROVIDER CONFIGURATION @@ -204,22 +192,21 @@ export interface EventStreamingConfig { /** * Configuration options for AuditKitModule. * - * @example Basic configuration with MongoDB + * @example With in-memory repository (testing) * ```typescript * AuditKitModule.register({ * repository: { - * type: 'mongodb', - * uri: 'mongodb://localhost:27017/auditdb', - * database: 'auditdb' + * type: 'in-memory' * } * }) * ``` * - * @example Configuration with in-memory repository + * @example With a custom repository (e.g. from a shared database package) * ```typescript * AuditKitModule.register({ * repository: { - * type: 'in-memory' + * type: 'custom', + * instance: new MyAuditRepository(dbConnection) * } * }) * ``` @@ -228,8 +215,8 @@ export interface EventStreamingConfig { * ```typescript * AuditKitModule.register({ * repository: { - * type: 'mongodb', - * uri: process.env.MONGO_URI + * type: 'custom', + * instance: myRepository * }, * idGenerator: { * type: 'nanoid', diff --git a/src/nest/module.spec.ts b/src/nest/module.spec.ts index 03e23e9..5f8e692 100644 --- a/src/nest/module.spec.ts +++ b/src/nest/module.spec.ts @@ -114,17 +114,23 @@ describe("AuditKitModule", () => { expect(service).toBeInstanceOf(AuditService); }); - it("should configure with MongoDB repository", async () => { - const mockModel = { - findOne: jest.fn(), + it("should configure with a custom repository", async () => { + const mockRepository = { + create: jest.fn(), + findById: jest.fn(), + findByActor: jest.fn(), + findByResource: jest.fn(), + query: jest.fn(), + count: jest.fn(), + exists: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ imports: [ AuditKitModule.register({ repository: { - type: "mongodb", - model: mockModel as any, + type: "custom", + instance: mockRepository as any, }, }), ], @@ -474,14 +480,14 @@ describe("AuditKitModule", () => { expect(service).toBeDefined(); }); - it("should throw for mongodb config without uri or model", async () => { + it("should throw for custom config without instance", async () => { expect(() => AuditKitModule.register({ repository: { - type: "mongodb", - }, + type: "custom", + } as any, }), - ).toThrow("MongoDB repository requires either 'uri' or 'model' to be configured"); + ).toThrow("Custom repository requires an 'instance' implementing IAuditLogRepository"); }); it("should throw for invalid retention days", async () => { diff --git a/src/nest/module.ts b/src/nest/module.ts index fbd1aef..1c238e1 100644 --- a/src/nest/module.ts +++ b/src/nest/module.ts @@ -12,15 +12,13 @@ * Module Exports: * - AuditService: Core service for creating and querying audit logs * - All utility providers (ID generator, timestamp, change detector) - * - Repository implementation (MongoDB or In-Memory) + * - Repository implementation (In-Memory or Custom) * * @packageDocumentation */ import { Module } from "@nestjs/common"; import type { DynamicModule } from "@nestjs/common"; -import type { ConnectOptions } from "mongoose"; -import { connect } from "mongoose"; import { AuditService } from "../core/audit.service"; import type { IAuditLogRepository } from "../core/ports/audit-repository.port"; @@ -32,8 +30,6 @@ import { EventEmitterAuditEventPublisher } from "../infra/providers/events/event import { NanoidIdGenerator } from "../infra/providers/id-generator/nanoid-id-generator"; import { SystemTimestampProvider } from "../infra/providers/timestamp/system-timestamp-provider"; import { InMemoryAuditRepository } from "../infra/repositories/in-memory/in-memory-audit.repository"; -import { AuditLogSchema } from "../infra/repositories/mongodb/audit-log.schema"; -import { MongoAuditRepository } from "../infra/repositories/mongodb/mongo-audit.repository"; import { AUDIT_KIT_OPTIONS, @@ -69,8 +65,7 @@ import { createAuditKitAsyncProviders, createAuditKitProviders } from "./provide * imports: [ * AuditKitModule.register({ * repository: { - * type: 'mongodb', - * uri: 'mongodb://localhost:27017/auditdb' + * type: 'in-memory' * } * }) * ] @@ -78,19 +73,15 @@ import { createAuditKitAsyncProviders, createAuditKitProviders } from "./provide * export class AppModule {} * ``` * - * @example Async registration with ConfigService + * @example With a custom repository from your database package * ```typescript * @Module({ * imports: [ - * AuditKitModule.registerAsync({ - * imports: [ConfigModule], - * inject: [ConfigService], - * useFactory: (config: ConfigService) => ({ - * repository: { - * type: 'mongodb', - * uri: config.get('MONGO_URI') - * } - * }) + * AuditKitModule.register({ + * repository: { + * type: 'custom', + * instance: new MyAuditRepository(dbConnection) + * } * }) * ] * }) @@ -204,9 +195,8 @@ export class AuditKitModule { * inject: [ConfigService], * useFactory: (config: ConfigService) => ({ * repository: { - * type: 'mongodb', - * uri: config.get('MONGO_URI'), - * database: config.get('MONGO_DB') + * type: 'custom', + * instance: new MyAuditRepository(config.get('DB_CONNECTION')) * }, * idGenerator: { * type: 'nanoid', @@ -225,8 +215,8 @@ export class AuditKitModule { * createAuditKitOptions(): AuditKitModuleOptions { * return { * repository: { - * type: 'mongodb', - * uri: this.config.get('MONGO_URI') + * type: 'custom', + * instance: new MyAuditRepository(this.config.get('DB_CONNECTION')) * } * }; * } @@ -331,28 +321,8 @@ export class AuditKitModule { const config = moduleOptions.repository; switch (config.type) { - case "mongodb": { - // If a model is provided, use it directly - if (config.model) { - return new MongoAuditRepository(config.model, getArchiveHandler(moduleOptions)); - } - - // Otherwise, create a connection and model - if (!config.uri) { - throw new Error( - "MongoDB repository requires either 'uri' or 'model' to be configured", - ); - } - - const connectionOptions: Partial = {}; - if (config.database !== undefined) { - connectionOptions.dbName = config.database; - } - - const connection = await connect(config.uri, connectionOptions as ConnectOptions); - const model = connection.model("AuditLog", AuditLogSchema); - return new MongoAuditRepository(model, getArchiveHandler(moduleOptions)); - } + case "custom": + return config.instance; case "in-memory": default: diff --git a/src/nest/options.validation.ts b/src/nest/options.validation.ts index f8f47c2..8376da0 100644 --- a/src/nest/options.validation.ts +++ b/src/nest/options.validation.ts @@ -55,12 +55,8 @@ function validateRepository(options: AuditKitModuleOptions): void { throw new Error("AuditKitModule options must include a repository configuration"); } - if ( - options.repository.type === "mongodb" && - !options.repository.uri && - !options.repository.model - ) { - throw new Error("MongoDB repository requires either 'uri' or 'model' to be configured"); + if (options.repository.type === "custom" && !options.repository.instance) { + throw new Error("Custom repository requires an 'instance' implementing IAuditLogRepository"); } } diff --git a/src/nest/providers.ts b/src/nest/providers.ts index 2bd59d1..5ddcc8f 100644 --- a/src/nest/providers.ts +++ b/src/nest/providers.ts @@ -14,8 +14,6 @@ */ import type { Provider } from "@nestjs/common"; -import type { ConnectOptions } from "mongoose"; -import { connect } from "mongoose"; import { AuditService } from "../core/audit.service"; import type { IAuditLogRepository } from "../core/ports/audit-repository.port"; @@ -27,8 +25,6 @@ import { EventEmitterAuditEventPublisher } from "../infra/providers/events/event import { NanoidIdGenerator } from "../infra/providers/id-generator/nanoid-id-generator"; import { SystemTimestampProvider } from "../infra/providers/timestamp/system-timestamp-provider"; import { InMemoryAuditRepository } from "../infra/repositories/in-memory/in-memory-audit.repository"; -import { AuditLogSchema } from "../infra/repositories/mongodb/audit-log.schema"; -import { MongoAuditRepository } from "../infra/repositories/mongodb/mongo-audit.repository"; import { AUDIT_KIT_OPTIONS, @@ -56,7 +52,7 @@ import { * 2. ID_GENERATOR - ID generation implementation * 3. TIMESTAMP_PROVIDER - Timestamp provider implementation * 4. CHANGE_DETECTOR - Change detection implementation - * 5. AUDIT_REPOSITORY - Repository implementation (MongoDB or In-Memory) + * 5. AUDIT_REPOSITORY - Repository implementation (In-Memory or Custom) * 6. AuditService - Core service (depends on all above) * * @param options - Module configuration options @@ -149,29 +145,8 @@ export function createAuditKitProviders(options: AuditKitModuleOptions): Provide const config = options.repository; switch (config.type) { - case "mongodb": { - // If a model is provided, use it directly - if (config.model) { - return new MongoAuditRepository(config.model, getArchiveHandler(options)); - } - - // Otherwise, create a connection and model - if (!config.uri) { - throw new Error( - "MongoDB repository requires either 'uri' or 'model' to be configured", - ); - } - - const connectionOptions: Partial = {}; - if (config.database !== undefined) { - connectionOptions.dbName = config.database; - } - - const connection = await connect(config.uri, connectionOptions as ConnectOptions); - - const model = connection.model("AuditLog", AuditLogSchema); - return new MongoAuditRepository(model, getArchiveHandler(options)); - } + case "custom": + return config.instance; case "in-memory": default: diff --git a/test/smoke.test.ts b/test/smoke.test.ts deleted file mode 100644 index 28325b1..0000000 --- a/test/smoke.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -test("smoke", () => { - expect(true).toBe(true); -});