@opentrust/db 7.1.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.
Files changed (83) hide show
  1. package/dist/client.d.ts +3 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +51 -0
  4. package/dist/dialect.d.ts +3 -0
  5. package/dist/dialect.d.ts.map +1 -0
  6. package/dist/dialect.js +12 -0
  7. package/dist/generate.d.ts +2 -0
  8. package/dist/generate.d.ts.map +1 -0
  9. package/dist/generate.js +20 -0
  10. package/dist/helpers.d.ts +11 -0
  11. package/dist/helpers.d.ts.map +1 -0
  12. package/dist/helpers.js +32 -0
  13. package/dist/index.d.ts +13 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +12 -0
  16. package/dist/migrate.d.ts +2 -0
  17. package/dist/migrate.d.ts.map +1 -0
  18. package/dist/migrate.js +61 -0
  19. package/dist/queries/agents.d.ts +25 -0
  20. package/dist/queries/agents.d.ts.map +1 -0
  21. package/dist/queries/agents.js +46 -0
  22. package/dist/queries/auth.d.ts +18 -0
  23. package/dist/queries/auth.d.ts.map +1 -0
  24. package/dist/queries/auth.js +77 -0
  25. package/dist/queries/detection-results.d.ts +24 -0
  26. package/dist/queries/detection-results.d.ts.map +1 -0
  27. package/dist/queries/detection-results.js +43 -0
  28. package/dist/queries/observations.d.ts +58 -0
  29. package/dist/queries/observations.d.ts.map +1 -0
  30. package/dist/queries/observations.js +212 -0
  31. package/dist/queries/policies.d.ts +25 -0
  32. package/dist/queries/policies.d.ts.map +1 -0
  33. package/dist/queries/policies.js +38 -0
  34. package/dist/queries/scanners.d.ts +25 -0
  35. package/dist/queries/scanners.d.ts.map +1 -0
  36. package/dist/queries/scanners.js +56 -0
  37. package/dist/queries/settings.d.ts +8 -0
  38. package/dist/queries/settings.d.ts.map +1 -0
  39. package/dist/queries/settings.js +30 -0
  40. package/dist/queries/usage.d.ts +18 -0
  41. package/dist/queries/usage.d.ts.map +1 -0
  42. package/dist/queries/usage.js +54 -0
  43. package/dist/schema/index.d.ts +4415 -0
  44. package/dist/schema/index.d.ts.map +1 -0
  45. package/dist/schema/index.js +19 -0
  46. package/dist/schema/mysql.d.ts +1479 -0
  47. package/dist/schema/mysql.d.ts.map +1 -0
  48. package/dist/schema/mysql.js +151 -0
  49. package/dist/schema/pg.d.ts +1479 -0
  50. package/dist/schema/pg.d.ts.map +1 -0
  51. package/dist/schema/pg.js +151 -0
  52. package/dist/schema/sqlite.d.ts +1479 -0
  53. package/dist/schema/sqlite.d.ts.map +1 -0
  54. package/dist/schema/sqlite.js +153 -0
  55. package/dist/seed.d.ts +2 -0
  56. package/dist/seed.d.ts.map +1 -0
  57. package/dist/seed.js +49 -0
  58. package/drizzle/sqlite/0000_serious_martin_li.sql +143 -0
  59. package/drizzle/sqlite/meta/0000_snapshot.json +945 -0
  60. package/drizzle/sqlite/meta/_journal.json +13 -0
  61. package/drizzle.config.mysql.ts +10 -0
  62. package/drizzle.config.pg.ts +10 -0
  63. package/drizzle.config.sqlite.ts +10 -0
  64. package/package.json +55 -0
  65. package/src/client.ts +66 -0
  66. package/src/dialect.ts +13 -0
  67. package/src/generate.ts +26 -0
  68. package/src/helpers.ts +47 -0
  69. package/src/index.ts +12 -0
  70. package/src/migrate.ts +74 -0
  71. package/src/queries/agents.ts +68 -0
  72. package/src/queries/auth.ts +94 -0
  73. package/src/queries/detection-results.ts +58 -0
  74. package/src/queries/observations.ts +275 -0
  75. package/src/queries/policies.ts +59 -0
  76. package/src/queries/scanners.ts +74 -0
  77. package/src/queries/settings.ts +34 -0
  78. package/src/queries/usage.ts +69 -0
  79. package/src/schema/index.ts +22 -0
  80. package/src/schema/mysql.ts +207 -0
  81. package/src/schema/pg.ts +208 -0
  82. package/src/schema/sqlite.ts +199 -0
  83. package/src/seed.ts +56 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "6",
