From db0d5fda493d71fabb62153c76094a4992fc3eb9 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Mon, 1 Jun 2026 21:42:01 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix:=20Normalize=20blan?= =?UTF-8?q?k=20tenantId=20before=20persisting=20audit=20entries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `recordAuditEntry` write path was treating any non-null tenantId as a real string, so empty or whitespace-only values reached the schema validator, failed the non-empty-string check, and silently dropped the audit row. The read-side `tenantFilter` already treats those values as platform-level scope, so the write path now mirrors it: blank or whitespace-only tenantId becomes an omitted field, which matches `{ tenantId: { $exists: false } }` queries and clears validation. Added a regression test that records two entries with blank and whitespace tenantId and asserts both persist with the tenantId field absent. --- packages/data-schemas/src/methods/auditLog.spec.ts | 12 ++++++++++++ packages/data-schemas/src/methods/auditLog.ts | 12 +++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/data-schemas/src/methods/auditLog.spec.ts b/packages/data-schemas/src/methods/auditLog.spec.ts index f4f8a0c512..79344d4c03 100644 --- a/packages/data-schemas/src/methods/auditLog.spec.ts +++ b/packages/data-schemas/src/methods/auditLog.spec.ts @@ -69,6 +69,18 @@ describe('auditLog methods', () => { const raw = await AuditLog.collection.findOne({}); expect(raw && Object.prototype.hasOwnProperty.call(raw, 'tenantId')).toBe(false); }); + + it('treats blank and whitespace-only tenantId as platform-level (no silent drop)', async () => { + const blank = await methods.recordAuditEntry(baseInput({ tenantId: '' })); + const whitespace = await methods.recordAuditEntry(baseInput({ tenantId: ' ' })); + expect(blank).not.toBeNull(); + expect(whitespace).not.toBeNull(); + const rows = await AuditLog.collection.find({}).toArray(); + expect(rows).toHaveLength(2); + for (const row of rows) { + expect(Object.prototype.hasOwnProperty.call(row, 'tenantId')).toBe(false); + } + }); }); describe('listAuditLogPage', () => { diff --git a/packages/data-schemas/src/methods/auditLog.ts b/packages/data-schemas/src/methods/auditLog.ts index 487bacea86..1ac7d0cc16 100644 --- a/packages/data-schemas/src/methods/auditLog.ts +++ b/packages/data-schemas/src/methods/auditLog.ts @@ -120,6 +120,16 @@ function clampLimit(limit?: number): number { export function createAuditLogMethods(mongoose: typeof import('mongoose')): AuditLogMethods { async function recordAuditEntry(input: RecordAuditEntryInput): Promise { const AuditLog = mongoose.models.AuditLog as Model; + /** + * Match the read-side `tenantFilter` convention: blank or whitespace-only + * tenant IDs are platform-level scope and the field must be omitted + * entirely (so the row matches `{ tenantId: { $exists: false } }` queries + * and satisfies the non-empty-string validator on the schema). + */ + const normalizedTenantId = + typeof input.tenantId === 'string' && input.tenantId.trim().length > 0 + ? input.tenantId + : undefined; try { const doc = await AuditLog.create({ action: input.action, @@ -129,7 +139,7 @@ export function createAuditLogMethods(mongoose: typeof import('mongoose')): Audi targetPrincipalId: input.targetPrincipalId, targetName: input.targetName, capability: input.capability, - ...(input.tenantId != null && { tenantId: input.tenantId }), + ...(normalizedTenantId !== undefined && { tenantId: normalizedTenantId }), }); return doc; } catch (err) {