@usebetterdev/audit-cli 0.6.1 → 0.7.0
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.
- package/dist/check.d.ts +4 -13
- package/dist/check.js +2 -2
- package/dist/{chunk-WVH5TQ2O.js → chunk-6HCQ22YY.js} +6 -6
- package/dist/chunk-6HCQ22YY.js.map +1 -0
- package/dist/{chunk-55KKYFKR.js → chunk-AURUOQDN.js} +5 -52
- package/dist/chunk-AURUOQDN.js.map +1 -0
- package/dist/{chunk-BKCJJOQN.js → chunk-HV3X7C45.js} +2 -2
- package/dist/chunk-IJDJMOCO.js +154 -0
- package/dist/chunk-IJDJMOCO.js.map +1 -0
- package/dist/{chunk-M46VJ3FO.js → chunk-TLZWAZAJ.js} +2 -2
- package/dist/cli.js +8 -13
- package/dist/cli.js.map +1 -1
- package/dist/{detect-adapter-DNHcPCKz.d.ts → detect-adapter-WIB0ARFR.d.ts} +2 -2
- package/dist/export.js +2 -2
- package/dist/migrate.d.ts +3 -2
- package/dist/migrate.js +2 -2
- package/dist/purge.js +2 -2
- package/package.json +8 -6
- package/dist/chunk-55KKYFKR.js.map +0 -1
- package/dist/chunk-HDO5P6X7.js +0 -77
- package/dist/chunk-HDO5P6X7.js.map +0 -1
- package/dist/chunk-WVH5TQ2O.js.map +0 -1
- /package/dist/{chunk-BKCJJOQN.js.map → chunk-HV3X7C45.js.map} +0 -0
- /package/dist/{chunk-M46VJ3FO.js.map → chunk-TLZWAZAJ.js.map} +0 -0
package/dist/check.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { CheckOutput, CheckResult } from '@usebetterdev/cli-utils';
|
|
2
|
+
export { CheckOutput, CheckResult } from '@usebetterdev/cli-utils';
|
|
1
3
|
import { ColumnType } from '@usebetterdev/audit-core';
|
|
2
|
-
import { D as DatabaseDialect } from './detect-adapter-
|
|
4
|
+
import { D as DatabaseDialect } from './detect-adapter-WIB0ARFR.js';
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* `better-audit check` — Verify audit_logs table schema, indexes, and write path.
|
|
@@ -20,17 +22,6 @@ interface CheckOptions {
|
|
|
20
22
|
databaseUrl?: string | undefined;
|
|
21
23
|
verbose?: boolean | undefined;
|
|
22
24
|
}
|
|
23
|
-
interface CheckResult {
|
|
24
|
-
check: string;
|
|
25
|
-
passed: boolean;
|
|
26
|
-
message?: string;
|
|
27
|
-
remediation?: string;
|
|
28
|
-
}
|
|
29
|
-
interface CheckOutput {
|
|
30
|
-
passed: boolean;
|
|
31
|
-
results: CheckResult[];
|
|
32
|
-
warnings: CheckResult[];
|
|
33
|
-
}
|
|
34
25
|
/** Thrown when one or more checks fail. Carries the full output for CLI rendering. */
|
|
35
26
|
declare class CheckFailedError extends Error {
|
|
36
27
|
readonly output: CheckOutput;
|
|
@@ -61,4 +52,4 @@ declare function runCheck(databaseUrl: string): Promise<CheckOutput>;
|
|
|
61
52
|
/** CLI action handler for `better-audit check`. */
|
|
62
53
|
declare function check(options?: CheckOptions): Promise<void>;
|
|
63
54
|
|
|
64
|
-
export { CheckFailedError, type CheckOptions,
|
|
55
|
+
export { CheckFailedError, type CheckOptions, DIALECT_TYPE_MAP, type IntrospectedColumn as _IntrospectedColumn, checkColumns as _checkColumns, checkIndexes as _checkIndexes, check, runCheck };
|
package/dist/check.js
CHANGED
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
checkColumns,
|
|
6
6
|
checkIndexes,
|
|
7
7
|
runCheck
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-AURUOQDN.js";
|
|
9
9
|
import "./chunk-O5LHE2AC.js";
|
|
10
10
|
import "./chunk-7GSN73TA.js";
|
|
11
|
-
import "./chunk-
|
|
11
|
+
import "./chunk-IJDJMOCO.js";
|
|
12
12
|
export {
|
|
13
13
|
CheckFailedError,
|
|
14
14
|
DIALECT_TYPE_MAP,
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
generateMigrationSql
|
|
3
3
|
} from "./chunk-O5LHE2AC.js";
|
|
4
4
|
import {
|
|
5
|
-
detectAdapter,
|
|
6
5
|
detectDialect,
|
|
6
|
+
detectOrmAdapter,
|
|
7
7
|
findMigrationDirectory,
|
|
8
8
|
formatTimestamp
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IJDJMOCO.js";
|
|
10
10
|
|
|
11
11
|
// src/migrate.ts
|
|
12
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
@@ -52,7 +52,7 @@ async function migrate(options = {}) {
|
|
|
52
52
|
process.stdout.write(sql);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
const adapter = options.adapter ??
|
|
55
|
+
const adapter = options.adapter ?? detectOrmAdapter(cwd);
|
|
56
56
|
if (adapter === void 0) {
|
|
57
57
|
throw new Error(
|
|
58
58
|
"Could not detect ORM adapter. Install drizzle-orm or @prisma/client, or pass --adapter drizzle|prisma."
|
|
@@ -66,12 +66,12 @@ async function migrate(options = {}) {
|
|
|
66
66
|
outputPath = dirname(filePath);
|
|
67
67
|
} else {
|
|
68
68
|
outputPath = options.output;
|
|
69
|
-
filePath = adapter === "prisma" ? join(outputPath, "migration.sql") : join(outputPath, `${formatTimestamp(/* @__PURE__ */ new Date())}_audit_logs.sql`);
|
|
69
|
+
filePath = adapter === "prisma" ? join(outputPath, "migration.sql") : join(outputPath, `${formatTimestamp(/* @__PURE__ */ new Date(), "")}_audit_logs.sql`);
|
|
70
70
|
}
|
|
71
71
|
} else {
|
|
72
72
|
const migrationDir = findMigrationDirectory(cwd, adapter);
|
|
73
73
|
outputPath = migrationDir;
|
|
74
|
-
filePath = adapter === "prisma" ? join(migrationDir, "migration.sql") : join(migrationDir, `${formatTimestamp(/* @__PURE__ */ new Date())}_audit_logs.sql`);
|
|
74
|
+
filePath = adapter === "prisma" ? join(migrationDir, "migration.sql") : join(migrationDir, `${formatTimestamp(/* @__PURE__ */ new Date(), "")}_audit_logs.sql`);
|
|
75
75
|
}
|
|
76
76
|
const scanDir = adapter === "prisma" ? join(cwd, "prisma", "migrations") : outputPath;
|
|
77
77
|
if (migrationAlreadyExists(scanDir)) {
|
|
@@ -98,4 +98,4 @@ async function migrate(options = {}) {
|
|
|
98
98
|
export {
|
|
99
99
|
migrate
|
|
100
100
|
};
|
|
101
|
-
//# sourceMappingURL=chunk-
|
|
101
|
+
//# sourceMappingURL=chunk-6HCQ22YY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["/**\n * `better-audit migrate` — Generate the audit_logs table migration.\n *\n * Generates appropriate DDL for the configured database:\n * - Postgres: JSONB columns, TIMESTAMPTZ, gen_random_uuid()\n * - MySQL: JSON columns, DATETIME(6), UUID() default\n * - SQLite: TEXT columns for JSON, no UUID default\n *\n * Writes the migration file into the appropriate ORM directory,\n * or prints to stdout with --dry-run.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport pc from \"picocolors\";\nimport { generateMigrationSql, type DatabaseDialect } from \"./generate-sql.js\";\nimport { formatTimestamp } from \"@usebetterdev/cli-utils\";\nimport {\n detectAdapter,\n detectDialect,\n findMigrationDirectory,\n type AdapterType,\n} from \"./detect-adapter.js\";\n\nexport interface MigrateOptions {\n /** Output SQL to stdout instead of writing a file */\n dryRun?: boolean | undefined;\n /** Override ORM auto-detection */\n adapter?: AdapterType | undefined;\n /** Override database dialect detection */\n dialect?: DatabaseDialect | undefined;\n /** Override output directory/file */\n output?: string | undefined;\n /** Working directory (defaults to process.cwd()) */\n cwd?: string | undefined;\n /** Database connection URL for dialect detection (defaults to DATABASE_URL env) */\n databaseUrl?: string | undefined;\n}\n\n/** Regex to detect an existing audit_logs CREATE TABLE statement (case-insensitive). */\nconst AUDIT_TABLE_REGEX = /create\\s+table\\b[^;]*\\baudit_logs\\b/i;\n\n/** Maximum directory depth for the idempotency scan. */\nconst MAX_SCAN_DEPTH = 3;\n\n/**\n * Check if a migration for `audit_logs` already exists in the given directory.\n * Scans `.sql` files up to `MAX_SCAN_DEPTH` levels deep, skipping symlinks.\n */\nfunction migrationAlreadyExists(directory: string, depth: number = 0): boolean {\n if (depth > MAX_SCAN_DEPTH) {\n return false;\n }\n if (!existsSync(directory)) {\n return false;\n }\n\n try {\n const entries = readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) {\n continue;\n }\n const fullPath = join(directory, entry.name);\n if (entry.isFile() && entry.name.endsWith(\".sql\")) {\n const content = readFileSync(fullPath, \"utf-8\");\n if (AUDIT_TABLE_REGEX.test(content)) {\n return true;\n }\n }\n if (entry.isDirectory()) {\n if (migrationAlreadyExists(fullPath, depth + 1)) {\n return true;\n }\n }\n }\n } catch {\n // Read error — treat as no existing migration\n }\n\n return false;\n}\n\nexport async function migrate(options: MigrateOptions = {}): Promise<void> {\n const cwd = options.cwd ?? process.cwd();\n\n // 1. Resolve dialect\n const dialect: DatabaseDialect =\n options.dialect ?? detectDialect(options.databaseUrl ?? process.env[\"DATABASE_URL\"]);\n\n // 2. Generate SQL\n const sql = generateMigrationSql(dialect);\n\n // 3. Dry-run: print to stdout and return\n if (options.dryRun === true) {\n process.stdout.write(sql);\n return;\n }\n\n // 4. Resolve adapter\n const adapter: AdapterType | undefined =\n options.adapter ?? detectAdapter(cwd);\n\n if (adapter === undefined) {\n throw new Error(\n \"Could not detect ORM adapter. Install drizzle-orm or @prisma/client, \" +\n \"or pass --adapter drizzle|prisma.\",\n );\n }\n\n // 5. Resolve output directory\n let outputPath: string;\n let filePath: string;\n\n if (options.output !== undefined) {\n if (options.output.endsWith(\".sql\")) {\n filePath = options.output;\n outputPath = dirname(filePath);\n } else {\n outputPath = options.output;\n filePath =\n adapter === \"prisma\"\n ? join(outputPath, \"migration.sql\")\n : join(outputPath, `${formatTimestamp(new Date(), \"\")}_audit_logs.sql`);\n }\n } else {\n const migrationDir = findMigrationDirectory(cwd, adapter);\n outputPath = migrationDir;\n filePath =\n adapter === \"prisma\"\n ? join(migrationDir, \"migration.sql\")\n : join(migrationDir, `${formatTimestamp(new Date(), \"\")}_audit_logs.sql`);\n }\n\n // 6. Check idempotency — scan parent migration directory\n const scanDir =\n adapter === \"prisma\"\n ? join(cwd, \"prisma\", \"migrations\")\n : outputPath;\n\n if (migrationAlreadyExists(scanDir)) {\n console.log(\n pc.green(\"✓\") + \" audit_logs migration already exists — up to date.\",\n );\n return;\n }\n\n // 7. Warn if output file already exists (explicit --output path)\n if (options.output !== undefined && existsSync(filePath)) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${filePath}`));\n }\n\n // 8. Write migration file\n mkdirSync(outputPath, { recursive: true });\n writeFileSync(filePath, sql, \"utf-8\");\n\n console.log(`${pc.green(\"✓\")} Wrote migration: ${pc.dim(filePath)}`);\n console.log(\"\");\n console.log(pc.bold(\"Next steps:\"));\n\n if (adapter === \"drizzle\") {\n console.log(` ${pc.yellow(\"$\")} npx drizzle-kit migrate`);\n } else {\n console.log(` ${pc.yellow(\"$\")} npx prisma migrate dev`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
createKyselyInstance
|
|
6
6
|
} from "./chunk-7GSN73TA.js";
|
|
7
7
|
import {
|
|
8
|
-
detectDialect
|
|
9
|
-
|
|
8
|
+
detectDialect,
|
|
9
|
+
printCheckResults
|
|
10
|
+
} from "./chunk-IJDJMOCO.js";
|
|
10
11
|
|
|
11
12
|
// src/check.ts
|
|
12
13
|
import { randomUUID } from "crypto";
|
|
13
14
|
import { sql } from "kysely";
|
|
14
|
-
import pc from "picocolors";
|
|
15
15
|
import { AUDIT_LOG_SCHEMA } from "@usebetterdev/audit-core";
|
|
16
16
|
var CheckFailedError = class extends Error {
|
|
17
17
|
output;
|
|
@@ -344,58 +344,11 @@ async function check(options = {}) {
|
|
|
344
344
|
}
|
|
345
345
|
const output = await runCheck(databaseUrl);
|
|
346
346
|
const verbose = options.verbose === true;
|
|
347
|
-
|
|
347
|
+
printCheckResults(output, { verbose });
|
|
348
348
|
if (!output.passed) {
|
|
349
349
|
throw new CheckFailedError(output);
|
|
350
350
|
}
|
|
351
351
|
}
|
|
352
|
-
function printCheckOutput(output, verbose) {
|
|
353
|
-
const passedCount = output.results.filter((r) => r.passed).length;
|
|
354
|
-
const failedCount = output.results.length - passedCount;
|
|
355
|
-
const failures = output.results.filter((r) => !r.passed);
|
|
356
|
-
if (verbose) {
|
|
357
|
-
console.log("");
|
|
358
|
-
for (const r of output.results) {
|
|
359
|
-
const icon = r.passed ? pc.green("\u2713") : pc.red("\u2717");
|
|
360
|
-
const detail = r.message !== void 0 ? pc.dim(` \u2014 ${r.message}`) : "";
|
|
361
|
-
console.log(` ${icon} ${r.check}${detail}`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (output.warnings.length > 0) {
|
|
365
|
-
console.log("");
|
|
366
|
-
for (const w of output.warnings) {
|
|
367
|
-
console.log(` ${pc.yellow("\u26A0")} ${w.message ?? w.check}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
console.log("");
|
|
371
|
-
if (output.passed) {
|
|
372
|
-
console.log(pc.green(`\u2713 All ${passedCount} checks passed`));
|
|
373
|
-
} else {
|
|
374
|
-
console.log(
|
|
375
|
-
pc.red(
|
|
376
|
-
`\u2717 ${failedCount} check${failedCount === 1 ? "" : "s"} failed`
|
|
377
|
-
) + pc.dim(`, ${passedCount} passed`)
|
|
378
|
-
);
|
|
379
|
-
console.log("");
|
|
380
|
-
if (verbose) {
|
|
381
|
-
for (const f of failures) {
|
|
382
|
-
if (f.remediation !== void 0) {
|
|
383
|
-
console.log(` ${pc.cyan("\u2192")} ${f.check}: ${f.remediation}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
for (const f of failures) {
|
|
388
|
-
console.log(` ${pc.red("\u2717")} ${f.check}`);
|
|
389
|
-
if (f.message !== void 0) {
|
|
390
|
-
console.log(` ${f.message}`);
|
|
391
|
-
}
|
|
392
|
-
if (f.remediation !== void 0) {
|
|
393
|
-
console.log(` ${pc.cyan("\u2192")} ${f.remediation}`);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
352
|
|
|
400
353
|
export {
|
|
401
354
|
CheckFailedError,
|
|
@@ -405,4 +358,4 @@ export {
|
|
|
405
358
|
runCheck,
|
|
406
359
|
check
|
|
407
360
|
};
|
|
408
|
-
//# sourceMappingURL=chunk-
|
|
361
|
+
//# sourceMappingURL=chunk-AURUOQDN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-7GSN73TA.js";
|
|
4
4
|
import {
|
|
5
5
|
detectDialect
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-IJDJMOCO.js";
|
|
7
7
|
|
|
8
8
|
// src/purge.ts
|
|
9
9
|
import pc from "picocolors";
|
|
@@ -179,4 +179,4 @@ export {
|
|
|
179
179
|
formatDuration,
|
|
180
180
|
purge
|
|
181
181
|
};
|
|
182
|
-
//# sourceMappingURL=chunk-
|
|
182
|
+
//# sourceMappingURL=chunk-HV3X7C45.js.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// ../../shared/cli-utils/dist/index.js
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
4
|
+
import { join as join2 } from "path";
|
|
5
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
6
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import pc2 from "picocolors";
|
|
9
|
+
function formatTimestamp(date, separator = "_") {
|
|
10
|
+
const y = date.getFullYear();
|
|
11
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
12
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
13
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
14
|
+
const min = String(date.getMinutes()).padStart(2, "0");
|
|
15
|
+
const sec = String(date.getSeconds()).padStart(2, "0");
|
|
16
|
+
return `${y}${m}${d}${separator}${h}${min}${sec}`;
|
|
17
|
+
}
|
|
18
|
+
function detectOrmAdapter(cwd) {
|
|
19
|
+
const pkgPath = join2(cwd, "package.json");
|
|
20
|
+
if (!existsSync2(pkgPath)) {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(pkgPath, "utf-8");
|
|
25
|
+
const parsed = JSON.parse(content);
|
|
26
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
const allDeps = {};
|
|
30
|
+
if ("dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
|
|
31
|
+
Object.assign(allDeps, parsed.dependencies);
|
|
32
|
+
}
|
|
33
|
+
if ("devDependencies" in parsed && typeof parsed.devDependencies === "object" && parsed.devDependencies !== null) {
|
|
34
|
+
Object.assign(allDeps, parsed.devDependencies);
|
|
35
|
+
}
|
|
36
|
+
if ("drizzle-orm" in allDeps) {
|
|
37
|
+
return "drizzle";
|
|
38
|
+
}
|
|
39
|
+
if ("@prisma/client" in allDeps) {
|
|
40
|
+
return "prisma";
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
function readCliVersion(metaUrl) {
|
|
47
|
+
const cliDir = dirname2(fileURLToPath(metaUrl));
|
|
48
|
+
try {
|
|
49
|
+
const raw = JSON.parse(
|
|
50
|
+
readFileSync2(join3(cliDir, "..", "package.json"), "utf-8")
|
|
51
|
+
);
|
|
52
|
+
if (typeof raw === "object" && raw !== null && "version" in raw && typeof raw.version === "string") {
|
|
53
|
+
return raw.version;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
return "0.0.0";
|
|
58
|
+
}
|
|
59
|
+
function printCheckResults(output, options) {
|
|
60
|
+
const verbose = options?.verbose === true;
|
|
61
|
+
const failures = [];
|
|
62
|
+
let passedCount = 0;
|
|
63
|
+
for (const r of output.results) {
|
|
64
|
+
if (r.passed) {
|
|
65
|
+
passedCount++;
|
|
66
|
+
} else {
|
|
67
|
+
failures.push(r);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const failedCount = failures.length;
|
|
71
|
+
if (verbose) {
|
|
72
|
+
console.log("");
|
|
73
|
+
for (const r of output.results) {
|
|
74
|
+
const icon = r.passed ? pc2.green("\u2713") : pc2.red("\u2717");
|
|
75
|
+
const detail = r.message !== void 0 ? pc2.dim(` \u2014 ${r.message}`) : "";
|
|
76
|
+
console.log(` ${icon} ${r.check}${detail}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (output.warnings.length > 0) {
|
|
80
|
+
console.log("");
|
|
81
|
+
for (const w of output.warnings) {
|
|
82
|
+
console.log(` ${pc2.yellow("\u26A0")} ${w.message ?? w.check}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log("");
|
|
86
|
+
if (output.passed) {
|
|
87
|
+
console.log(pc2.green(`\u2713 All ${passedCount} checks passed`));
|
|
88
|
+
} else {
|
|
89
|
+
console.log(
|
|
90
|
+
pc2.red(
|
|
91
|
+
`\u2717 ${failedCount} check${failedCount === 1 ? "" : "s"} failed`
|
|
92
|
+
) + pc2.dim(`, ${passedCount} passed`)
|
|
93
|
+
);
|
|
94
|
+
console.log("");
|
|
95
|
+
if (verbose) {
|
|
96
|
+
for (const f of failures) {
|
|
97
|
+
if (f.remediation !== void 0) {
|
|
98
|
+
console.log(` ${pc2.cyan("\u2192")} ${f.check}: ${f.remediation}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
for (const f of failures) {
|
|
103
|
+
console.log(` ${pc2.red("\u2717")} ${f.check}`);
|
|
104
|
+
if (f.message !== void 0) {
|
|
105
|
+
console.log(` ${f.message}`);
|
|
106
|
+
}
|
|
107
|
+
if (f.remediation !== void 0) {
|
|
108
|
+
console.log(` ${pc2.cyan("\u2192")} ${f.remediation}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/detect-adapter.ts
|
|
116
|
+
import { existsSync } from "fs";
|
|
117
|
+
import { join } from "path";
|
|
118
|
+
function detectDialect(databaseUrl) {
|
|
119
|
+
if (databaseUrl === void 0) {
|
|
120
|
+
return "postgres";
|
|
121
|
+
}
|
|
122
|
+
const lower = databaseUrl.toLowerCase();
|
|
123
|
+
if (lower.startsWith("postgres://") || lower.startsWith("postgresql://")) {
|
|
124
|
+
return "postgres";
|
|
125
|
+
}
|
|
126
|
+
if (lower.startsWith("mysql://")) {
|
|
127
|
+
return "mysql";
|
|
128
|
+
}
|
|
129
|
+
if (lower.startsWith("file:") || lower.endsWith(".db") || lower.endsWith(".sqlite") || lower.endsWith(".sqlite3")) {
|
|
130
|
+
return "sqlite";
|
|
131
|
+
}
|
|
132
|
+
return "postgres";
|
|
133
|
+
}
|
|
134
|
+
function findMigrationDirectory(cwd, adapter) {
|
|
135
|
+
if (adapter === "drizzle") {
|
|
136
|
+
const supabasePath = join(cwd, "supabase", "migrations");
|
|
137
|
+
if (existsSync(supabasePath)) {
|
|
138
|
+
return supabasePath;
|
|
139
|
+
}
|
|
140
|
+
return join(cwd, "drizzle");
|
|
141
|
+
}
|
|
142
|
+
const timestamp = formatTimestamp(/* @__PURE__ */ new Date(), "");
|
|
143
|
+
return join(cwd, "prisma", "migrations", `${timestamp}_add_audit_logs`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
formatTimestamp,
|
|
148
|
+
detectOrmAdapter,
|
|
149
|
+
readCliVersion,
|
|
150
|
+
printCheckResults,
|
|
151
|
+
detectDialect,
|
|
152
|
+
findMigrationDirectory
|
|
153
|
+
};
|
|
154
|
+
//# sourceMappingURL=chunk-IJDJMOCO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-7GSN73TA.js";
|
|
5
5
|
import {
|
|
6
6
|
detectDialect
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IJDJMOCO.js";
|
|
8
8
|
|
|
9
9
|
// src/export.ts
|
|
10
10
|
import { createWriteStream } from "fs";
|
|
@@ -179,4 +179,4 @@ export {
|
|
|
179
179
|
createStdoutWritableStream,
|
|
180
180
|
exportLogs
|
|
181
181
|
};
|
|
182
|
-
//# sourceMappingURL=chunk-
|
|
182
|
+
//# sourceMappingURL=chunk-TLZWAZAJ.js.map
|
package/dist/cli.js
CHANGED
|
@@ -2,34 +2,29 @@
|
|
|
2
2
|
import {
|
|
3
3
|
CheckFailedError,
|
|
4
4
|
check
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-AURUOQDN.js";
|
|
6
6
|
import {
|
|
7
7
|
exportLogs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TLZWAZAJ.js";
|
|
9
9
|
import {
|
|
10
10
|
migrate
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-6HCQ22YY.js";
|
|
12
12
|
import "./chunk-O5LHE2AC.js";
|
|
13
13
|
import {
|
|
14
14
|
purge
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-HV3X7C45.js";
|
|
16
16
|
import "./chunk-7GSN73TA.js";
|
|
17
|
-
import
|
|
17
|
+
import {
|
|
18
|
+
readCliVersion
|
|
19
|
+
} from "./chunk-IJDJMOCO.js";
|
|
18
20
|
import {
|
|
19
21
|
stats
|
|
20
22
|
} from "./chunk-UASMKVFP.js";
|
|
21
23
|
|
|
22
24
|
// src/cli.ts
|
|
23
|
-
import { readFileSync } from "fs";
|
|
24
|
-
import { dirname, join } from "path";
|
|
25
|
-
import { fileURLToPath } from "url";
|
|
26
25
|
import { Command } from "commander";
|
|
27
26
|
import pc from "picocolors";
|
|
28
|
-
var
|
|
29
|
-
var pkgRaw = JSON.parse(
|
|
30
|
-
readFileSync(join(cliDir, "..", "package.json"), "utf-8")
|
|
31
|
-
);
|
|
32
|
-
var cliVersion = typeof pkgRaw === "object" && pkgRaw !== null && "version" in pkgRaw && typeof pkgRaw.version === "string" ? pkgRaw.version : "0.0.0";
|
|
27
|
+
var cliVersion = readCliVersion(import.meta.url);
|
|
33
28
|
function parseAdapter(value) {
|
|
34
29
|
if (value === "drizzle" || value === "prisma" || value === void 0) {
|
|
35
30
|
return value;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { migrate } from \"./migrate.js\";\nimport { check, CheckFailedError } from \"./check.js\";\nimport { stats } from \"./stats.js\";\nimport { purge } from \"./purge.js\";\nimport { exportLogs } from \"./export.js\";\n\n// Read version from package.json at the package root\nconst cliDir = dirname(fileURLToPath(import.meta.url));\nconst pkgRaw: unknown = JSON.parse(\n readFileSync(join(cliDir, \"..\", \"package.json\"), \"utf-8\"),\n);\nconst cliVersion =\n typeof pkgRaw === \"object\" &&\n pkgRaw !== null &&\n \"version\" in pkgRaw &&\n typeof pkgRaw.version === \"string\"\n ? pkgRaw.version\n : \"0.0.0\";\n\n/** Validate and narrow the --adapter flag value. */\nfunction parseAdapter(\n value: string | undefined,\n): \"drizzle\" | \"prisma\" | undefined {\n if (value === \"drizzle\" || value === \"prisma\" || value === undefined) {\n return value;\n }\n throw new Error(\"--adapter must be 'drizzle' or 'prisma'\");\n}\n\n/** Validate and narrow the --dialect flag value. */\nfunction parseDialect(\n value: string | undefined,\n): \"postgres\" | \"mysql\" | \"sqlite\" | undefined {\n if (\n value === \"postgres\" ||\n value === \"mysql\" ||\n value === \"sqlite\" ||\n value === undefined\n ) {\n return value;\n }\n throw new Error(\"--dialect must be 'postgres', 'mysql', or 'sqlite'\");\n}\n\n/** Extract a human-readable message from an error, including AggregateError nested errors. */\nfunction formatErrorMessage(err: unknown): string {\n if (err instanceof AggregateError && err.errors.length > 0) {\n // AggregateError (e.g. from pg connection failure) has an empty .message\n // but contains nested errors with the actual details\n const nested = err.errors\n .map((e: unknown) => (e instanceof Error ? e.message : String(e)))\n .join(\"; \");\n return err.message !== \"\" ? `${err.message}: ${nested}` : nested;\n }\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\n/** Wrap a subcommand action with consistent error handling. */\nasync function runAction(fn: () => Promise<void>): Promise<void> {\n try {\n await fn();\n } catch (err) {\n // CheckFailedError already printed its output — just exit\n if (err instanceof CheckFailedError) {\n process.exit(1);\n }\n const message = formatErrorMessage(err);\n console.error(pc.red(message));\n process.exit(1);\n }\n}\n\nconst program = new Command()\n .name(\"@usebetterdev/audit-cli\")\n .description(\"CLI for @usebetterdev/audit — compliance-ready audit logging\")\n .version(cliVersion);\n\nprogram\n .command(\"migrate\")\n .description(\"Generate the audit_logs table migration\")\n .option(\n \"--dry-run\",\n \"Print SQL to stdout without writing a file\",\n )\n .option(\n \"--adapter <adapter>\",\n \"ORM adapter: drizzle or prisma (auto-detected from package.json)\",\n )\n .option(\n \"--dialect <dialect>\",\n \"Database dialect: postgres, mysql, or sqlite (auto-detected from DATABASE_URL)\",\n )\n .option(\"-o, --output <path>\", \"Output directory or .sql file path\")\n .action(\n async (opts: {\n dryRun?: boolean;\n adapter?: string;\n dialect?: string;\n output?: string;\n }) => {\n await runAction(() =>\n migrate({\n dryRun: opts.dryRun,\n adapter: parseAdapter(opts.adapter),\n dialect: parseDialect(opts.dialect),\n output: opts.output,\n }),\n );\n },\n );\n\nprogram\n .command(\"check\")\n .description(\"Verify the audit_logs table and ORM adapter are working\")\n .option(\"--verbose\", \"Show detailed results for each check\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(async (opts: { verbose?: boolean; databaseUrl?: string }) => {\n await runAction(() =>\n check({ verbose: opts.verbose, databaseUrl: opts.databaseUrl }),\n );\n });\n\nprogram\n .command(\"stats\")\n .description(\"Show audit log statistics\")\n .option(\"--since <period>\", \"Time window (e.g. 30d)\", \"30d\")\n .action(async (opts: { since: string }) => {\n await runAction(() => stats({ since: opts.since }));\n });\n\nprogram\n .command(\"purge\")\n .description(\"Delete audit logs older than the retention period\")\n .option(\"--dry-run\", \"Preview rows to be deleted without deleting them\")\n .option(\n \"--since <value>\",\n 'ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\") — overrides config',\n )\n .option(\"--batch-size <n>\", \"Rows per DELETE batch (default: 1000)\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .option(\"-y, --yes\", \"Skip confirmation prompt (required for live deletion)\")\n .action(\n async (opts: {\n dryRun?: boolean;\n since?: string;\n batchSize?: string;\n databaseUrl?: string;\n yes?: boolean;\n }) => {\n await runAction(async () => {\n const purgeOpts: Parameters<typeof purge>[0] = {};\n if (opts.dryRun !== undefined) {\n purgeOpts.dryRun = opts.dryRun;\n }\n if (opts.since !== undefined) {\n purgeOpts.since = opts.since;\n }\n if (opts.batchSize !== undefined) {\n const n = Number(opts.batchSize);\n if (!Number.isInteger(n) || n <= 0) {\n throw new Error(\n `Invalid --batch-size \"${opts.batchSize}\". Expected a positive integer.`,\n );\n }\n purgeOpts.batchSize = n;\n }\n if (opts.databaseUrl !== undefined) {\n purgeOpts.databaseUrl = opts.databaseUrl;\n }\n if (opts.yes !== undefined) {\n purgeOpts.yes = opts.yes;\n }\n await purge(purgeOpts);\n });\n },\n );\n\nprogram\n .command(\"export\")\n .description(\"Export audit log entries as CSV or JSON\")\n .option(\"--format <format>\", \"Output format: csv or json (default: csv)\")\n .option(\"-o, --output <path>\", \"Output file path (default: stdout)\")\n .option(\n \"--since <value>\",\n 'Duration (e.g. \"90d\") or ISO date (e.g. \"2025-01-01\")',\n )\n .option(\n \"--severity <level>\",\n \"Filter by severity: low, medium, high, or critical\",\n )\n .option(\n \"--compliance <tags>\",\n 'Comma-separated compliance tags (e.g. \"soc2:access-control,gdpr\")',\n )\n .option(\"--actor <id>\", \"Filter by actor ID\")\n .option(\"--limit <n>\", \"Maximum number of rows to export\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(\n async (opts: {\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 await runAction(() => exportLogs(opts));\n },\n );\n\nprogram.parseAsync(process.argv).catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport { readCliVersion } from \"@usebetterdev/cli-utils\";\nimport { migrate } from \"./migrate.js\";\nimport { check, CheckFailedError } from \"./check.js\";\nimport { stats } from \"./stats.js\";\nimport { purge } from \"./purge.js\";\nimport { exportLogs } from \"./export.js\";\n\nconst cliVersion = readCliVersion(import.meta.url);\n\n/** Validate and narrow the --adapter flag value. */\nfunction parseAdapter(\n value: string | undefined,\n): \"drizzle\" | \"prisma\" | undefined {\n if (value === \"drizzle\" || value === \"prisma\" || value === undefined) {\n return value;\n }\n throw new Error(\"--adapter must be 'drizzle' or 'prisma'\");\n}\n\n/** Validate and narrow the --dialect flag value. */\nfunction parseDialect(\n value: string | undefined,\n): \"postgres\" | \"mysql\" | \"sqlite\" | undefined {\n if (\n value === \"postgres\" ||\n value === \"mysql\" ||\n value === \"sqlite\" ||\n value === undefined\n ) {\n return value;\n }\n throw new Error(\"--dialect must be 'postgres', 'mysql', or 'sqlite'\");\n}\n\n/** Extract a human-readable message from an error, including AggregateError nested errors. */\nfunction formatErrorMessage(err: unknown): string {\n if (err instanceof AggregateError && err.errors.length > 0) {\n // AggregateError (e.g. from pg connection failure) has an empty .message\n // but contains nested errors with the actual details\n const nested = err.errors\n .map((e: unknown) => (e instanceof Error ? e.message : String(e)))\n .join(\"; \");\n return err.message !== \"\" ? `${err.message}: ${nested}` : nested;\n }\n if (err instanceof Error) {\n return err.message;\n }\n return String(err);\n}\n\n/** Wrap a subcommand action with consistent error handling. */\nasync function runAction(fn: () => Promise<void>): Promise<void> {\n try {\n await fn();\n } catch (err) {\n // CheckFailedError already printed its output — just exit\n if (err instanceof CheckFailedError) {\n process.exit(1);\n }\n const message = formatErrorMessage(err);\n console.error(pc.red(message));\n process.exit(1);\n }\n}\n\nconst program = new Command()\n .name(\"@usebetterdev/audit-cli\")\n .description(\"CLI for @usebetterdev/audit — compliance-ready audit logging\")\n .version(cliVersion);\n\nprogram\n .command(\"migrate\")\n .description(\"Generate the audit_logs table migration\")\n .option(\n \"--dry-run\",\n \"Print SQL to stdout without writing a file\",\n )\n .option(\n \"--adapter <adapter>\",\n \"ORM adapter: drizzle or prisma (auto-detected from package.json)\",\n )\n .option(\n \"--dialect <dialect>\",\n \"Database dialect: postgres, mysql, or sqlite (auto-detected from DATABASE_URL)\",\n )\n .option(\"-o, --output <path>\", \"Output directory or .sql file path\")\n .action(\n async (opts: {\n dryRun?: boolean;\n adapter?: string;\n dialect?: string;\n output?: string;\n }) => {\n await runAction(() =>\n migrate({\n dryRun: opts.dryRun,\n adapter: parseAdapter(opts.adapter),\n dialect: parseDialect(opts.dialect),\n output: opts.output,\n }),\n );\n },\n );\n\nprogram\n .command(\"check\")\n .description(\"Verify the audit_logs table and ORM adapter are working\")\n .option(\"--verbose\", \"Show detailed results for each check\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(async (opts: { verbose?: boolean; databaseUrl?: string }) => {\n await runAction(() =>\n check({ verbose: opts.verbose, databaseUrl: opts.databaseUrl }),\n );\n });\n\nprogram\n .command(\"stats\")\n .description(\"Show audit log statistics\")\n .option(\"--since <period>\", \"Time window (e.g. 30d)\", \"30d\")\n .action(async (opts: { since: string }) => {\n await runAction(() => stats({ since: opts.since }));\n });\n\nprogram\n .command(\"purge\")\n .description(\"Delete audit logs older than the retention period\")\n .option(\"--dry-run\", \"Preview rows to be deleted without deleting them\")\n .option(\n \"--since <value>\",\n 'ISO date (e.g. \"2025-01-01\") or duration shorthand (e.g. \"90d\") — overrides config',\n )\n .option(\"--batch-size <n>\", \"Rows per DELETE batch (default: 1000)\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .option(\"-y, --yes\", \"Skip confirmation prompt (required for live deletion)\")\n .action(\n async (opts: {\n dryRun?: boolean;\n since?: string;\n batchSize?: string;\n databaseUrl?: string;\n yes?: boolean;\n }) => {\n await runAction(async () => {\n const purgeOpts: Parameters<typeof purge>[0] = {};\n if (opts.dryRun !== undefined) {\n purgeOpts.dryRun = opts.dryRun;\n }\n if (opts.since !== undefined) {\n purgeOpts.since = opts.since;\n }\n if (opts.batchSize !== undefined) {\n const n = Number(opts.batchSize);\n if (!Number.isInteger(n) || n <= 0) {\n throw new Error(\n `Invalid --batch-size \"${opts.batchSize}\". Expected a positive integer.`,\n );\n }\n purgeOpts.batchSize = n;\n }\n if (opts.databaseUrl !== undefined) {\n purgeOpts.databaseUrl = opts.databaseUrl;\n }\n if (opts.yes !== undefined) {\n purgeOpts.yes = opts.yes;\n }\n await purge(purgeOpts);\n });\n },\n );\n\nprogram\n .command(\"export\")\n .description(\"Export audit log entries as CSV or JSON\")\n .option(\"--format <format>\", \"Output format: csv or json (default: csv)\")\n .option(\"-o, --output <path>\", \"Output file path (default: stdout)\")\n .option(\n \"--since <value>\",\n 'Duration (e.g. \"90d\") or ISO date (e.g. \"2025-01-01\")',\n )\n .option(\n \"--severity <level>\",\n \"Filter by severity: low, medium, high, or critical\",\n )\n .option(\n \"--compliance <tags>\",\n 'Comma-separated compliance tags (e.g. \"soc2:access-control,gdpr\")',\n )\n .option(\"--actor <id>\", \"Filter by actor ID\")\n .option(\"--limit <n>\", \"Maximum number of rows to export\")\n .option(\n \"--database-url <url>\",\n \"Database URL (default: DATABASE_URL env)\",\n )\n .action(\n async (opts: {\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 await runAction(() => exportLogs(opts));\n },\n );\n\nprogram.parseAsync(process.argv).catch((err: unknown) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-detection utilities for ORM adapter, database dialect, and migration directories.
|
|
3
3
|
*/
|
|
4
|
+
|
|
4
5
|
type DatabaseDialect = "postgres" | "mysql" | "sqlite";
|
|
5
|
-
type AdapterType = "drizzle" | "prisma";
|
|
6
6
|
|
|
7
|
-
export type {
|
|
7
|
+
export type { DatabaseDialect as D };
|
package/dist/export.js
CHANGED
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
parseFormat,
|
|
6
6
|
parseSeverity,
|
|
7
7
|
parseSinceValue
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TLZWAZAJ.js";
|
|
9
9
|
import "./chunk-7GSN73TA.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-IJDJMOCO.js";
|
|
11
11
|
export {
|
|
12
12
|
createFileWritableStream,
|
|
13
13
|
createStdoutWritableStream,
|
package/dist/migrate.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as DatabaseDialect } from './detect-adapter-WIB0ARFR.js';
|
|
2
|
+
import { OrmAdapter } from '@usebetterdev/cli-utils';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* `better-audit migrate` — Generate the audit_logs table migration.
|
|
@@ -16,7 +17,7 @@ interface MigrateOptions {
|
|
|
16
17
|
/** Output SQL to stdout instead of writing a file */
|
|
17
18
|
dryRun?: boolean | undefined;
|
|
18
19
|
/** Override ORM auto-detection */
|
|
19
|
-
adapter?:
|
|
20
|
+
adapter?: OrmAdapter | undefined;
|
|
20
21
|
/** Override database dialect detection */
|
|
21
22
|
dialect?: DatabaseDialect | undefined;
|
|
22
23
|
/** Override output directory/file */
|
package/dist/migrate.js
CHANGED
package/dist/purge.js
CHANGED
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
parseSinceValue,
|
|
4
4
|
purge,
|
|
5
5
|
resolveCutoffDate
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-HV3X7C45.js";
|
|
7
7
|
import "./chunk-7GSN73TA.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-IJDJMOCO.js";
|
|
9
9
|
export {
|
|
10
10
|
formatDuration,
|
|
11
11
|
parseSinceValue,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebetterdev/audit-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"repository": "github:usebetter-dev/usebetter",
|
|
5
5
|
"bugs": "https://github.com/usebetter-dev/usebetter/issues",
|
|
6
6
|
"homepage": "https://github.com/usebetter-dev/usebetter#readme",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"commander": "^12.1.0",
|
|
23
23
|
"kysely": "^0.28.11",
|
|
24
24
|
"picocolors": "^1.1.0",
|
|
25
|
-
"@usebetterdev/audit-core": "0.
|
|
26
|
-
"@usebetterdev/plugin": "0.7.
|
|
25
|
+
"@usebetterdev/audit-core": "0.7.0",
|
|
26
|
+
"@usebetterdev/plugin": "0.7.1"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"
|
|
29
|
+
"better-sqlite3": ">=11.0.0",
|
|
30
30
|
"mysql2": ">=3.0.0",
|
|
31
|
-
"
|
|
31
|
+
"pg": ">=8.0.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependenciesMeta": {
|
|
34
34
|
"pg": {
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"@types/node": "^22.10.0",
|
|
46
46
|
"tsup": "^8.3.5",
|
|
47
47
|
"typescript": "~5.7.2",
|
|
48
|
-
"vitest": "^2.1.6"
|
|
48
|
+
"vitest": "^2.1.6",
|
|
49
|
+
"@usebetterdev/cli-utils": "0.1.0",
|
|
50
|
+
"@usebetterdev/test-utils": "^0.5.2"
|
|
49
51
|
},
|
|
50
52
|
"engines": {
|
|
51
53
|
"node": ">=22"
|
|
@@ -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 pc from \"picocolors\";\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 interface CheckOptions {\n databaseUrl?: string | undefined;\n verbose?: boolean | undefined;\n}\n\nexport interface CheckResult {\n check: string;\n passed: boolean;\n message?: string;\n remediation?: string;\n}\n\nexport interface CheckOutput {\n passed: boolean;\n results: CheckResult[];\n warnings: CheckResult[];\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 printCheckOutput(output, verbose);\n\n if (!output.passed) {\n throw new CheckFailedError(output);\n }\n}\n\n/** Print check results to stdout. Separated for testability. */\nfunction printCheckOutput(output: CheckOutput, verbose: boolean): void {\n const passedCount = output.results.filter((r) => r.passed).length;\n const failedCount = output.results.length - passedCount;\n const failures = output.results.filter((r) => !r.passed);\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// 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":[]}
|
package/dist/chunk-HDO5P6X7.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// src/utils.ts
|
|
2
|
-
function formatTimestamp(date) {
|
|
3
|
-
const y = date.getFullYear();
|
|
4
|
-
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
5
|
-
const d = String(date.getDate()).padStart(2, "0");
|
|
6
|
-
const h = String(date.getHours()).padStart(2, "0");
|
|
7
|
-
const min = String(date.getMinutes()).padStart(2, "0");
|
|
8
|
-
const sec = String(date.getSeconds()).padStart(2, "0");
|
|
9
|
-
return `${y}${m}${d}${h}${min}${sec}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// src/detect-adapter.ts
|
|
13
|
-
import { existsSync, readFileSync } from "fs";
|
|
14
|
-
import { join } from "path";
|
|
15
|
-
function detectAdapter(cwd) {
|
|
16
|
-
const pkgPath = join(cwd, "package.json");
|
|
17
|
-
if (!existsSync(pkgPath)) {
|
|
18
|
-
return void 0;
|
|
19
|
-
}
|
|
20
|
-
try {
|
|
21
|
-
const content = readFileSync(pkgPath, "utf-8");
|
|
22
|
-
const parsed = JSON.parse(content);
|
|
23
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
24
|
-
return void 0;
|
|
25
|
-
}
|
|
26
|
-
const allDeps = {};
|
|
27
|
-
if ("dependencies" in parsed && typeof parsed.dependencies === "object" && parsed.dependencies !== null) {
|
|
28
|
-
Object.assign(allDeps, parsed.dependencies);
|
|
29
|
-
}
|
|
30
|
-
if ("devDependencies" in parsed && typeof parsed.devDependencies === "object" && parsed.devDependencies !== null) {
|
|
31
|
-
Object.assign(allDeps, parsed.devDependencies);
|
|
32
|
-
}
|
|
33
|
-
if ("drizzle-orm" in allDeps) {
|
|
34
|
-
return "drizzle";
|
|
35
|
-
}
|
|
36
|
-
if ("@prisma/client" in allDeps) {
|
|
37
|
-
return "prisma";
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
41
|
-
return void 0;
|
|
42
|
-
}
|
|
43
|
-
function detectDialect(databaseUrl) {
|
|
44
|
-
if (databaseUrl === void 0) {
|
|
45
|
-
return "postgres";
|
|
46
|
-
}
|
|
47
|
-
const lower = databaseUrl.toLowerCase();
|
|
48
|
-
if (lower.startsWith("postgres://") || lower.startsWith("postgresql://")) {
|
|
49
|
-
return "postgres";
|
|
50
|
-
}
|
|
51
|
-
if (lower.startsWith("mysql://")) {
|
|
52
|
-
return "mysql";
|
|
53
|
-
}
|
|
54
|
-
if (lower.startsWith("file:") || lower.endsWith(".db") || lower.endsWith(".sqlite") || lower.endsWith(".sqlite3")) {
|
|
55
|
-
return "sqlite";
|
|
56
|
-
}
|
|
57
|
-
return "postgres";
|
|
58
|
-
}
|
|
59
|
-
function findMigrationDirectory(cwd, adapter) {
|
|
60
|
-
if (adapter === "drizzle") {
|
|
61
|
-
const supabasePath = join(cwd, "supabase", "migrations");
|
|
62
|
-
if (existsSync(supabasePath)) {
|
|
63
|
-
return supabasePath;
|
|
64
|
-
}
|
|
65
|
-
return join(cwd, "drizzle");
|
|
66
|
-
}
|
|
67
|
-
const timestamp = formatTimestamp(/* @__PURE__ */ new Date());
|
|
68
|
-
return join(cwd, "prisma", "migrations", `${timestamp}_add_audit_logs`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export {
|
|
72
|
-
formatTimestamp,
|
|
73
|
-
detectAdapter,
|
|
74
|
-
detectDialect,
|
|
75
|
-
findMigrationDirectory
|
|
76
|
-
};
|
|
77
|
-
//# sourceMappingURL=chunk-HDO5P6X7.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/utils.ts","../src/detect-adapter.ts"],"sourcesContent":["/** Format a date as YYYYMMDDHHMMSS for migration file/directory names. */\nexport function formatTimestamp(date: Date): 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}${h}${min}${sec}`;\n}\n","/**\n * Auto-detection utilities for ORM adapter, database dialect, and migration directories.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { formatTimestamp } from \"./utils.js\";\n\nexport type DatabaseDialect = \"postgres\" | \"mysql\" | \"sqlite\";\n\nexport type AdapterType = \"drizzle\" | \"prisma\";\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 detectAdapter(cwd: string): AdapterType | 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\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: AdapterType,\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\n"],"mappings":";AACO,SAAS,gBAAgB,MAAoB;AAClD,QAAM,IAAI,KAAK,YAAY;AAC3B,QAAM,IAAI,OAAO,KAAK,SAAS,IAAI,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,QAAM,IAAI,OAAO,KAAK,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG;AAChD,QAAM,IAAI,OAAO,KAAK,SAAS,CAAC,EAAE,SAAS,GAAG,GAAG;AACjD,QAAM,MAAM,OAAO,KAAK,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,QAAM,MAAM,OAAO,KAAK,WAAW,CAAC,EAAE,SAAS,GAAG,GAAG;AACrD,SAAO,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG;AACrC;;;ACLA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AAYd,SAAS,cAAc,KAAsC;AAClE,QAAM,UAAU,KAAK,KAAK,cAAc;AACxC,MAAI,CAAC,WAAW,OAAO,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,SAAS,OAAO;AAC7C,UAAM,SAAkB,KAAK,MAAM,OAAO;AAC1C,QAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AACjD,aAAO;AAAA,IACT;AAEA,UAAM,UAAmC,CAAC;AAC1C,QAAI,kBAAkB,UAAU,OAAO,OAAO,iBAAiB,YAAY,OAAO,iBAAiB,MAAM;AACvG,aAAO,OAAO,SAAS,OAAO,YAAY;AAAA,IAC5C;AACA,QAAI,qBAAqB,UAAU,OAAO,OAAO,oBAAoB,YAAY,OAAO,oBAAoB,MAAM;AAChH,aAAO,OAAO,SAAS,OAAO,eAAe;AAAA,IAC/C;AAEA,QAAI,iBAAiB,SAAS;AAC5B,aAAO;AAAA,IACT;AACA,QAAI,oBAAoB,SAAS;AAC/B,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO;AACT;AAMO,SAAS,cAAc,aAAuC;AACnE,MAAI,gBAAgB,QAAW;AAC7B,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY,YAAY;AAEtC,MAAI,MAAM,WAAW,aAAa,KAAK,MAAM,WAAW,eAAe,GAAG;AACxE,WAAO;AAAA,EACT;AACA,MAAI,MAAM,WAAW,UAAU,GAAG;AAChC,WAAO;AAAA,EACT;AACA,MAAI,MAAM,WAAW,OAAO,KAAK,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,SAAS,KAAK,MAAM,SAAS,UAAU,GAAG;AACjH,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAQO,SAAS,uBACd,KACA,SACQ;AACR,MAAI,YAAY,WAAW;AACzB,UAAM,eAAe,KAAK,KAAK,YAAY,YAAY;AACvD,QAAI,WAAW,YAAY,GAAG;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,KAAK,KAAK,SAAS;AAAA,EAC5B;AAGA,QAAM,YAAY,gBAAgB,oBAAI,KAAK,CAAC;AAC5C,SAAO,KAAK,KAAK,UAAU,cAAc,GAAG,SAAS,iBAAiB;AACxE;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/migrate.ts"],"sourcesContent":["/**\n * `better-audit migrate` — Generate the audit_logs table migration.\n *\n * Generates appropriate DDL for the configured database:\n * - Postgres: JSONB columns, TIMESTAMPTZ, gen_random_uuid()\n * - MySQL: JSON columns, DATETIME(6), UUID() default\n * - SQLite: TEXT columns for JSON, no UUID default\n *\n * Writes the migration file into the appropriate ORM directory,\n * or prints to stdout with --dry-run.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport pc from \"picocolors\";\nimport { generateMigrationSql, type DatabaseDialect } from \"./generate-sql.js\";\nimport {\n detectAdapter,\n detectDialect,\n findMigrationDirectory,\n type AdapterType,\n} from \"./detect-adapter.js\";\nimport { formatTimestamp } from \"./utils.js\";\n\nexport interface MigrateOptions {\n /** Output SQL to stdout instead of writing a file */\n dryRun?: boolean | undefined;\n /** Override ORM auto-detection */\n adapter?: AdapterType | undefined;\n /** Override database dialect detection */\n dialect?: DatabaseDialect | undefined;\n /** Override output directory/file */\n output?: string | undefined;\n /** Working directory (defaults to process.cwd()) */\n cwd?: string | undefined;\n /** Database connection URL for dialect detection (defaults to DATABASE_URL env) */\n databaseUrl?: string | undefined;\n}\n\n/** Regex to detect an existing audit_logs CREATE TABLE statement (case-insensitive). */\nconst AUDIT_TABLE_REGEX = /create\\s+table\\b[^;]*\\baudit_logs\\b/i;\n\n/** Maximum directory depth for the idempotency scan. */\nconst MAX_SCAN_DEPTH = 3;\n\n/**\n * Check if a migration for `audit_logs` already exists in the given directory.\n * Scans `.sql` files up to `MAX_SCAN_DEPTH` levels deep, skipping symlinks.\n */\nfunction migrationAlreadyExists(directory: string, depth: number = 0): boolean {\n if (depth > MAX_SCAN_DEPTH) {\n return false;\n }\n if (!existsSync(directory)) {\n return false;\n }\n\n try {\n const entries = readdirSync(directory, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) {\n continue;\n }\n const fullPath = join(directory, entry.name);\n if (entry.isFile() && entry.name.endsWith(\".sql\")) {\n const content = readFileSync(fullPath, \"utf-8\");\n if (AUDIT_TABLE_REGEX.test(content)) {\n return true;\n }\n }\n if (entry.isDirectory()) {\n if (migrationAlreadyExists(fullPath, depth + 1)) {\n return true;\n }\n }\n }\n } catch {\n // Read error — treat as no existing migration\n }\n\n return false;\n}\n\nexport async function migrate(options: MigrateOptions = {}): Promise<void> {\n const cwd = options.cwd ?? process.cwd();\n\n // 1. Resolve dialect\n const dialect: DatabaseDialect =\n options.dialect ?? detectDialect(options.databaseUrl ?? process.env[\"DATABASE_URL\"]);\n\n // 2. Generate SQL\n const sql = generateMigrationSql(dialect);\n\n // 3. Dry-run: print to stdout and return\n if (options.dryRun === true) {\n process.stdout.write(sql);\n return;\n }\n\n // 4. Resolve adapter\n const adapter: AdapterType | undefined =\n options.adapter ?? detectAdapter(cwd);\n\n if (adapter === undefined) {\n throw new Error(\n \"Could not detect ORM adapter. Install drizzle-orm or @prisma/client, \" +\n \"or pass --adapter drizzle|prisma.\",\n );\n }\n\n // 5. Resolve output directory\n let outputPath: string;\n let filePath: string;\n\n if (options.output !== undefined) {\n if (options.output.endsWith(\".sql\")) {\n filePath = options.output;\n outputPath = dirname(filePath);\n } else {\n outputPath = options.output;\n filePath =\n adapter === \"prisma\"\n ? join(outputPath, \"migration.sql\")\n : join(outputPath, `${formatTimestamp(new Date())}_audit_logs.sql`);\n }\n } else {\n const migrationDir = findMigrationDirectory(cwd, adapter);\n outputPath = migrationDir;\n filePath =\n adapter === \"prisma\"\n ? join(migrationDir, \"migration.sql\")\n : join(migrationDir, `${formatTimestamp(new Date())}_audit_logs.sql`);\n }\n\n // 6. Check idempotency — scan parent migration directory\n const scanDir =\n adapter === \"prisma\"\n ? join(cwd, \"prisma\", \"migrations\")\n : outputPath;\n\n if (migrationAlreadyExists(scanDir)) {\n console.log(\n pc.green(\"✓\") + \" audit_logs migration already exists — up to date.\",\n );\n return;\n }\n\n // 7. Warn if output file already exists (explicit --output path)\n if (options.output !== undefined && existsSync(filePath)) {\n console.warn(pc.yellow(`Warning: overwriting existing file ${filePath}`));\n }\n\n // 8. Write migration file\n mkdirSync(outputPath, { recursive: true });\n writeFileSync(filePath, sql, \"utf-8\");\n\n console.log(`${pc.green(\"✓\")} Wrote migration: ${pc.dim(filePath)}`);\n console.log(\"\");\n console.log(pc.bold(\"Next steps:\"));\n\n if (adapter === \"drizzle\") {\n console.log(` ${pc.yellow(\"$\")} npx drizzle-kit migrate`);\n } else {\n console.log(` ${pc.yellow(\"$\")} npx prisma migrate dev`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|