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