@usebetterdev/audit-drizzle 0.5.0-beta.1 → 0.6.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/index.cjs CHANGED
@@ -7,11 +7,11 @@ var __export = (target, all) => {
7
7
  for (var name in all)
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
- var __copyProps = (to, from, except, desc3) => {
10
+ var __copyProps = (to, from, except, desc5) => {
11
11
  if (from && typeof from === "object" || typeof from === "function") {
12
12
  for (let key of __getOwnPropNames(from))
13
13
  if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc3 = __getOwnPropDesc(from, key)) || desc3.enumerable });
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc5 = __getOwnPropDesc(from, key)) || desc5.enumerable });
15
15
  }
16
16
  return to;
17
17
  };
@@ -26,7 +26,9 @@ __export(index_exports, {
26
26
  buildWhereConditions: () => buildWhereConditions,
27
27
  decodeCursor: () => decodeCursor,
28
28
  drizzleAuditAdapter: () => drizzleAuditAdapter,
29
+ drizzleSqliteAuditAdapter: () => drizzleSqliteAuditAdapter,
29
30
  encodeCursor: () => encodeCursor,
31
+ sqliteAuditLogs: () => sqliteAuditLogs,
30
32
  withAuditProxy: () => withAuditProxy
31
33
  });
32
34
  module.exports = __toCommonJS(index_exports);
@@ -466,8 +468,439 @@ function assembleStats(summaryRows, eventsPerDayRows, topActorsRows, topTablesRo
466
468
  };
467
469
  }
468
470
 
