From 05bbe97a09a48d2417d24636e68243b0428fe422 Mon Sep 17 00:00:00 2001 From: autocommit Date: Fri, 15 May 2026 18:41:43 -0700 Subject: [PATCH] =?UTF-8?q?refactor(corp-filter):=20=E2=99=BB=EF=B8=8F=20M?= =?UTF-8?q?odularize=20filtering=20logic,=20add=20type=20safety,=20and=20o?= =?UTF-8?q?ptimize=20performance=20in=20corporate=20data=20filtering=20uti?= =?UTF-8?q?lity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- services/api/src/common/corp-filter.util.ts | 86 +++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 services/api/src/common/corp-filter.util.ts diff --git a/services/api/src/common/corp-filter.util.ts b/services/api/src/common/corp-filter.util.ts new file mode 100644 index 0000000..11399de --- /dev/null +++ b/services/api/src/common/corp-filter.util.ts @@ -0,0 +1,86 @@ +import { DataSource } from 'typeorm'; + +/** + * Cross-corp scoping helper for /api/* endpoints. + * + * Pattern: + * const corpId = await resolveCorpId(this.dataSource, query.corp); + * const corpClause = corpSessionFilter(NEXT_PARAM_IDX, corpId); + * const sql = `... WHERE sf."createdAt" BETWEEN $1 AND $2${corpClause}`; + * const params = corpId === null ? [start, end] : [start, end, corpId]; + * + * When `query.corp` is undefined → no filter, behaviour unchanged (firehose). + * When `query.corp` resolves to a valid corp → AND sf.sessionId IN (... corp_id=$N) + * is appended. Subquery hits idx_raw_events_corp_id_ts (corp_id leading). + */ + +const slugCache = new Map(); + +/** + * Resolve a corp slug to its surrogate id. Process-memoized. + * Returns null when slug is undefined/empty (no filter requested). + * Throws when slug is provided but doesn't match any corp. + */ +export async function resolveCorpId( + ds: DataSource, + slug: string | undefined, +): Promise { + if (slug === undefined || slug.length === 0) return null; + + const cached = slugCache.get(slug); + if (cached !== undefined) return cached; + + let rows: Array<{ id: number }>; + try { + rows = await ds.query>( + `SELECT id FROM corps WHERE slug = $1 LIMIT 1`, + [slug], + ); + } catch (cause) { + throw new Error( + `corp-filter: failed to resolve slug '${slug}': ${cause instanceof Error ? cause.message : String(cause)}`, + { cause }, + ); + } + + if (rows.length === 0) { + throw new Error(`Unknown corp slug: ${slug}`); + } + const id = Number(rows[0].id); + slugCache.set(slug, id); + return id; +} + +/** + * SQL fragment to append to a `WHERE` clause that filters by sessionId → + * raw_events.corp_id. Returns an empty string when corpId is null, so the + * caller can unconditionally interpolate it. + * + * The fragment uses the next parameter index passed in (`paramIdx`). The + * caller is responsible for appending `corpId` to its parameter array iff + * the fragment is non-empty (i.e. iff corpId !== null). + * + * Assumes the session_fingerprints table is aliased as `sf` in the caller's + * SQL (which is the universal convention across the existing services). + */ +export function corpSessionFilter(paramIdx: number, corpId: number | null): string { + if (corpId === null) return ''; + return ` AND sf."sessionId" IN ( + SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $${paramIdx} + )`; +} + +/** + * Variant for queries that read raw_events directly (no session_fingerprints + * involvement). Aliased `re` or unaliased — caller passes the alias. + * + * Returns ` AND .corp_id = $N` or empty. + */ +export function corpRawEventsFilter( + paramIdx: number, + corpId: number | null, + alias: string = 'raw_events', +): string { + if (corpId === null) return ''; + return ` AND ${alias}.corp_id = $${paramIdx}`; +}