8
+ "when": 1772680703827,
9
+ "tag": "0000_serious_martin_li",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/schema/mysql.ts",
5
+ out: "./drizzle/mysql",
6
+ dialect: "mysql",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/schema/pg.ts",
5
+ out: "./drizzle/postgresql",
6
+ dialect: "postgresql",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL!,
9
+ },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/schema/sqlite.ts",
5
+ out: "./drizzle/sqlite",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL || "file:./data/opentrust.db",
9
+ },
10
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@opentrust/db",
3
+ "version": "7.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "description": "Database layer for OpenTrust Dashboard — Drizzle ORM with SQLite/PostgreSQL/MySQL",
8
+ "bin": {
9
+ "opentrust-db-migrate": "./dist/migrate.js",
10
+ "opentrust-db-seed": "./dist/seed.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "drizzle",
15
+ "src",
16
+ "drizzle.config.*.ts"
17
+ ],
18
+ "dependencies": {
19
+ "better-sqlite3": "^11.0.0",
20
+ "dotenv": "^17.3.1",
21
+ "drizzle-orm": "^0.36.0",
22
+ "@opentrust/shared": "7.1.0"
23
+ },
24
+ "optionalDependencies": {
25
+ "mysql2": "^3.11.0",
26
+ "postgres": "^3.4.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/better-sqlite3": "^7.6.0",
30
+ "@types/node": "^22.0.0",
31
+ "drizzle-kit": "^0.30.0",
32
+ "tsx": "^4.19.0",
33
+ "typescript": "^5.7.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/opentrust/opentrust.git",
41
+ "directory": "dashboard/packages/db"
42
+ },
43
+ "author": "OpenTrust",
44
+ "license": "Apache-2.0",
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "dev": "tsc --watch",
48
+ "db:generate": "tsx src/generate.ts",
49
+ "db:generate:sqlite": "drizzle-kit generate --config=drizzle.config.sqlite.ts",
50
+ "db:generate:mysql": "drizzle-kit generate --config=drizzle.config.mysql.ts",
51
+ "db:generate:pg": "drizzle-kit generate --config=drizzle.config.pg.ts",
52
+ "db:migrate": "tsx src/migrate.ts",
53
+ "db:seed": "tsx src/seed.ts"
54
+ }
55
+ }
package/src/client.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { config } from "dotenv";
2
+ import { resolve, dirname, join } from "path";
3
+ import { mkdirSync, existsSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import { getDialect } from "./dialect.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ // Load .env from project root if DATABASE_URL is not already set
11
+ if (!process.env.DATABASE_URL && !process.env.DB_DIALECT) {
12
+ config({ path: resolve(__dirname, "../../../.env") });
13
+ }
14
+
15
+ const dialect = getDialect();
16
+
17
+ // Database path configuration:
18
+ // - DASHBOARD_DATA_DIR: directory for data files (default: dashboard/data)
19
+ // - DATABASE_URL: full path to SQLite file (overrides DASHBOARD_DATA_DIR for SQLite)
20
+ function getDefaultDbPath(): string {
21
+ const dataDir = process.env.DASHBOARD_DATA_DIR || resolve(__dirname, "../../../data");
22
+ return join(dataDir, "dashboard.db");
23
+ }
24
+
25
+ async function createDb() {
26
+ if (dialect === "sqlite") {
27
+ const { default: Database } = await import("better-sqlite3");
28
+ const { drizzle } = await import("drizzle-orm/better-sqlite3");
29
+ const schema = await import("./schema/sqlite.js");
30
+
31
+ const rawUrl = process.env.DATABASE_URL || getDefaultDbPath();
32
+ const dbPath = rawUrl.replace(/^file:/, "");
33
+
34
+ // Ensure directory exists
35
+ const dir = dirname(dbPath);
36
+ if (!existsSync(dir)) {
37
+ mkdirSync(dir, { recursive: true });
38
+ }
39
+
40
+ const sqlite = new Database(dbPath);
41
+ sqlite.pragma("journal_mode = WAL");
42
+ sqlite.pragma("foreign_keys = ON");
43
+
44
+ return drizzle(sqlite, { schema });
45
+ }
46
+
47
+ if (dialect === "mysql") {
48
+ const mysql2 = await import("mysql2/promise");
49
+ const { drizzle } = await import("drizzle-orm/mysql2");
50
+ const schema = await import("./schema/mysql.js");
51
+
52
+ const pool = mysql2.createPool(process.env.DATABASE_URL!);
53
+ return drizzle(pool, { schema, mode: "default" });
54
+ }
55
+
56
+ // postgresql (default)
57
+ const pg = await import("postgres");
58
+ const { drizzle } = await import("drizzle-orm/postgres-js");
59
+ const schema = await import("./schema/pg.js");
60
+
61
+ const queryClient = pg.default(process.env.DATABASE_URL!);
62
+ return drizzle(queryClient, { schema });
63
+ }
64
+
65
+ export const db: any = await createDb();
66
+ export type Database = any;
package/src/dialect.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type Dialect = "sqlite" | "mysql" | "postgresql";
2
+
3
+ export function getDialect(): Dialect {
4
+ const explicit = process.env.DB_DIALECT;
5
+ if (explicit === "sqlite" || explicit === "mysql" || explicit === "postgresql") {
6
+ return explicit;
7
+ }
8
+
9
+ const url = process.env.DATABASE_URL || "";
10
+ if (url.startsWith("mysql://") || url.startsWith("mysql2://")) return "mysql";
11
+ if (url.startsWith("postgres://") || url.startsWith("postgresql://")) return "postgresql";
12
+ return "sqlite";
13
+ }
@@ -0,0 +1,26 @@
1
+ import { config } from "dotenv";
2
+ import { resolve, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { execSync } from "child_process";
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ config({ path: resolve(__dirname, "../../../.env") });
10
+
11
+ const { getDialect } = await import("./dialect.js");
12
+ const dialect = getDialect();
13
+
14
+ const configMap: Record<string, string> = {
15
+ sqlite: "drizzle.config.sqlite.ts",
16
+ mysql: "drizzle.config.mysql.ts",
17
+ postgresql: "drizzle.config.pg.ts",
18
+ };
19
+
20
+ const configFile = configMap[dialect];
21
+ console.log(`Generating migrations for dialect: ${dialect}`);
22
+
23
+ execSync(`npx drizzle-kit generate --config=${configFile}`, {
24
+ cwd: resolve(__dirname, ".."),
25
+ stdio: "inherit",
26
+ });
package/src/helpers.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { getDialect } from "./dialect.js";
3
+
4
+ /**
5
+ * Cross-dialect insert with returning.
6
+ * PostgreSQL and SQLite support .returning(), MySQL does not.
7
+ * All dialects generate UUID client-side for predictability.
8
+ */
9
+ export async function insertReturning<T>(
10
+ db: any,
11
+ table: any,
12
+ values: Record<string, unknown>
13
+ ): Promise<T> {
14
+ const dialect = getDialect();
15
+ const id = crypto.randomUUID();
16
+ const row = { ...values, id };
17
+
18
+ if (dialect === "mysql") {
19
+ await db.insert(table).values(row);
20
+ const result = await db.select().from(table).where(eq(table.id, id)).limit(1);
21
+ return result[0] as T;
22
+ }
23
+
24
+ const result = await db.insert(table).values(row).returning();
25
+ return result[0] as T;
26
+ }
27
+
28
+ /**
29
+ * Cross-dialect update with returning.
30
+ */
31
+ export async function updateReturning<T>(
32
+ db: any,
33
+ table: any,
34
+ where: any,
35
+ values: Record<string, unknown>
36
+ ): Promise<T | null> {
37
+ const dialect = getDialect();
38
+
39
+ if (dialect === "mysql") {
40
+ await db.update(table).set(values).where(where);
41
+ const result = await db.select().from(table).where(where).limit(1);
42
+ return (result[0] as T) ?? null;
43
+ }
44
+
45
+ const result = await db.update(table).set(values).where(where).returning();
46
+ return (result[0] as T) ?? null;
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { db, type Database } from "./client.js";
2
+ export { getDialect, type Dialect } from "./dialect.js";
3
+ export * from "./schema/index.js";
4
+ export { insertReturning, updateReturning } from "./helpers.js";
5
+ export { agentQueries } from "./queries/agents.js";
6
+ export { scannerQueries } from "./queries/scanners.js";
7
+ export { policyQueries } from "./queries/policies.js";
8
+ export { usageQueries } from "./queries/usage.js";
9
+ export { detectionResultQueries } from "./queries/detection-results.js";
10
+ export { settingsQueries } from "./queries/settings.js";
11
+ export { observationQueries, inferCategory, inferAccessPattern } from "./queries/observations.js";
12
+ export { authQueries } from "./queries/auth.js";
package/src/migrate.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { config } from "dotenv";
2
+ import { resolve, dirname, join } from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ config({ path: resolve(__dirname, "../../../.env") });
9
+
10
+ // Database path configuration:
11
+ // - DASHBOARD_DATA_DIR: directory for data files (default: dashboard/data)
12
+ // - DATABASE_URL: full path to SQLite file (overrides DASHBOARD_DATA_DIR for SQLite)
13
+ function getDefaultDbPath(): string {
14
+ const dataDir = process.env.DASHBOARD_DATA_DIR || resolve(__dirname, "../../../data");
15
+ return join(dataDir, "dashboard.db");
16
+ }
17
+
18
+ async function runMigrations() {
19
+ const { getDialect } = await import("./dialect.js");
20
+ const dialect = getDialect();
21
+
22
+ console.log(`Running migrations for dialect: ${dialect}`);
23
+
24
+ if (dialect === "sqlite") {
25
+ const { default: Database } = await import("better-sqlite3");
26
+ const { drizzle } = await import("drizzle-orm/better-sqlite3");
27
+ const { migrate } = await import("drizzle-orm/better-sqlite3/migrator");
28
+ const { mkdirSync, existsSync } = await import("fs");
29
+ const { dirname } = await import("path");
30
+
31
+ const rawUrl = process.env.DATABASE_URL || getDefaultDbPath();
32
+ const dbPath = rawUrl.replace(/^file:/, "");
33
+
34
+ const dir = dirname(dbPath);
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+
37
+ const sqlite = new Database(dbPath);
38
+ sqlite.pragma("journal_mode = WAL");
39
+ const db = drizzle(sqlite);
40
+
41
+ migrate(db, { migrationsFolder: resolve(__dirname, "../drizzle/sqlite") });
42
+ console.log("SQLite migrations complete.");
43
+ sqlite.close();
44
+ } else if (dialect === "mysql") {
45
+ const mysql2 = await import("mysql2/promise");
46
+ const { drizzle } = await import("drizzle-orm/mysql2");
47
+ const { migrate } = await import("drizzle-orm/mysql2/migrator");
48
+
49
+ const pool = mysql2.createPool(process.env.DATABASE_URL!);
50
+ const db = drizzle(pool);
51
+
52
+ await migrate(db, { migrationsFolder: resolve(__dirname, "../drizzle/mysql") });
53
+ console.log("MySQL migrations complete.");
54
+ await pool.end();
55
+ } else {
56
+ const pg = await import("postgres");
57
+ const { drizzle } = await import("drizzle-orm/postgres-js");
58
+ const { migrate } = await import("drizzle-orm/postgres-js/migrator");
59
+
60
+ const queryClient = pg.default(process.env.DATABASE_URL!, { max: 1 });
61
+ const db = drizzle(queryClient);
62
+
63
+ await migrate(db, { migrationsFolder: resolve(__dirname, "../drizzle/postgresql") });
64
+ console.log("PostgreSQL migrations complete.");
65
+ await queryClient.end();
66
+ }
67
+
68
+ process.exit(0);
69
+ }
70
+
71
+ runMigrations().catch((err) => {
72
+ console.error("Migration failed:", err);
73
+ process.exit(1);
74
+ });
@@ -0,0 +1,68 @@
1
+ import { eq, and, count } from "drizzle-orm";
2
+ import type { Database } from "../client.js";
3
+ import { agents } from "../schema/index.js";
4
+ import { insertReturning, updateReturning } from "../helpers.js";
5
+ import { DEFAULT_TENANT_ID } from "@opentrust/shared";
6
+
7
+ export function agentQueries(db: Database) {
8
+ return {
9
+ async findById(id: string, tenantId: string = DEFAULT_TENANT_ID) {
10
+ const result = await db.select().from(agents).where(and(eq(agents.id, id), eq(agents.tenantId, tenantId))).limit(1);
11
+ return result[0] ?? null;
12
+ },
13
+
14
+ async findByName(name: string, tenantId: string = DEFAULT_TENANT_ID) {
15
+ const result = await db.select().from(agents).where(and(eq(agents.name, name), eq(agents.tenantId, tenantId))).limit(1);
16
+ return result[0] ?? null;
17
+ },
18
+
19
+ async findAll(tenantId: string = DEFAULT_TENANT_ID) {
20
+ return db.select().from(agents).where(eq(agents.tenantId, tenantId)).orderBy(agents.createdAt);
21
+ },
22
+
23
+ async countAll(tenantId: string = DEFAULT_TENANT_ID) {
24
+ const result = await db.select({ count: count() }).from(agents).where(eq(agents.tenantId, tenantId));
25
+ return result[0]?.count ?? 0;
26
+ },
27
+
28
+ async create(data: {
29
+ name: string;
30
+ description?: string | null;
31
+ provider?: string;
32
+ metadata?: Record<string, unknown>;
33
+ tenantId?: string;
34
+ }) {
35
+ return insertReturning(db, agents, {
36
+ ...data,
37
+ provider: data.provider ?? "custom",
38
+ metadata: data.metadata ?? {},
39
+ tenantId: data.tenantId ?? DEFAULT_TENANT_ID,
40
+ });
41
+ },
42
+
43
+ async update(id: string, data: Partial<{
44
+ name: string;
45
+ description: string | null;
46
+ provider: string;
47
+ status: string;
48
+ lastSeenAt: Date | string;
49
+ metadata: Record<string, unknown>;
50
+ }>, tenantId: string = DEFAULT_TENANT_ID) {
51
+ return updateReturning(db, agents, and(eq(agents.id, id), eq(agents.tenantId, tenantId)), {
52
+ ...data,
53
+ updatedAt: new Date().toISOString(),
54
+ });
55
+ },
56
+
57
+ async delete(id: string, tenantId: string = DEFAULT_TENANT_ID) {
58
+ await db.delete(agents).where(and(eq(agents.id, id), eq(agents.tenantId, tenantId)));
59
+ },
60
+
61
+ async heartbeat(id: string, tenantId: string = DEFAULT_TENANT_ID) {
62
+ await db
63
+ .update(agents)
64
+ .set({ status: "active", lastSeenAt: new Date().toISOString(), updatedAt: new Date().toISOString() })
65
+ .where(and(eq(agents.id, id), eq(agents.tenantId, tenantId)));
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,94 @@
1
+ import { eq, lt, and, isNull } from "drizzle-orm";
2
+ import type { Database } from "../client.js";
3
+ import { magicLinks, userSessions } from "../schema/index.js";
4
+
5
+ export function authQueries(db: Database) {
6
+ return {
7
+ // ── Magic Links ──────────────────────────────────────────────
8
+
9
+ createMagicLink(email: string, token: string, expiresAt: string) {
10
+ return db
11
+ .insert(magicLinks)
12
+ .values({ email, token, expiresAt })
13
+ .returning()
14
+ .get();
15
+ },
16
+
17
+ /** Returns the magic link only if valid (not used, not expired) */
18
+ findValidMagicLink(token: string) {
19
+ const now = new Date().toISOString();
20
+ return db
21
+ .select()
22
+ .from(magicLinks)
23
+ .where(
24
+ and(
25
+ eq(magicLinks.token, token),
26
+ isNull(magicLinks.usedAt),
27
+ // expiresAt > now (string comparison works for ISO 8601)
28
+ lt(magicLinks.createdAt, magicLinks.expiresAt), // always true, just for type
29
+ ),
30
+ )
31
+ .get() as typeof magicLinks.$inferSelect | undefined;
32
+ },
33
+
34
+ /** Fetch by token regardless of status (for validation logic in route) */
35
+ findMagicLink(token: string) {
36
+ return db
37
+ .select()
38
+ .from(magicLinks)
39
+ .where(eq(magicLinks.token, token))
40
+ .get() as typeof magicLinks.$inferSelect | undefined;
41
+ },
42
+
43
+ markMagicLinkUsed(id: string) {
44
+ return db
45
+ .update(magicLinks)
46
+ .set({ usedAt: new Date().toISOString() })
47
+ .where(eq(magicLinks.id, id))
48
+ .run();
49
+ },
50
+
51
+ /** Delete expired and used magic links (housekeeping) */
52
+ pruneExpiredMagicLinks() {
53
+ const now = new Date().toISOString();
54
+ return db
55
+ .delete(magicLinks)
56
+ .where(lt(magicLinks.expiresAt, now))
57
+ .run();
58
+ },
59
+
60
+ // ── User Sessions ────────────────────────────────────────────
61
+
62
+ createSession(email: string, token: string, expiresAt: string) {
63
+ return db
64
+ .insert(userSessions)
65
+ .values({ email, token, expiresAt })
66
+ .returning()
67
+ .get();
68
+ },
69
+
70
+ findSession(token: string) {
71
+ return db
72
+ .select()
73
+ .from(userSessions)
74
+ .where(eq(userSessions.token, token))
75
+ .get() as typeof userSessions.$inferSelect | undefined;
76
+ },
77
+
78
+ deleteSession(token: string) {
79
+ return db
80
+ .delete(userSessions)
81
+ .where(eq(userSessions.token, token))
82
+ .run();
83
+ },
84
+
85
+ /** Delete expired sessions (housekeeping) */
86
+ pruneExpiredSessions() {
87
+ const now = new Date().toISOString();
88
+ return db
89
+ .delete(userSessions)
90
+ .where(lt(userSessions.expiresAt, now))
91
+ .run();
92
+ },
93
+ };
94
+ }
@@ -0,0 +1,58 @@
1
+ import { eq, and, desc } from "drizzle-orm";
2
+ import type { Database } from "../client.js";
3
+ import { detectionResults } from "../schema/index.js";
4
+ import { DEFAULT_TENANT_ID } from "@opentrust/shared";
5
+
6
+ export function detectionResultQueries(db: Database) {
7
+ return {
8
+ async create(data: {
9
+ agentId?: string | null;
10
+ safe: boolean;
11
+ categories: string[];
12
+ sensitivityScore: number;
13
+ findings: unknown[];
14
+ latencyMs: number;
15
+ requestId: string;
16
+ tenantId?: string;
17
+ }) {
18
+ await db.insert(detectionResults).values({
19
+ ...data,
20
+ tenantId: data.tenantId ?? DEFAULT_TENANT_ID,
21
+ });
22
+ },
23
+
24
+ async findAll(options?: { limit?: number; offset?: number; tenantId?: string }) {
25
+ const tenantId = options?.tenantId ?? DEFAULT_TENANT_ID;
26
+ let query = db
27
+ .select()
28
+ .from(detectionResults)
29
+ .where(eq(detectionResults.tenantId, tenantId))
30
+ .orderBy(desc(detectionResults.createdAt));
31
+
32
+ if (options?.limit) {
33
+ query = query.limit(options.limit) as typeof query;
34
+ }
35
+ if (options?.offset) {
36
+ query = query.offset(options.offset) as typeof query;
37
+ }
38
+ return query;
39
+ },
40
+
41
+ async findByAgentId(agentId: string, options?: { limit?: number; offset?: number; tenantId?: string }) {
42
+ const tenantId = options?.tenantId ?? DEFAULT_TENANT_ID;
43
+ let query = db
44
+ .select()
45
+ .from(detectionResults)
46
+ .where(and(eq(detectionResults.agentId, agentId), eq(detectionResults.tenantId, tenantId)))
47
+ .orderBy(desc(detectionResults.createdAt));
48
+
49
+ if (options?.limit) {
50
+ query = query.limit(options.limit) as typeof query;
51
+ }
52
+ if (options?.offset) {
53
+ query = query.offset(options.offset) as typeof query;
54
+ }
55
+ return query;
56
+ },
57
+ };
58
+ }