469
- // src/proxy.ts
471
+ // src/sqlite-adapter.ts
472
+ var import_drizzle_orm4 = require("drizzle-orm");
473
+
474
+ // src/sqlite-schema.ts
475
+ var import_sqlite_core = require("drizzle-orm/sqlite-core");
476
+ var sqliteAuditLogs = (0, import_sqlite_core.sqliteTable)(
477
+ "audit_logs",
478
+ {
479
+ id: (0, import_sqlite_core.text)().primaryKey(),
480
+ timestamp: (0, import_sqlite_core.integer)({ mode: "timestamp" }).notNull(),
481
+ tableName: (0, import_sqlite_core.text)("table_name").notNull(),
482
+ operation: (0, import_sqlite_core.text)().notNull(),
483
+ recordId: (0, import_sqlite_core.text)("record_id").notNull(),
484
+ actorId: (0, import_sqlite_core.text)("actor_id"),
485
+ beforeData: (0, import_sqlite_core.text)("before_data", { mode: "json" }),
486
+ afterData: (0, import_sqlite_core.text)("after_data", { mode: "json" }),
487
+ diff: (0, import_sqlite_core.text)({ mode: "json" }),
488
+ label: (0, import_sqlite_core.text)(),
489
+ description: (0, import_sqlite_core.text)(),
490
+ severity: (0, import_sqlite_core.text)(),
491
+ compliance: (0, import_sqlite_core.text)({ mode: "json" }),
492
+ notify: (0, import_sqlite_core.integer)({ mode: "boolean" }),
493
+ reason: (0, import_sqlite_core.text)(),
494
+ metadata: (0, import_sqlite_core.text)({ mode: "json" }),
495
+ redactedFields: (0, import_sqlite_core.text)("redacted_fields", { mode: "json" })
496
+ },
497
+ (table) => ({
498
+ tableNameTimestampIdx: (0, import_sqlite_core.index)("audit_logs_table_name_timestamp_idx").on(
499
+ table.tableName,
500
+ table.timestamp
501
+ ),
502
+ actorIdIdx: (0, import_sqlite_core.index)("audit_logs_actor_id_idx").on(table.actorId),
503
+ recordIdIdx: (0, import_sqlite_core.index)("audit_logs_record_id_idx").on(table.recordId),
504
+ tableNameRecordIdIdx: (0, import_sqlite_core.index)("audit_logs_table_name_record_id_idx").on(
505
+ table.tableName,
506
+ table.recordId
507
+ ),
508
+ operationIdx: (0, import_sqlite_core.index)("audit_logs_operation_idx").on(table.operation),
509
+ timestampIdx: (0, import_sqlite_core.index)("audit_logs_timestamp_idx").on(table.timestamp),
510
+ timestampIdIdx: (0, import_sqlite_core.index)("audit_logs_timestamp_id_idx").on(table.timestamp, table.id)
511
+ })
512
+ );
513
+
514
+ // src/sqlite-column-map.ts
515
+ var VALID_OPERATIONS2 = /* @__PURE__ */ new Set([
516
+ "INSERT",
517
+ "UPDATE",
518
+ "DELETE"
519
+ ]);
520
+ var VALID_SEVERITIES2 = /* @__PURE__ */ new Set([
521
+ "low",
522
+ "medium",
523
+ "high",
524
+ "critical"
525
+ ]);
526
+ function isAuditOperation2(value) {
527
+ return VALID_OPERATIONS2.has(value);
528
+ }
529
+ function isAuditSeverity2(value) {
530
+ return VALID_SEVERITIES2.has(value);
531
+ }
532
+ function sqliteAuditLogToRow(log) {
533
+ const row = {
534
+ id: log.id,
535
+ timestamp: log.timestamp,
536
+ tableName: log.tableName,
537
+ operation: log.operation,
538
+ recordId: log.recordId
539
+ };
540
+ if (log.actorId !== void 0) {
541
+ row.actorId = log.actorId;
542
+ }
543
+ if (log.beforeData !== void 0) {
544
+ row.beforeData = log.beforeData;
545
+ }
546
+ if (log.afterData !== void 0) {
547
+ row.afterData = log.afterData;
548
+ }
549
+ if (log.diff !== void 0) {
550
+ row.diff = log.diff;
551
+ }
552
+ if (log.label !== void 0) {
553
+ row.label = log.label;
554
+ }
555
+ if (log.description !== void 0) {
556
+ row.description = log.description;
557
+ }
558
+ if (log.severity !== void 0) {
559
+ row.severity = log.severity;
560
+ }
561
+ if (log.compliance !== void 0) {
562
+ row.compliance = log.compliance;
563
+ }
564
+ if (log.notify !== void 0) {
565
+ row.notify = log.notify;
566
+ }
567
+ if (log.reason !== void 0) {
568
+ row.reason = log.reason;
569
+ }
570
+ if (log.metadata !== void 0) {
571
+ row.metadata = log.metadata;
572
+ }
573
+ if (log.redactedFields !== void 0) {
574
+ row.redactedFields = log.redactedFields;
575
+ }
576
+ return row;
577
+ }
578
+ function sqliteRowToAuditLog(row) {
579
+ if (!isAuditOperation2(row.operation)) {
580
+ throw new Error(
581
+ `Invalid audit operation: "${row.operation}". Expected one of: INSERT, UPDATE, DELETE`
582
+ );
583
+ }
584
+ const log = {
585
+ id: row.id,
586
+ timestamp: row.timestamp,
587
+ tableName: row.tableName,
588
+ operation: row.operation,
589
+ recordId: row.recordId
590
+ };
591
+ if (row.actorId !== null) {
592
+ log.actorId = row.actorId;
593
+ }
594
+ if (row.beforeData !== null) {
595
+ log.beforeData = row.beforeData;
596
+ }
597
+ if (row.afterData !== null) {
598
+ log.afterData = row.afterData;
599
+ }
600
+ if (row.diff !== null) {
601
+ log.diff = row.diff;
602
+ }
603
+ if (row.label !== null) {
604
+ log.label = row.label;
605
+ }
606
+ if (row.description !== null) {
607
+ log.description = row.description;
608
+ }
609
+ if (row.severity !== null && row.severity !== void 0) {
610
+ if (!isAuditSeverity2(row.severity)) {
611
+ throw new Error(
612
+ `Invalid audit severity: "${row.severity}". Expected one of: low, medium, high, critical`
613
+ );
614
+ }
615
+ log.severity = row.severity;
616
+ }
617
+ if (row.compliance !== null) {
618
+ log.compliance = row.compliance;
619
+ }
620
+ if (row.notify !== null) {
621
+ log.notify = row.notify;
622
+ }
623
+ if (row.reason !== null) {
624
+ log.reason = row.reason;
625
+ }
626
+ if (row.metadata !== null) {
627
+ log.metadata = row.metadata;
628
+ }
629
+ if (row.redactedFields !== null) {
630
+ log.redactedFields = row.redactedFields;
631
+ }
632
+ return log;
633
+ }
634
+
635
+ // src/sqlite-query.ts
636
+ var import_audit_core2 = require("@usebetterdev/audit-core");
470
637
  var import_drizzle_orm3 = require("drizzle-orm");
