@usebetterdev/audit-cli 0.7.0 → 0.8.1

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.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/check.ts"],"sourcesContent":["/**\n * `better-audit check` — Verify audit_logs table schema, indexes, and write path.\n *\n * Performs:\n * 1. Database connectivity test\n * 2. Schema validation: table exists, columns have correct types and nullability\n * 3. Index validation: all expected indexes are present\n * 4. Live integration test: sentinel INSERT → SELECT → verify actor_id → DELETE\n *\n * Supports Postgres, MySQL, and SQLite via dialect-specific introspection queries.\n *\n * Exits with code 0 on success, 1 on failure.\n * All sentinel data is cleaned up regardless of check outcome.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport { sql } from \"kysely\";\nimport type { Kysely } from \"kysely\";\nimport { printCheckResults, type CheckResult, type CheckOutput } from \"@usebetterdev/cli-utils\";\nimport { AUDIT_LOG_SCHEMA, type ColumnType } from \"@usebetterdev/audit-core\";\nimport { createKyselyInstance } from \"./sql-executor.js\";\nimport type { Database } from \"./sql-executor.js\";\nimport { detectDialect, type DatabaseDialect } from \"./detect-adapter.js\";\nimport { INDEX_DEFINITIONS } from \"./generate-sql.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type { CheckResult, CheckOutput };\n\nexport interface CheckOptions {\n databaseUrl?: string | undefined;\n verbose?: boolean | undefined;\n}\n\n/** Thrown when one or more checks fail. Carries the full output for CLI rendering. */\nexport class CheckFailedError extends Error {\n readonly output: CheckOutput;\n constructor(output: CheckOutput) {\n super(\"One or more audit checks failed\");\n this.name = \"CheckFailedError\";\n this.output = output;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst SENTINEL_TABLE_NAME = \"__audit_cli_check__\";\nconst SENTINEL_ACTOR_ID = \"__check_actor__\";\nconst SENTINEL_RECORD_ID = \"__sentinel__\";\n\n/**\n * Map from core ColumnType to the type string each dialect reports via\n * introspection. Mirrors the sqlType map in generate-sql.ts but in the\n * direction the database returns (lowercase / as-reported).\n *\n * Postgres: information_schema.columns → udt_name\n * MySQL: information_schema.columns → data_type (lowercase)\n * SQLite: PRAGMA table_info → type (uppercase, as declared)\n */\nexport const DIALECT_TYPE_MAP: Record<\n DatabaseDialect,\n Record<ColumnType, ReadonlyArray<string>>\n> = {\n postgres: {\n uuid: [\"uuid\"],\n timestamptz: [\"timestamptz\"],\n text: [\"text\"],\n jsonb: [\"jsonb\"],\n boolean: [\"bool\"],\n },\n mysql: {\n uuid: [\"char\"],\n timestamptz: [\"datetime\"],\n text: [\"text\", \"mediumtext\", \"longtext\"],\n jsonb: [\"json\"],\n boolean: [\"tinyint\"],\n },\n sqlite: {\n uuid: [\"TEXT\"],\n timestamptz: [\"TEXT\"],\n text: [\"TEXT\"],\n jsonb: [\"TEXT\"],\n boolean: [\"INTEGER\"],\n },\n};\n\n// ---------------------------------------------------------------------------\n// Shared helpers\n// ---------------------------------------------------------------------------\n\n/** Normalize a reported type string for comparison against the dialect map. */\nfunction normalizeReportedType(\n raw: string,\n dialect: DatabaseDialect,\n): string {\n if (dialect === \"sqlite\") {\n return raw.toUpperCase();\n }\n // Postgres udt_name and MySQL data_type are already lowercase\n return raw.toLowerCase();\n}\n\n/** Check whether `actual` matches any of the acceptable types for a column. */\nfunction typeMatches(\n actual: string,\n definition: { type: ColumnType },\n dialect: DatabaseDialect,\n): boolean {\n const normalized = normalizeReportedType(actual, dialect);\n const acceptable = DIALECT_TYPE_MAP[dialect][definition.type];\n return acceptable.includes(normalized);\n}\n\n/** Build the \"expected\" label shown in error messages. */\nfunction expectedTypeLabel(\n definition: { type: ColumnType },\n dialect: DatabaseDialect,\n): string {\n const acceptable = DIALECT_TYPE_MAP[dialect][definition.type];\n if (acceptable.length === 1) {\n return acceptable[0] ?? definition.type;\n }\n return acceptable.join(\" | \");\n}\n\n// ---------------------------------------------------------------------------\n// Individual checks\n// ---------------------------------------------------------------------------\n\nasync function checkConnection(\n db: Kysely<Database>,\n): Promise<CheckResult> {\n try {\n await sql`SELECT 1`.execute(db);\n return { check: \"database connection\", passed: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n check: \"database connection\",\n passed: false,\n message: `Cannot connect to database: ${message}`,\n remediation:\n \"Verify DATABASE_URL is correct and the database server is running\",\n };\n }\n}\n\nasync function checkTableExists(\n db: Kysely<Database>,\n): Promise<CheckResult> {\n try {\n await sql`SELECT 1 FROM audit_logs WHERE 1 = 0`.execute(db);\n return { check: \"audit_logs table exists\", passed: true };\n } catch {\n return {\n check: \"audit_logs table exists\",\n passed: false,\n message: \"Table audit_logs not found\",\n remediation:\n \"Run `npx @usebetterdev/audit-cli migrate` to create the table\",\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Column checks — per-dialect introspection\n// ---------------------------------------------------------------------------\n\ninterface IntrospectedColumn {\n name: string;\n reportedType: string;\n nullable: boolean;\n}\n\nasync function introspectColumnsPostgres(\n db: Kysely<Database>,\n): Promise<IntrospectedColumn[]> {\n const result = await sql<{\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>`\n SELECT column_name, udt_name, is_nullable\n FROM information_schema.columns\n WHERE table_schema = 'public' AND table_name = ${AUDIT_LOG_SCHEMA.tableName}\n ORDER BY ordinal_position\n `.execute(db);\n\n return result.rows.map((row) => ({\n name: row.column_name,\n reportedType: row.udt_name,\n nullable: row.is_nullable === \"YES\",\n }));\n}\n\nasync function introspectColumnsMysql(\n db: Kysely<Database>,\n): Promise<IntrospectedColumn[]> {\n const result = await sql<{\n COLUMN_NAME: string;\n DATA_TYPE: string;\n IS_NULLABLE: string;\n }>`\n SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n FROM information_schema.columns\n WHERE table_schema = DATABASE() AND table_name = ${AUDIT_LOG_SCHEMA.tableName}\n ORDER BY ORDINAL_POSITION\n `.execute(db);\n\n return result.rows.map((row) => ({\n name: row.COLUMN_NAME,\n reportedType: row.DATA_TYPE,\n nullable: row.IS_NULLABLE === \"YES\",\n }));\n}\n\nasync function introspectColumnsSqlite(\n db: Kysely<Database>,\n): Promise<IntrospectedColumn[]> {\n const result = await sql<{\n name: string;\n type: string;\n notnull: number;\n }>`PRAGMA table_info(audit_logs)`.execute(db);\n\n return result.rows.map((row) => ({\n name: row.name,\n reportedType: row.type,\n nullable: row.notnull === 0,\n }));\n}\n\nasync function introspectColumns(\n db: Kysely<Database>,\n dialect: DatabaseDialect,\n): Promise<IntrospectedColumn[]> {\n if (dialect === \"mysql\") {\n return introspectColumnsMysql(db);\n }\n if (dialect === \"sqlite\") {\n return introspectColumnsSqlite(db);\n }\n return introspectColumnsPostgres(db);\n}\n\nfunction checkColumns(\n columns: ReadonlyArray<IntrospectedColumn>,\n dialect: DatabaseDialect,\n): CheckResult[] {\n const results: CheckResult[] = [];\n\n const actualColumns = new Map<string, IntrospectedColumn>();\n for (const col of columns) {\n actualColumns.set(col.name, col);\n }\n\n for (const [name, definition] of Object.entries(AUDIT_LOG_SCHEMA.columns)) {\n const actual = actualColumns.get(name);\n\n if (actual === undefined) {\n results.push({\n check: `column ${name}`,\n passed: false,\n message: `Missing column: ${name}`,\n remediation:\n \"Run `npx @usebetterdev/audit-cli migrate` to add missing columns\",\n });\n continue;\n }\n\n // Type check\n if (!typeMatches(actual.reportedType, definition, dialect)) {\n const expected = expectedTypeLabel(definition, dialect);\n results.push({\n check: `column ${name} type`,\n passed: false,\n message: `Column ${name} has type \"${actual.reportedType}\", expected \"${expected}\"`,\n remediation: `Alter column ${name} to type ${definition.type.toUpperCase()}`,\n });\n } else {\n results.push({ check: `column ${name} type`, passed: true });\n }\n\n // Nullability check\n // SQLite does not enforce NOT NULL for PRIMARY KEY columns the same way,\n // so skip nullability checks on the primary key in SQLite.\n if (dialect === \"sqlite\" && definition.primaryKey === true) {\n results.push({ check: `column ${name} nullability`, passed: true });\n } else if (actual.nullable !== definition.nullable) {\n const actualDesc = actual.nullable ? \"nullable\" : \"NOT NULL\";\n const expectedDesc = definition.nullable ? \"nullable\" : \"NOT NULL\";\n results.push({\n check: `column ${name} nullability`,\n passed: false,\n message: `Column ${name} is ${actualDesc}, expected ${expectedDesc}`,\n remediation: `ALTER TABLE audit_logs ALTER COLUMN ${name} ${definition.nullable ? \"DROP NOT NULL\" : \"SET NOT NULL\"}`,\n });\n } else {\n results.push({ check: `column ${name} nullability`, passed: true });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Index checks — per-dialect introspection\n// ---------------------------------------------------------------------------\n\nasync function introspectIndexNamesPostgres(\n db: Kysely<Database>,\n): Promise<Set<string>> {\n const result = await sql<{ indexname: string }>`\n SELECT indexname\n FROM pg_indexes\n WHERE schemaname = 'public' AND tablename = ${AUDIT_LOG_SCHEMA.tableName}\n `.execute(db);\n return new Set(result.rows.map((row) => row.indexname));\n}\n\nasync function introspectIndexNamesMysql(\n db: Kysely<Database>,\n): Promise<Set<string>> {\n const result = await sql<{ INDEX_NAME: string }>`\n SELECT DISTINCT INDEX_NAME\n FROM information_schema.statistics\n WHERE table_schema = DATABASE() AND table_name = ${AUDIT_LOG_SCHEMA.tableName}\n `.execute(db);\n return new Set(result.rows.map((row) => row.INDEX_NAME));\n}\n\nasync function introspectIndexNamesSqlite(\n db: Kysely<Database>,\n): Promise<Set<string>> {\n const result = await sql<{ name: string }>`\n PRAGMA index_list(audit_logs)\n `.execute(db);\n return new Set(result.rows.map((row) => row.name));\n}\n\nasync function introspectIndexNames(\n db: Kysely<Database>,\n dialect: DatabaseDialect,\n): Promise<Set<string>> {\n if (dialect === \"mysql\") {\n return introspectIndexNamesMysql(db);\n }\n if (dialect === \"sqlite\") {\n return introspectIndexNamesSqlite(db);\n }\n return introspectIndexNamesPostgres(db);\n}\n\nfunction checkIndexes(actualIndexes: Set<string>): CheckResult[] {\n const results: CheckResult[] = [];\n\n for (const idx of INDEX_DEFINITIONS) {\n if (actualIndexes.has(idx.name)) {\n results.push({ check: `index ${idx.name}`, passed: true });\n } else {\n results.push({\n check: `index ${idx.name}`,\n passed: false,\n message: `Missing index ${idx.name} on (${idx.columns.join(\", \")})`,\n remediation:\n \"Run `npx @usebetterdev/audit-cli migrate` to create missing indexes\",\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Live write test\n// ---------------------------------------------------------------------------\n\nasync function checkLiveWrite(\n db: Kysely<Database>,\n): Promise<CheckResult[]> {\n const results: CheckResult[] = [];\n const sentinelId = randomUUID();\n\n try {\n // INSERT sentinel row\n try {\n await sql`\n INSERT INTO audit_logs (id, table_name, operation, record_id, actor_id)\n VALUES (\n ${sentinelId},\n ${SENTINEL_TABLE_NAME},\n 'INSERT',\n ${SENTINEL_RECORD_ID},\n ${SENTINEL_ACTOR_ID}\n )\n `.execute(db);\n results.push({\n check: \"write test: insert sentinel row\",\n passed: true,\n });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n results.push({\n check: \"write test: insert sentinel row\",\n passed: false,\n message: `Failed to insert into audit_logs: ${message}`,\n remediation:\n \"Verify the database user has INSERT permission on audit_logs\",\n });\n return results;\n }\n\n // SELECT sentinel row back\n try {\n const readResult = await sql<{\n id: string;\n actor_id: string | null;\n }>`\n SELECT id, actor_id FROM audit_logs WHERE id = ${sentinelId}\n `.execute(db);\n\n const row = readResult.rows[0];\n\n if (row === undefined) {\n results.push({\n check: \"write test: read back sentinel row\",\n passed: false,\n message:\n \"Sentinel row not found after INSERT — possible trigger or RLS interference\",\n remediation:\n \"Check for DELETE triggers or restrictive RLS policies on audit_logs\",\n });\n return results;\n }\n\n results.push({\n check: \"write test: read back sentinel row\",\n passed: true,\n });\n\n // Verify actor_id round-trip (context propagation proxy)\n if (row.actor_id === SENTINEL_ACTOR_ID) {\n results.push({\n check: \"write test: actor context captured\",\n passed: true,\n });\n } else {\n results.push({\n check: \"write test: actor context captured\",\n passed: false,\n message: `actor_id is \"${row.actor_id ?? \"NULL\"}\", expected \"${SENTINEL_ACTOR_ID}\"`,\n remediation:\n \"Verify AsyncLocalStorage context propagation in your middleware setup\",\n });\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n results.push({\n check: \"write test: read back sentinel row\",\n passed: false,\n message: `Failed to read from audit_logs: ${message}`,\n remediation:\n \"Verify the database user has SELECT permission on audit_logs\",\n });\n }\n } finally {\n // Always clean up sentinel data\n try {\n await sql`\n DELETE FROM audit_logs WHERE id = ${sentinelId}\n `.execute(db);\n } catch {\n // Cleanup failure is not critical — sentinel has a distinctive table_name\n // for manual identification\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Run all audit_logs checks and return structured results.\n * Does not print to stdout — use `check()` for CLI output.\n */\nexport async function runCheck(databaseUrl: string): Promise<CheckOutput> {\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n const results: CheckResult[] = [];\n const warnings: CheckResult[] = [];\n\n try {\n // 0. Connection test\n const connectionCheck = await checkConnection(db);\n results.push(connectionCheck);\n if (!connectionCheck.passed) {\n return { passed: false, results, warnings };\n }\n\n // 1. Table exists\n const tableCheck = await checkTableExists(db);\n results.push(tableCheck);\n if (!tableCheck.passed) {\n return { passed: false, results, warnings };\n }\n\n // 2–3. Column + index validation (independent — run in parallel)\n const [columns, indexNames] = await Promise.all([\n introspectColumns(db, dialect),\n introspectIndexNames(db, dialect),\n ]);\n results.push(...checkColumns(columns, dialect));\n results.push(...checkIndexes(indexNames));\n\n // 4. Live write + read-back + actor context + cleanup\n results.push(...(await checkLiveWrite(db)));\n } finally {\n await db.destroy();\n }\n\n const passed = results.every((r) => r.passed);\n return { passed, results, warnings };\n}\n\n// ---------------------------------------------------------------------------\n// CLI entry point\n// ---------------------------------------------------------------------------\n\n/** CLI action handler for `better-audit check`. */\nexport async function check(options: CheckOptions = {}): Promise<void> {\n const databaseUrl = options.databaseUrl ?? process.env[\"DATABASE_URL\"];\n if (databaseUrl === undefined || databaseUrl === \"\") {\n throw new Error(\n \"DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url.\",\n );\n }\n\n const output = await runCheck(databaseUrl);\n const verbose = options.verbose === true;\n\n printCheckResults(output, { verbose });\n\n if (!output.passed) {\n throw new CheckFailedError(output);\n }\n}\n\n// Re-export internals for unit testing\nexport {\n checkColumns as _checkColumns,\n checkIndexes as _checkIndexes,\n};\nexport type { IntrospectedColumn as _IntrospectedColumn };\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/purge.ts"],"sourcesContent":["/**\n * `better-audit purge` — Delete audit logs older than the configured retention period.\n *\n * Connects to the database directly via Kysely (no ORM adapter needed).\n * Executes batched DELETEs to avoid holding long row-level locks on large tables.\n *\n * Always run with --dry-run first to preview what will be deleted.\n */\n\nimport pc from \"picocolors\";\nimport { createKyselyInstance } from \"./sql-executor.js\";\nimport { detectDialect } from \"./detect-adapter.js\";\nimport {\n loadBetterConfig,\n BetterConfigNotFoundError,\n ConfigValidationError,\n requireAuditConfig,\n} from \"@usebetterdev/plugin/config\";\nimport type { Database } from \"./sql-executor.js\";\nimport type { Kysely } from \"kysely\";\n\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T[\\w:.+-]+)?$/;\nconst DURATION_REGEX = /^(\\d+)(d|w|m|y)$/i;\nconst DEFAULT_BATCH_SIZE = 1000;\nconst MAX_BATCHES = 100_000;\n\nexport interface PurgeOptions {\n /** Preview rows to be deleted without deleting them. */\n dryRun?: boolean;\n /** ISO date string (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\", \"1y\"). */\n since?: string;\n /** Rows per DELETE batch. Default: 1000. */\n batchSize?: number;\n /** Database URL (default: DATABASE_URL env). */\n databaseUrl?: string;\n /** Skip confirmation prompt (required for live deletion). */\n yes?: boolean;\n}\n\n/**\n * Parse a `--since` value to an absolute cutoff `Date`.\n *\n * Accepts:\n * - ISO-8601 date strings: \"2025-01-01\" or \"2025-01-01T00:00:00Z\"\n * - Duration shorthands: \"90d\", \"4w\", \"3m\", \"1y\"\n *\n * Exported for testing.\n */\nexport function parseSinceValue(value: string): Date {\n if (ISO_DATE_REGEX.test(value)) {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Invalid date \"${value}\". Expected ISO-8601 format (e.g. \"2025-01-01\").`);\n }\n return date;\n }\n\n const match = DURATION_REGEX.exec(value);\n if (match !== null) {\n const amount = parseInt(match[1]!, 10);\n if (amount <= 0) {\n throw new Error(\n `Invalid --since value \"${value}\". Duration amount must be greater than zero.`,\n );\n }\n const unit = match[2]!.toLowerCase();\n const now = new Date();\n if (unit === \"d\") {\n now.setDate(now.getDate() - amount);\n } else if (unit === \"w\") {\n now.setDate(now.getDate() - amount * 7);\n } else if (unit === \"m\") {\n now.setMonth(now.getMonth() - amount);\n } else {\n // y\n now.setFullYear(now.getFullYear() - amount);\n }\n return now;\n }\n\n throw new Error(\n `Invalid --since value \"${value}\". ` +\n `Expected an ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\", \"4w\", \"3m\", \"1y\").`,\n );\n}\n\n/**\n * Resolve the cutoff date from options.\n *\n * Priority: `--since` flag > config `retention.days`.\n * Throws if neither is available.\n *\n * Exported for testing.\n */\nexport async function resolveCutoffDate(options: {\n since?: string;\n cwd?: string;\n}): Promise<Date> {\n if (options.since !== undefined) {\n return parseSinceValue(options.since);\n }\n\n try {\n const config = await loadBetterConfig(\n options.cwd !== undefined ? { cwd: options.cwd } : {},\n );\n const auditConfig = requireAuditConfig(config);\n if (auditConfig.retention !== undefined) {\n const cutoff = new Date();\n cutoff.setDate(cutoff.getDate() - auditConfig.retention.days);\n return cutoff;\n }\n } catch (err) {\n if (\n err instanceof BetterConfigNotFoundError ||\n err instanceof ConfigValidationError\n ) {\n // Config file not found or audit section missing/invalid — fall through\n } else {\n throw err;\n }\n }\n\n throw new Error(\n \"No retention policy configured. \" +\n \"Pass --since <date|duration> or set audit.retention.days in your better.config file.\",\n );\n}\n\n/**\n * Count rows eligible for deletion (for --dry-run).\n */\nasync function countEligibleRows(\n db: Kysely<Database>,\n before: Date,\n): Promise<number> {\n const result = await db\n .selectFrom(\"audit_logs\")\n .select((eb) => eb.fn.countAll<string>().as(\"count\"))\n .where(\"timestamp\", \"<\", before)\n .executeTakeFirst();\n return result !== undefined ? Number(result.count) : 0;\n}\n\n/**\n * Delete one batch of rows older than `before`.\n *\n * Uses `DELETE … WHERE id IN (SELECT id … LIMIT n)` on all dialects.\n * This avoids the lack of LIMIT support on DELETE in Postgres and SQLite.\n *\n * Returns the number of rows deleted in this batch.\n */\nasync function deleteBatch(\n db: Kysely<Database>,\n before: Date,\n batchSize: number,\n): Promise<number> {\n const subquery = db\n .selectFrom(\"audit_logs\")\n .select(\"id\")\n .where(\"timestamp\", \"<\", before)\n .limit(batchSize);\n\n const result = await db\n .deleteFrom(\"audit_logs\")\n .where(\"id\", \"in\", subquery)\n .executeTakeFirst();\n\n return Number(result.numDeletedRows);\n}\n\n/**\n * Format a number with locale-aware thousands separators.\n */\nfunction formatCount(n: number): string {\n return n.toLocaleString(\"en-US\");\n}\n\n/**\n * Format elapsed milliseconds to a human-readable string.\n *\n * Exported for testing.\n */\nexport function formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${ms}ms`;\n }\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nexport async function purge(options: PurgeOptions = {}): Promise<void> {\n // 1. Resolve database URL\n const databaseUrl = options.databaseUrl ?? process.env[\"DATABASE_URL\"];\n if (databaseUrl === undefined || databaseUrl === \"\") {\n throw new Error(\n \"DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url.\",\n );\n }\n\n // 2. Resolve the cutoff date\n // Only consult config when --since is absent (short-circuit when --since is set)\n const before = await resolveCutoffDate({\n ...(options.since !== undefined ? { since: options.since } : {}),\n cwd: process.cwd(),\n });\n\n // 3. Validate cutoff is in the past\n if (before > new Date()) {\n throw new Error(\n `Cutoff date ${before.toISOString().split(\"T\")[0]!} is in the future. ` +\n `Purge only accepts past dates.`,\n );\n }\n\n const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;\n if (!Number.isInteger(batchSize) || batchSize <= 0) {\n throw new Error(`--batch-size must be a positive integer, got ${String(batchSize)}`);\n }\n\n // 4. Connect\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n\n const startTime = Date.now();\n\n try {\n // 4. Dry run — count only, no confirmation needed\n if (options.dryRun) {\n const count = await countEligibleRows(db, before);\n process.stderr.write(\n `${pc.cyan(\"→\")} ${pc.bold(formatCount(count))} rows would be deleted` +\n ` (cutoff: ${pc.dim(before.toISOString().split(\"T\")[0]!)})\\n`,\n );\n process.stderr.write(`${pc.dim(\" Dry run — no changes made.\")}\\n`);\n return;\n }\n\n // 5. Confirmation guard — require --yes for live deletion\n if (!options.yes) {\n const count = await countEligibleRows(db, before);\n process.stderr.write(\n `${pc.yellow(\"!\")} ${pc.bold(formatCount(count))} rows will be deleted` +\n ` (cutoff: ${pc.dim(before.toISOString().split(\"T\")[0]!)})\\n` +\n ` Run with ${pc.bold(\"--yes\")} to confirm deletion, or ${pc.bold(\"--dry-run\")} to preview.\\n`,\n );\n return;\n }\n\n // 6. Batched delete loop\n let totalDeleted = 0;\n let batchNumber = 0;\n\n while (batchNumber < MAX_BATCHES) {\n const deleted = await deleteBatch(db, before, batchSize);\n totalDeleted += deleted;\n batchNumber++;\n\n if (batchNumber % 10 === 0) {\n process.stderr.write(\n ` ${pc.dim(`batch ${batchNumber}: ${formatCount(totalDeleted)} rows deleted so far...`)}\\n`,\n );\n }\n\n if (deleted < batchSize) {\n break;\n }\n }\n\n if (batchNumber >= MAX_BATCHES) {\n process.stderr.write(\n pc.yellow(\n ` Warning: reached max batch limit (${MAX_BATCHES.toLocaleString(\"en-US\")}). Some rows may remain.\\n`,\n ),\n );\n }\n\n const elapsed = Date.now() - startTime;\n\n // 7. Summary\n process.stderr.write(`${pc.green(\"✓\")} Purge complete\\n`);\n process.stderr.write(` Rows deleted: ${pc.bold(formatCount(totalDeleted))}\\n`);\n process.stderr.write(` Cutoff date: ${pc.dim(before.toISOString().split(\"T\")[0]!)}\\n`);\n process.stderr.write(` Time taken: ${pc.dim(formatDuration(elapsed))}\\n`);\n } finally {\n await db.destroy();\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../shared/cli-utils/src/format-timestamp.ts","../../../shared/cli-utils/src/write-sql-output.ts","../../../shared/cli-utils/src/detect-orm-adapter.ts","../../../shared/cli-utils/src/read-cli-version.ts","../../../shared/cli-utils/src/print-check-results.ts","../src/detect-adapter.ts"],"sourcesContent":["/**\n * Format a date as `YYYYMMDD<sep>HHMMSS`.\n *\n * @param date - The date to format.\n * @param separator - String inserted between date and time parts. Defaults to `\"_\"`.\n * Pass `\"\"` for a compact `YYYYMMDDHHMMSS` format (e.g. for Prisma migration directories).\n */\nexport function formatTimestamp(date: Date, separator: string = \"_\"): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n const h = String(date.getHours()).padStart(2, \"0\");\n const min = String(date.getMinutes()).padStart(2, \"0\");\n const sec = String(date.getSeconds()).padStart(2, \"0\");\n return `${y}${m}${d}${separator}${h}${min}${sec}`;\n}\n","import { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport pc from \"picocolors\";\n\n/**\n * Write SQL to a file. If `output` ends in `.sql`, treat it as a file path.\n * Otherwise treat it as a directory and generate a filename inside it.\n *\n * @returns `true` if the file was written, `false` if it already exists and `force` is not set.\n */\nexport function writeSqlOutput(\n sql: string,\n output: string,\n defaultFilename: string,\n force: boolean,\n): boolean {\n let filePath: string;\n if (output.endsWith(\".sql\")) {\n mkdirSync(dirname(output), { recursive: true });\n filePath = output;\n } else {\n mkdirSync(output, { recursive: true });\n filePath = join(output, defaultFilename);\n }\n const existed = existsSync(filePath);\n if (existed && !force) {\n console.error(pc.red(`File already exists: ${filePath}`));\n console.error(pc.dim(\"Use --force to overwrite.\"));\n return false;\n }\n if (existed) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${filePath}`));\n }\n writeFileSync(filePath, sql, \"utf-8\");\n console.log(`${pc.green(\"✓\")} Wrote ${filePath}`);\n return true;\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { OrmAdapter } from \"./types.js\";\n\n/**\n * Detect the ORM adapter from `package.json` dependencies.\n * Prefers drizzle when both are present.\n * Returns `undefined` if neither is found.\n */\nexport function detectOrmAdapter(cwd: string): OrmAdapter | undefined {\n const pkgPath = join(cwd, \"package.json\");\n if (!existsSync(pkgPath)) {\n return undefined;\n }\n\n try {\n const content = readFileSync(pkgPath, \"utf-8\");\n const parsed: unknown = JSON.parse(content);\n if (typeof parsed !== \"object\" || parsed === null) {\n return undefined;\n }\n\n const allDeps: Record<string, unknown> = {};\n if (\"dependencies\" in parsed && typeof parsed.dependencies === \"object\" && parsed.dependencies !== null) {\n Object.assign(allDeps, parsed.dependencies);\n }\n if (\"devDependencies\" in parsed && typeof parsed.devDependencies === \"object\" && parsed.devDependencies !== null) {\n Object.assign(allDeps, parsed.devDependencies);\n }\n\n if (\"drizzle-orm\" in allDeps) {\n return \"drizzle\";\n }\n if (\"@prisma/client\" in allDeps) {\n return \"prisma\";\n }\n } catch {\n // Invalid JSON or read error — skip detection\n }\n\n return undefined;\n}\n","import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Read the `version` field from the `package.json` located one directory above\n * the given `import.meta.url`.\n *\n * Returns `\"0.0.0\"` if the file cannot be read or does not contain a valid version.\n */\nexport function readCliVersion(metaUrl: string): string {\n const cliDir = dirname(fileURLToPath(metaUrl));\n try {\n const raw: unknown = JSON.parse(\n readFileSync(join(cliDir, \"..\", \"package.json\"), \"utf-8\"),\n );\n if (\n typeof raw === \"object\" &&\n raw !== null &&\n \"version\" in raw &&\n typeof raw.version === \"string\"\n ) {\n return raw.version;\n }\n } catch {\n // Read or parse error — fall through to default\n }\n return \"0.0.0\";\n}\n","import pc from \"picocolors\";\nimport type { CheckOutput, PrintCheckOptions } from \"./types.js\";\n\n/**\n * Print structured check results to stdout.\n *\n * When `verbose` is true, every individual check is printed.\n * Failed checks are always shown with details and optional remediation hints.\n * A summary line is always printed at the end.\n */\nexport function printCheckResults(\n output: CheckOutput,\n options?: PrintCheckOptions,\n): void {\n const verbose = options?.verbose === true;\n const failures: typeof output.results = [];\n let passedCount = 0;\n for (const r of output.results) {\n if (r.passed) {\n passedCount++;\n } else {\n failures.push(r);\n }\n }\n const failedCount = failures.length;\n\n // Verbose: show all individual checks\n if (verbose) {\n console.log(\"\");\n for (const r of output.results) {\n const icon = r.passed ? pc.green(\"✓\") : pc.red(\"✗\");\n const detail =\n r.message !== undefined ? pc.dim(` — ${r.message}`) : \"\";\n console.log(` ${icon} ${r.check}${detail}`);\n }\n }\n\n // Warnings\n if (output.warnings.length > 0) {\n console.log(\"\");\n for (const w of output.warnings) {\n console.log(` ${pc.yellow(\"⚠\")} ${w.message ?? w.check}`);\n }\n }\n\n // Summary\n console.log(\"\");\n if (output.passed) {\n console.log(pc.green(`✓ All ${passedCount} checks passed`));\n } else {\n console.log(\n pc.red(\n `✗ ${failedCount} check${failedCount === 1 ? \"\" : \"s\"} failed`,\n ) + pc.dim(`, ${passedCount} passed`),\n );\n\n // Failure details\n console.log(\"\");\n if (verbose) {\n // In verbose mode, individual results were already printed — just show remediation\n for (const f of failures) {\n if (f.remediation !== undefined) {\n console.log(` ${pc.cyan(\"→\")} ${f.check}: ${f.remediation}`);\n }\n }\n } else {\n // Default mode: show full failure details\n for (const f of failures) {\n console.log(` ${pc.red(\"✗\")} ${f.check}`);\n if (f.message !== undefined) {\n console.log(` ${f.message}`);\n }\n if (f.remediation !== undefined) {\n console.log(` ${pc.cyan(\"→\")} ${f.remediation}`);\n }\n }\n }\n }\n}\n","/**\n * Auto-detection utilities for ORM adapter, database dialect, and migration directories.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { detectOrmAdapter, formatTimestamp } from \"@usebetterdev/cli-utils\";\n\nexport { detectOrmAdapter as detectAdapter };\nexport type { OrmAdapter as AdapterType } from \"@usebetterdev/cli-utils\";\n\nexport type DatabaseDialect = \"postgres\" | \"mysql\" | \"sqlite\";\n\n/**\n * Detect the database dialect from a connection URL.\n * Falls back to `\"postgres\"` if the URL is missing or unrecognizable.\n */\nexport function detectDialect(databaseUrl?: string): DatabaseDialect {\n if (databaseUrl === undefined) {\n return \"postgres\";\n }\n\n const lower = databaseUrl.toLowerCase();\n\n if (lower.startsWith(\"postgres://\") || lower.startsWith(\"postgresql://\")) {\n return \"postgres\";\n }\n if (lower.startsWith(\"mysql://\")) {\n return \"mysql\";\n }\n if (lower.startsWith(\"file:\") || lower.endsWith(\".db\") || lower.endsWith(\".sqlite\") || lower.endsWith(\".sqlite3\")) {\n return \"sqlite\";\n }\n\n return \"postgres\";\n}\n\n/**\n * Determine the migration output directory based on the detected adapter.\n *\n * - **Drizzle**: `./drizzle/` (or `./supabase/migrations/` if that directory exists)\n * - **Prisma**: `./prisma/migrations/<timestamp_add_audit_logs>/migration.sql`\n */\nexport function findMigrationDirectory(\n cwd: string,\n adapter: \"drizzle\" | \"prisma\",\n): string {\n if (adapter === \"drizzle\") {\n const supabasePath = join(cwd, \"supabase\", \"migrations\");\n if (existsSync(supabasePath)) {\n return supabasePath;\n }\n return join(cwd, \"drizzle\");\n }\n\n // prisma\n const timestamp = formatTimestamp(new Date(), \"\");\n return join(cwd, \"prisma\", \"migrations\", `${timestamp}_add_audit_logs`);\n}\n"],"mappings":"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","names":["existsSync","join","readFileSync","dirname","pc","join","existsSync","dirname","readFileSync","pc"]}
@@ -1,119 +0,0 @@
1
- // src/generate-sql.ts
2
- import {
3
- AUDIT_LOG_SCHEMA
4
- } from "@usebetterdev/audit-core";
5
- var INDEX_DEFINITIONS = [
6
- {
7
- name: "audit_logs_table_name_timestamp_idx",
8
- columns: ["table_name", "timestamp"]
9
- },
10
- { name: "audit_logs_actor_id_idx", columns: ["actor_id"] },
11
- { name: "audit_logs_record_id_idx", columns: ["record_id"] },
12
- {
13
- name: "audit_logs_table_name_record_id_idx",
14
- columns: ["table_name", "record_id"]
15
- },
16
- { name: "audit_logs_operation_idx", columns: ["operation"] },
17
- { name: "audit_logs_timestamp_idx", columns: ["timestamp"] },
18
- { name: "audit_logs_timestamp_id_idx", columns: ["timestamp", "id"] }
19
- ];
20
- function escapeIdentifier(name, dialect) {
21
- if (dialect === "mysql") {
22
- return `\`${name.replaceAll("`", "``")}\``;
23
- }
24
- return `"${name.replaceAll('"', '""')}"`;
25
- }
26
- function sqlType(type, dialect) {
27
- const map = {
28
- uuid: { postgres: "UUID", mysql: "CHAR(36)", sqlite: "TEXT" },
29
- timestamptz: {
30
- postgres: "TIMESTAMPTZ",
31
- mysql: "DATETIME(6)",
32
- sqlite: "TEXT"
33
- },
34
- text: { postgres: "TEXT", mysql: "TEXT", sqlite: "TEXT" },
35
- jsonb: { postgres: "JSONB", mysql: "JSON", sqlite: "TEXT" },
36
- boolean: { postgres: "BOOLEAN", mysql: "BOOLEAN", sqlite: "INTEGER" }
37
- };
38
- return map[type][dialect];
39
- }
40
- function defaultExpression(expression, dialect) {
41
- const map = {
42
- "gen_random_uuid()": {
43
- postgres: "gen_random_uuid()",
44
- mysql: "(UUID())",
45
- sqlite: void 0
46
- },
47
- "now()": {
48
- postgres: "now()",
49
- mysql: "CURRENT_TIMESTAMP(6)",
50
- sqlite: "(datetime('now'))"
51
- }
52
- };
53
- const entry = map[expression];
54
- if (entry === void 0) {
55
- console.warn(
56
- `[better-audit] Warning: unrecognized default expression "${expression}" \u2014 passing through verbatim for ${dialect}. It may not be dialect-compatible.`
57
- );
58
- return expression;
59
- }
60
- return entry[dialect];
61
- }
62
- function columnDdl(name, definition, dialect) {
63
- const parts = [];
64
- parts.push(escapeIdentifier(name, dialect));
65
- parts.push(sqlType(definition.type, dialect));
66
- if (!definition.nullable) {
67
- parts.push("NOT NULL");
68
- }
69
- if (definition.defaultExpression !== void 0) {
70
- const expr = defaultExpression(definition.defaultExpression, dialect);
71
- if (expr !== void 0) {
72
- parts.push(`DEFAULT ${expr}`);
73
- }
74
- }
75
- return parts.join(" ");
76
- }
77
- function indexDdl(indexDef, dialect) {
78
- const cols = indexDef.columns.map((c) => escapeIdentifier(c, dialect)).join(", ");
79
- const tableName = escapeIdentifier(AUDIT_LOG_SCHEMA.tableName, dialect);
80
- const indexName = escapeIdentifier(indexDef.name, dialect);
81
- return `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${cols});`;
82
- }
83
- function generateMigrationSql(dialect) {
84
- const { tableName, columns } = AUDIT_LOG_SCHEMA;
85
- const quotedTable = escapeIdentifier(tableName, dialect);
86
- const columnLines = [];
87
- let primaryKeyColumn;
88
- for (const [name, definition] of Object.entries(columns)) {
89
- columnLines.push(` ${columnDdl(name, definition, dialect)}`);
90
- if (definition.primaryKey === true) {
91
- primaryKeyColumn = name;
92
- }
93
- }
94
- if (primaryKeyColumn !== void 0) {
95
- columnLines.push(
96
- ` PRIMARY KEY (${escapeIdentifier(primaryKeyColumn, dialect)})`
97
- );
98
- }
99
- const parts = [
100
- `-- better-audit: ${dialect} migration for ${tableName}`,
101
- `-- Generated by @usebetterdev/audit-cli`,
102
- "",
103
- `CREATE TABLE IF NOT EXISTS ${quotedTable} (`,
104
- columnLines.join(",\n"),
105
- ");",
106
- ""
107
- ];
108
- for (const idx of INDEX_DEFINITIONS) {
109
- parts.push(indexDdl(idx, dialect));
110
- }
111
- parts.push("");
112
- return parts.join("\n");
113
- }
114
-
115
- export {
116
- INDEX_DEFINITIONS,
117
- generateMigrationSql
118
- };
119
- //# sourceMappingURL=chunk-O5LHE2AC.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/generate-sql.ts"],"sourcesContent":["/**\n * Multi-dialect SQL generation for the `audit_logs` table.\n *\n * Reads the declarative `AUDIT_LOG_SCHEMA` from audit-core and produces\n * dialect-appropriate DDL for Postgres, MySQL, and SQLite.\n */\n\nimport {\n AUDIT_LOG_SCHEMA,\n type ColumnDefinition,\n type ColumnType,\n} from \"@usebetterdev/audit-core\";\n\nexport type { DatabaseDialect } from \"./detect-adapter.js\";\nimport type { DatabaseDialect } from \"./detect-adapter.js\";\n\n/**\n * Index definitions matching `packages/audit/drizzle/src/schema.ts`.\n * Each entry has a name and the list of column names it covers.\n */\nexport const INDEX_DEFINITIONS: ReadonlyArray<{\n name: string;\n columns: ReadonlyArray<string>;\n}> = [\n {\n name: \"audit_logs_table_name_timestamp_idx\",\n columns: [\"table_name\", \"timestamp\"],\n },\n { name: \"audit_logs_actor_id_idx\", columns: [\"actor_id\"] },\n { name: \"audit_logs_record_id_idx\", columns: [\"record_id\"] },\n {\n name: \"audit_logs_table_name_record_id_idx\",\n columns: [\"table_name\", \"record_id\"],\n },\n { name: \"audit_logs_operation_idx\", columns: [\"operation\"] },\n { name: \"audit_logs_timestamp_idx\", columns: [\"timestamp\"] },\n { name: \"audit_logs_timestamp_id_idx\", columns: [\"timestamp\", \"id\"] },\n];\n\n/** Escape an identifier (table name, column name, index name) for safe SQL interpolation. */\nfunction escapeIdentifier(name: string, dialect: DatabaseDialect): string {\n if (dialect === \"mysql\") {\n return `\\`${name.replaceAll(\"`\", \"``\")}\\``;\n }\n return `\"${name.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Map a core ColumnType to a dialect-specific SQL type. */\nfunction sqlType(type: ColumnType, dialect: DatabaseDialect): string {\n const map: Record<ColumnType, Record<DatabaseDialect, string>> = {\n uuid: { postgres: \"UUID\", mysql: \"CHAR(36)\", sqlite: \"TEXT\" },\n timestamptz: {\n postgres: \"TIMESTAMPTZ\",\n mysql: \"DATETIME(6)\",\n sqlite: \"TEXT\",\n },\n text: { postgres: \"TEXT\", mysql: \"TEXT\", sqlite: \"TEXT\" },\n jsonb: { postgres: \"JSONB\", mysql: \"JSON\", sqlite: \"TEXT\" },\n boolean: { postgres: \"BOOLEAN\", mysql: \"BOOLEAN\", sqlite: \"INTEGER\" },\n };\n return map[type][dialect];\n}\n\n/** Map a core default expression to a dialect-specific SQL expression. */\nfunction defaultExpression(\n expression: string,\n dialect: DatabaseDialect,\n): string | undefined {\n const map: Record<string, Record<DatabaseDialect, string | undefined>> = {\n \"gen_random_uuid()\": {\n postgres: \"gen_random_uuid()\",\n mysql: \"(UUID())\",\n sqlite: undefined,\n },\n \"now()\": {\n postgres: \"now()\",\n mysql: \"CURRENT_TIMESTAMP(6)\",\n sqlite: \"(datetime('now'))\",\n },\n };\n const entry = map[expression];\n if (entry === undefined) {\n console.warn(\n `[better-audit] Warning: unrecognized default expression \"${expression}\" — ` +\n `passing through verbatim for ${dialect}. It may not be dialect-compatible.`,\n );\n return expression;\n }\n return entry[dialect];\n}\n\n/** Build a single column DDL fragment (e.g. `\"id\" UUID NOT NULL DEFAULT gen_random_uuid()`). */\nfunction columnDdl(\n name: string,\n definition: ColumnDefinition,\n dialect: DatabaseDialect,\n): string {\n const parts: string[] = [];\n\n parts.push(escapeIdentifier(name, dialect));\n parts.push(sqlType(definition.type, dialect));\n\n if (!definition.nullable) {\n parts.push(\"NOT NULL\");\n }\n\n if (definition.defaultExpression !== undefined) {\n const expr = defaultExpression(definition.defaultExpression, dialect);\n if (expr !== undefined) {\n parts.push(`DEFAULT ${expr}`);\n }\n }\n\n return parts.join(\" \");\n}\n\n/** Build a single index DDL statement. */\nfunction indexDdl(\n indexDef: { name: string; columns: ReadonlyArray<string> },\n dialect: DatabaseDialect,\n): string {\n const cols = indexDef.columns.map((c) => escapeIdentifier(c, dialect)).join(\", \");\n const tableName = escapeIdentifier(AUDIT_LOG_SCHEMA.tableName, dialect);\n const indexName = escapeIdentifier(indexDef.name, dialect);\n\n return `CREATE INDEX IF NOT EXISTS ${indexName} ON ${tableName} (${cols});`;\n}\n\n/**\n * Generate the full migration SQL for the `audit_logs` table in the given dialect.\n *\n * Includes `CREATE TABLE IF NOT EXISTS` and all indexes.\n */\nexport function generateMigrationSql(dialect: DatabaseDialect): string {\n const { tableName, columns } = AUDIT_LOG_SCHEMA;\n const quotedTable = escapeIdentifier(tableName, dialect);\n\n // Column definitions\n const columnLines: string[] = [];\n let primaryKeyColumn: string | undefined;\n\n for (const [name, definition] of Object.entries(columns)) {\n columnLines.push(` ${columnDdl(name, definition, dialect)}`);\n if (definition.primaryKey === true) {\n primaryKeyColumn = name;\n }\n }\n\n // Primary key constraint\n if (primaryKeyColumn !== undefined) {\n columnLines.push(\n ` PRIMARY KEY (${escapeIdentifier(primaryKeyColumn, dialect)})`,\n );\n }\n\n const parts: string[] = [\n `-- better-audit: ${dialect} migration for ${tableName}`,\n `-- Generated by @usebetterdev/audit-cli`,\n \"\",\n `CREATE TABLE IF NOT EXISTS ${quotedTable} (`,\n columnLines.join(\",\\n\"),\n \");\",\n \"\",\n ];\n\n // Index definitions\n for (const idx of INDEX_DEFINITIONS) {\n parts.push(indexDdl(idx, dialect));\n }\n\n parts.push(\"\");\n return parts.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/export.ts"],"sourcesContent":["/**\n * `better-audit export` — Export audit log entries as CSV or JSON.\n *\n * Connects to the database via Kysely (multi-dialect), builds query filters\n * from CLI flags, and streams results to a file or stdout.\n */\n\nimport { createWriteStream } from \"node:fs\";\nimport pc from \"picocolors\";\nimport { runExport, AuditQueryBuilder, parseDuration } from \"@usebetterdev/audit-core\";\nimport type { AuditSeverity, ExportOptions } from \"@usebetterdev/audit-core\";\nimport { createKyselyInstance, createSqlExecutor } from \"./sql-executor.js\";\nimport { detectDialect } from \"./detect-adapter.js\";\n\nconst VALID_FORMATS = new Set([\"csv\", \"json\"]);\nconst VALID_SEVERITIES: ReadonlySet<string> = new Set([\n \"low\",\n \"medium\",\n \"high\",\n \"critical\",\n]);\n\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T[\\w:.+-]+)?$/;\n\nexport interface ExportCommandOptions {\n format?: string;\n output?: string;\n since?: string;\n severity?: string;\n compliance?: string;\n actor?: string;\n limit?: string;\n databaseUrl?: string;\n}\n\n/** Parse `--format` flag, validate and default to \"csv\". */\nexport function parseFormat(value: string | undefined): \"csv\" | \"json\" {\n if (value === undefined) {\n return \"csv\";\n }\n if (!VALID_FORMATS.has(value)) {\n throw new Error(\n `Invalid format \"${value}\". Expected \"csv\" or \"json\".`,\n );\n }\n return value as \"csv\" | \"json\";\n}\n\n/** Parse `--severity` flag, validate against known values. */\nexport function parseSeverity(value: string): AuditSeverity {\n if (!VALID_SEVERITIES.has(value)) {\n throw new Error(\n `Invalid severity \"${value}\". Expected one of: low, medium, high, critical.`,\n );\n }\n return value as AuditSeverity;\n}\n\n/**\n * Parse `--since` flag. ISO date strings (e.g. \"2025-01-01\") become Date objects.\n * Duration strings (e.g. \"90d\") are returned as-is for the query builder.\n */\nexport function parseSinceValue(value: string): Date | string {\n if (ISO_DATE_REGEX.test(value)) {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) {\n throw new Error(`Invalid date \"${value}\". Expected ISO-8601 format.`);\n }\n return date;\n }\n // Validate as duration — throws if invalid\n parseDuration(value);\n return value;\n}\n\n/**\n * Bridge a single `nodeStream.write()` call into a Promise that respects\n * backpressure and settles exactly once — whichever of error/drain fires first.\n */\nfunction writeToNodeStream(\n nodeStream: NodeJS.WritableStream,\n chunk: string,\n): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n let settled = false;\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n cleanup();\n fn();\n }\n };\n const onError = (err: Error) => { settle(() => reject(err)); };\n const onDrain = () => { settle(resolve); };\n const cleanup = () => {\n nodeStream.removeListener(\"error\", onError);\n nodeStream.removeListener(\"drain\", onDrain);\n };\n\n nodeStream.once(\"error\", onError);\n const canContinue = nodeStream.write(chunk);\n if (canContinue) {\n settle(resolve);\n } else {\n nodeStream.once(\"drain\", onDrain);\n }\n });\n}\n\n/**\n * Create a WHATWG WritableStream<string> that writes to a Node.js file.\n * Handles backpressure by awaiting the drain event.\n */\nexport function createFileWritableStream(\n path: string,\n): WritableStream<string> {\n const nodeStream = createWriteStream(path, { encoding: \"utf-8\" });\n\n return new WritableStream<string>({\n write(chunk) {\n return writeToNodeStream(nodeStream, chunk);\n },\n close() {\n return new Promise<void>((resolve, reject) => {\n const onError = (err: Error) => { reject(err); };\n nodeStream.once(\"error\", onError);\n nodeStream.end(() => {\n nodeStream.removeListener(\"error\", onError);\n resolve();\n });\n });\n },\n abort() {\n nodeStream.destroy();\n },\n });\n}\n\n/** Create a WHATWG WritableStream<string> that writes to process.stdout. */\nexport function createStdoutWritableStream(): WritableStream<string> {\n return new WritableStream<string>({\n write(chunk) {\n return writeToNodeStream(process.stdout, chunk);\n },\n });\n}\n\nexport async function exportLogs(options: ExportCommandOptions): Promise<void> {\n // 1. Resolve DATABASE_URL\n const databaseUrl = options.databaseUrl ?? process.env[\"DATABASE_URL\"];\n if (databaseUrl === undefined || databaseUrl === \"\") {\n throw new Error(\n \"DATABASE_URL is required. Set the DATABASE_URL environment variable or pass --database-url.\",\n );\n }\n\n // 2. Parse flags\n const format = parseFormat(options.format);\n\n // 3. Detect dialect + create Kysely instance\n const dialect = detectDialect(databaseUrl);\n const db = await createKyselyInstance(databaseUrl, dialect);\n\n try {\n const executor = createSqlExecutor(db, dialect);\n\n // 4. Build query\n let query: AuditQueryBuilder | undefined;\n const hasFilters =\n options.since !== undefined ||\n options.severity !== undefined ||\n options.compliance !== undefined ||\n options.actor !== undefined ||\n options.limit !== undefined;\n\n if (hasFilters) {\n let builder = new AuditQueryBuilder(executor);\n\n if (options.since !== undefined) {\n const sinceValue = parseSinceValue(options.since);\n builder = builder.since(sinceValue);\n }\n\n if (options.severity !== undefined) {\n const severity = parseSeverity(options.severity);\n builder = builder.severity(severity);\n }\n\n if (options.compliance !== undefined) {\n const tags = options.compliance.split(\",\").map((t) => t.trim());\n builder = builder.compliance(...tags);\n }\n\n if (options.actor !== undefined) {\n builder = builder.actor(options.actor);\n }\n\n if (options.limit !== undefined) {\n const n = Number(options.limit);\n if (Number.isNaN(n) || n <= 0 || !Number.isInteger(n)) {\n throw new Error(\n `Invalid limit \"${options.limit}\". Expected a positive integer.`,\n );\n }\n builder = builder.limit(n);\n }\n\n query = builder;\n }\n\n // 5. Create output sink\n const outputPath = options.output;\n const output: WritableStream<string> = outputPath !== undefined\n ? createFileWritableStream(outputPath)\n : createStdoutWritableStream();\n\n // 6. Run export\n const exportOptions: ExportOptions = {\n format,\n output,\n ...(query !== undefined && { query }),\n };\n\n const result = await runExport(executor, exportOptions);\n\n // 7. Summary message\n if (outputPath !== undefined) {\n console.error(\n `${pc.green(\"✓\")} Exported ${result.rowCount} rows to ${pc.dim(outputPath)}`,\n );\n } else {\n console.error(\n `${pc.green(\"✓\")} Exported ${result.rowCount} rows`,\n );\n }\n } finally {\n await db.destroy();\n }\n}\n"],"mappings":"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","names":[]}