638
+ function escapeLikePattern2(input) {
639
+ return input.replace(/[%_\\]/g, "\\$&");
640
+ }
641
+ function resolveTimeFilter2(filter) {
642
+ if ("date" in filter && filter.date !== void 0) {
643
+ return filter.date;
644
+ }
645
+ if ("duration" in filter && filter.duration !== void 0) {
646
+ return (0, import_audit_core2.parseDuration)(filter.duration);
647
+ }
648
+ throw new Error("TimeFilter must have either date or duration");
649
+ }
650
+ function buildSqliteWhereConditions(filters) {
651
+ const conditions = [];
652
+ if (filters.resource !== void 0) {
653
+ conditions.push((0, import_drizzle_orm3.eq)(sqliteAuditLogs.tableName, filters.resource.tableName));
654
+ if (filters.resource.recordId !== void 0) {
655
+ conditions.push((0, import_drizzle_orm3.eq)(sqliteAuditLogs.recordId, filters.resource.recordId));
656
+ }
657
+ }
658
+ if (filters.actorIds !== void 0 && filters.actorIds.length > 0) {
659
+ if (filters.actorIds.length === 1) {
660
+ conditions.push((0, import_drizzle_orm3.eq)(sqliteAuditLogs.actorId, filters.actorIds[0]));
661
+ } else {
662
+ conditions.push((0, import_drizzle_orm3.inArray)(sqliteAuditLogs.actorId, filters.actorIds));
663
+ }
664
+ }
665
+ if (filters.severities !== void 0 && filters.severities.length > 0) {
666
+ if (filters.severities.length === 1) {
667
+ conditions.push((0, import_drizzle_orm3.eq)(sqliteAuditLogs.severity, filters.severities[0]));
668
+ } else {
669
+ conditions.push((0, import_drizzle_orm3.inArray)(sqliteAuditLogs.severity, filters.severities));
670
+ }
671
+ }
672
+ if (filters.operations !== void 0 && filters.operations.length > 0) {
673
+ if (filters.operations.length === 1) {
674
+ conditions.push((0, import_drizzle_orm3.eq)(sqliteAuditLogs.operation, filters.operations[0]));
675
+ } else {
676
+ conditions.push((0, import_drizzle_orm3.inArray)(sqliteAuditLogs.operation, filters.operations));
677
+ }
678
+ }
679
+ if (filters.since !== void 0) {
680
+ conditions.push((0, import_drizzle_orm3.gte)(sqliteAuditLogs.timestamp, resolveTimeFilter2(filters.since)));
681
+ }
682
+ if (filters.until !== void 0) {
683
+ conditions.push((0, import_drizzle_orm3.lte)(sqliteAuditLogs.timestamp, resolveTimeFilter2(filters.until)));
684
+ }
685
+ if (filters.searchText !== void 0 && filters.searchText.length > 0) {
686
+ const escaped = escapeLikePattern2(filters.searchText);
687
+ const pattern = `%${escaped}%`;
688
+ const searchCondition = (0, import_drizzle_orm3.or)(
689
+ (0, import_drizzle_orm3.like)(sqliteAuditLogs.label, pattern),
690
+ (0, import_drizzle_orm3.like)(sqliteAuditLogs.description, pattern)
691
+ );
692
+ if (searchCondition !== void 0) {
693
+ conditions.push(searchCondition);
694
+ }
695
+ }
696
+ if (filters.compliance !== void 0 && filters.compliance.length > 0) {
697
+ for (const tag of filters.compliance) {
698
+ conditions.push(
699
+ import_drizzle_orm3.sql`EXISTS (SELECT 1 FROM json_each(${sqliteAuditLogs.compliance}) WHERE value = ${tag})`
700
+ );
701
+ }
702
+ }
703
+ if (conditions.length === 0) {
704
+ return void 0;
705
+ }
706
+ return (0, import_drizzle_orm3.and)(...conditions);
707
+ }
708
+ function buildSqliteCursorCondition(cursor, sortOrder) {
709
+ const decoded = decodeSqliteCursor(cursor);
710
+ const tsCompare = sortOrder === "asc" ? import_drizzle_orm3.gt : import_drizzle_orm3.lt;
711
+ const idCompare = sortOrder === "asc" ? import_drizzle_orm3.gt : import_drizzle_orm3.lt;
712
+ return (0, import_drizzle_orm3.or)(
713
+ tsCompare(sqliteAuditLogs.timestamp, decoded.timestamp),
714
+ (0, import_drizzle_orm3.and)(
715
+ (0, import_drizzle_orm3.eq)(sqliteAuditLogs.timestamp, decoded.timestamp),
716
+ idCompare(sqliteAuditLogs.id, decoded.id)
717
+ )
718
+ );
719
+ }
720
+ function buildSqliteOrderBy(sortOrder) {
721
+ if (sortOrder === "asc") {
722
+ return [(0, import_drizzle_orm3.asc)(sqliteAuditLogs.timestamp), (0, import_drizzle_orm3.asc)(sqliteAuditLogs.id)];
723
+ }
724
+ return [(0, import_drizzle_orm3.desc)(sqliteAuditLogs.timestamp), (0, import_drizzle_orm3.desc)(sqliteAuditLogs.id)];
725
+ }
726
+ function encodeSqliteCursor(timestamp2, id) {
727
+ const payload = JSON.stringify({ t: timestamp2.toISOString(), i: id });
728
+ return btoa(payload);
729
+ }
730
+ var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
731
+ function decodeSqliteCursor(cursor) {
732
+ let parsed;
733
+ try {
734
+ parsed = JSON.parse(atob(cursor));
735
+ } catch {
736
+ throw new Error("Invalid cursor: failed to decode");
737
+ }
738
+ if (typeof parsed !== "object" || parsed === null || !("t" in parsed) || !("i" in parsed)) {
739
+ throw new Error("Invalid cursor: missing required fields");
740
+ }
741
+ const { t, i } = parsed;
742
+ if (typeof t !== "string" || typeof i !== "string") {
743
+ throw new Error("Invalid cursor: fields must be strings");
744
+ }
745
+ const timestamp2 = new Date(t);
746
+ if (isNaN(timestamp2.getTime())) {
747
+ throw new Error("Invalid cursor: invalid timestamp");
748
+ }
749
+ if (!UUID_PATTERN2.test(i)) {
750
+ throw new Error("Invalid cursor: id must be a valid UUID");
751
+ }
752
+ return { timestamp: timestamp2, id: i };
753
+ }
754
+
755
+ // src/sqlite-adapter.ts
756
+ var DEFAULT_LIMIT2 = 50;
757
+ var MAX_LIMIT2 = 250;
758
+ function toCount2(value) {
759
+ if (typeof value === "number") {
760
+ return value;
761
+ }
762
+ if (typeof value === "string") {
763
+ return Number(value);
764
+ }
765
+ return 0;
766
+ }
767
+ function drizzleSqliteAuditAdapter(db) {
768
+ return {
769
+ async writeLog(log) {
770
+ const row = sqliteAuditLogToRow(log);
771
+ await db.insert(sqliteAuditLogs).values(row).execute();
772
+ },
773
+ async queryLogs(spec) {
774
+ const sortOrder = spec.sortOrder ?? "desc";
775
+ const limit = Math.min(spec.limit ?? DEFAULT_LIMIT2, MAX_LIMIT2);
776
+ const whereCondition = buildSqliteWhereConditions(spec.filters);
777
+ const cursorCondition = spec.cursor !== void 0 ? buildSqliteCursorCondition(spec.cursor, sortOrder) : void 0;
778
+ const combined = (0, import_drizzle_orm4.and)(whereCondition, cursorCondition);
779
+ const fetchLimit = limit + 1;
780
+ const orderColumns = buildSqliteOrderBy(sortOrder);
781
+ const query = db.select().from(sqliteAuditLogs).where(combined).orderBy(...orderColumns).limit(fetchLimit);
782
+ const rows = await query;
783
+ const hasNextPage = rows.length > limit;
784
+ const resultRows = hasNextPage ? rows.slice(0, -1) : rows;
785
+ const entries = resultRows.map(sqliteRowToAuditLog);
786
+ const lastRow = resultRows[resultRows.length - 1];
787
+ if (hasNextPage && lastRow !== void 0) {
788
+ return { entries, nextCursor: encodeSqliteCursor(lastRow.timestamp, lastRow.id) };
789
+ }
790
+ return { entries };
791
+ },
792
+ async getLogById(id) {
793
+ const query = db.select().from(sqliteAuditLogs).where((0, import_drizzle_orm4.eq)(sqliteAuditLogs.id, id)).limit(1);
794
+ const rows = await query;
795
+ const row = rows[0];
796
+ if (row === void 0) {
797
+ return null;
798
+ }
799
+ return sqliteRowToAuditLog(row);
800
+ },
801
+ async purgeLogs(options) {
802
+ if (options.tableName !== void 0 && options.tableName.trim().length === 0) {
803
+ throw new Error("purgeLogs: tableName must be a non-empty string when provided");
804
+ }
805
+ const conditions = [(0, import_drizzle_orm4.lt)(sqliteAuditLogs.timestamp, options.before)];
806
+ if (options.tableName !== void 0) {
807
+ conditions.push((0, import_drizzle_orm4.eq)(sqliteAuditLogs.tableName, options.tableName));
808
+ }
809
+ await db.delete(sqliteAuditLogs).where((0, import_drizzle_orm4.and)(...conditions));
810
+ const changesResult = await db.select({ count: import_drizzle_orm4.sql`changes()` }).from(sqliteAuditLogs);
811
+ const deletedCount = changesResult[0] !== void 0 ? toCount2(changesResult[0].count) : 0;
812
+ return { deletedCount };
813
+ },
814
+ async getStats(options) {
815
+ const sinceCondition = options?.since !== void 0 ? (0, import_drizzle_orm4.gte)(sqliteAuditLogs.timestamp, options.since) : void 0;
816
+ const summaryQuery = db.select({
817
+ totalLogs: import_drizzle_orm4.sql`count(*)`,
818
+ tablesAudited: import_drizzle_orm4.sql`count(DISTINCT ${sqliteAuditLogs.tableName})`
819
+ }).from(sqliteAuditLogs).where(sinceCondition);
820
+ const eventsPerDayQuery = db.select({
821
+ date: import_drizzle_orm4.sql`strftime('%Y-%m-%d', ${sqliteAuditLogs.timestamp}, 'unixepoch')`,
822
+ count: import_drizzle_orm4.sql`count(*)`
823
+ }).from(sqliteAuditLogs).where(sinceCondition).groupBy(import_drizzle_orm4.sql`strftime('%Y-%m-%d', ${sqliteAuditLogs.timestamp}, 'unixepoch')`).orderBy(import_drizzle_orm4.sql`strftime('%Y-%m-%d', ${sqliteAuditLogs.timestamp}, 'unixepoch')`).limit(365);
824
+ const topActorsQuery = db.select({
825
+ actorId: sqliteAuditLogs.actorId,
826
+ count: import_drizzle_orm4.sql`count(*)`
827
+ }).from(sqliteAuditLogs).where((0, import_drizzle_orm4.and)(sinceCondition, (0, import_drizzle_orm4.isNotNull)(sqliteAuditLogs.actorId))).groupBy(sqliteAuditLogs.actorId).orderBy((0, import_drizzle_orm4.desc)(import_drizzle_orm4.sql`count(*)`)).limit(10);
828
+ const topTablesQuery = db.select({
829
+ tableName: sqliteAuditLogs.tableName,
830
+ count: import_drizzle_orm4.sql`count(*)`
831
+ }).from(sqliteAuditLogs).where(sinceCondition).groupBy(sqliteAuditLogs.tableName).orderBy((0, import_drizzle_orm4.desc)(import_drizzle_orm4.sql`count(*)`)).limit(10);
832
+ const operationQuery = db.select({
833
+ operation: sqliteAuditLogs.operation,
834
+ count: import_drizzle_orm4.sql`count(*)`
835
+ }).from(sqliteAuditLogs).where(sinceCondition).groupBy(sqliteAuditLogs.operation);
836
+ const severityQuery = db.select({
837
+ severity: sqliteAuditLogs.severity,
838
+ count: import_drizzle_orm4.sql`count(*)`
839
+ }).from(sqliteAuditLogs).where((0, import_drizzle_orm4.and)(sinceCondition, (0, import_drizzle_orm4.isNotNull)(sqliteAuditLogs.severity))).groupBy(sqliteAuditLogs.severity);
840
+ const results = await Promise.all([
841
+ summaryQuery,
842
+ eventsPerDayQuery,
843
+ topActorsQuery,
844
+ topTablesQuery,
845
+ operationQuery,
846
+ severityQuery
847
+ ]);
848
+ const [
849
+ summaryRows,
850
+ eventsPerDayRows,
851
+ topActorsRows,
852
+ topTablesRows,
853
+ operationRows,
854
+ severityRows
855
+ ] = results;
856
+ return assembleStats2(
857
+ summaryRows,
858
+ eventsPerDayRows,
859
+ topActorsRows,
860
+ topTablesRows,
861
+ operationRows,
862
+ severityRows
863
+ );
864
+ }
865
+ };
866
+ }
867
+ function assembleStats2(summaryRows, eventsPerDayRows, topActorsRows, topTablesRows, operationRows, severityRows) {
868
+ const summary = summaryRows[0];
869
+ const totalLogs = summary !== void 0 ? toCount2(summary.totalLogs) : 0;
870
+ const tablesAudited = summary !== void 0 ? toCount2(summary.tablesAudited) : 0;
871
+ const eventsPerDay = eventsPerDayRows.map((row) => ({
872
+ date: String(row.date),
873
+ count: toCount2(row.count)
874
+ }));
875
+ const topActors = topActorsRows.map((row) => ({
876
+ actorId: String(row.actorId),
877
+ count: toCount2(row.count)
878
+ }));
879
+ const topTables = topTablesRows.map((row) => ({
880
+ tableName: String(row.tableName),
881
+ count: toCount2(row.count)
882
+ }));
883
+ const operationBreakdown = {};
884
+ for (const row of operationRows) {
885
+ operationBreakdown[String(row.operation)] = toCount2(row.count);
886
+ }
887
+ const severityBreakdown = {};
888
+ for (const row of severityRows) {
889
+ severityBreakdown[String(row.severity)] = toCount2(row.count);
890
+ }
891
+ return {
892
+ totalLogs,
893
+ tablesAudited,
894
+ eventsPerDay,
895
+ topActors,
896
+ topTables,
897
+ operationBreakdown,
898
+ severityBreakdown
899
+ };
900
+ }
901
+
902
+ // src/proxy.ts
903
+ var import_drizzle_orm5 = require("drizzle-orm");
471
904
 
472
905
  // src/operation-map.ts
473
906
  var OPERATION_MAP = {
@@ -501,7 +934,7 @@ function withAuditProxy(db, captureLog, options) {
501
934
  const method = prop;
502
935
  const originalMethod = Reflect.get(target, prop, receiver);
503
936
  return (table) => {
504
- const tableName = (0, import_drizzle_orm3.getTableName)(table);
937
+ const tableName = (0, import_drizzle_orm5.getTableName)(table);
505
938
  const detectedPk = getPrimaryKeyColumnName(table);
506
939
  const effectivePk = detectedPk ?? primaryKey;
507
940
  const originalBuilder = originalMethod.call(target, table);
@@ -965,7 +1398,9 @@ async function fireCaptureLog(result, ctx, state) {
965
1398
  buildWhereConditions,
966
1399
  decodeCursor,
967
1400
  drizzleAuditAdapter,
1401
+ drizzleSqliteAuditAdapter,
968
1402
  encodeCursor,
1403
+ sqliteAuditLogs,
969
1404
  withAuditProxy
970
1405
  });
971
1406
  //# sourceMappingURL=index.cjs.map