ghagga-db 2.0.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/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @ghagga/db
2
+
3
+ Database layer for [GHAGGA](https://github.com/JNZader/ghagga) — AI-powered multi-agent code reviewer.
4
+
5
+ Provides schema definitions (Drizzle ORM), encryption utilities (AES-256-GCM), and migration tooling for the GHAGGA platform.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @ghagga/db
11
+ ```
12
+
13
+ > **Note:** This package is primarily used internally by `@ghagga/core` and `@ghagga/cli`. You probably want [`@ghagga/cli`](https://www.npmjs.com/package/@ghagga/cli) instead.
14
+
15
+ ## License
16
+
17
+ MIT
@@ -0,0 +1,17 @@
1
+ import * as schema from './schema.js';
2
+ export type Database = ReturnType<typeof createDatabase>;
3
+ /**
4
+ * Create a Drizzle database client from a connection string.
5
+ * Uses node-postgres (pg) as the driver.
6
+ */
7
+ export declare function createDatabase(connectionString: string): import("drizzle-orm/node-postgres").NodePgDatabase<typeof schema> & {
8
+ $client: import("pg").Pool;
9
+ };
10
+ /**
11
+ * Create a database client from the DATABASE_URL environment variable.
12
+ * Throws if DATABASE_URL is not set.
13
+ */
14
+ export declare function createDatabaseFromEnv(): import("drizzle-orm/node-postgres").NodePgDatabase<typeof schema> & {
15
+ $client: import("pg").Pool;
16
+ };
17
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,gBAAgB,EAAE,MAAM;;EAStD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB;;EAMpC"}
package/dist/client.js ADDED
@@ -0,0 +1,28 @@
1
+ import { drizzle } from 'drizzle-orm/node-postgres';
2
+ import pg from 'pg';
3
+ import * as schema from './schema.js';
4
+ /**
5
+ * Create a Drizzle database client from a connection string.
6
+ * Uses node-postgres (pg) as the driver.
7
+ */
8
+ export function createDatabase(connectionString) {
9
+ const pool = new pg.Pool({
10
+ connectionString,
11
+ max: 10,
12
+ idleTimeoutMillis: 30_000,
13
+ connectionTimeoutMillis: 5_000,
14
+ });
15
+ return drizzle(pool, { schema });
16
+ }
17
+ /**
18
+ * Create a database client from the DATABASE_URL environment variable.
19
+ * Throws if DATABASE_URL is not set.
20
+ */
21
+ export function createDatabaseFromEnv() {
22
+ const url = process.env.DATABASE_URL;
23
+ if (!url) {
24
+ throw new Error('DATABASE_URL environment variable is not set');
25
+ }
26
+ return createDatabase(url);
27
+ }
28
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACpD,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAItC;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,gBAAwB;IACrD,MAAM,IAAI,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC;QACvB,gBAAgB;QAChB,GAAG,EAAE,EAAE;QACP,iBAAiB,EAAE,MAAM;QACzB,uBAAuB,EAAE,KAAK;KAC/B,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC;IACrC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * AES-256-GCM encryption/decryption for API keys.
3
+ *
4
+ * Uses Node.js crypto module (Web Crypto API compatible).
5
+ * Format: base64(iv[12] + ciphertext + authTag[16])
6
+ *
7
+ * ENCRYPTION_KEY env var: 64 hex characters (32 bytes).
8
+ * Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
9
+ */
10
+ /**
11
+ * Encrypt plaintext using AES-256-GCM.
12
+ * @returns base64(iv[12] + ciphertext + authTag[16])
13
+ */
14
+ export declare function encrypt(plaintext: string): string;
15
+ /**
16
+ * Decrypt base64(iv[12] + ciphertext + authTag[16]) back to plaintext.
17
+ */
18
+ export declare function decrypt(base64str: string): string;
19
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmBH;;;GAGG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAWjD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAiBjD"}
package/dist/crypto.js ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * AES-256-GCM encryption/decryption for API keys.
3
+ *
4
+ * Uses Node.js crypto module (Web Crypto API compatible).
5
+ * Format: base64(iv[12] + ciphertext + authTag[16])
6
+ *
7
+ * ENCRYPTION_KEY env var: 64 hex characters (32 bytes).
8
+ * Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
9
+ */
10
+ import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
11
+ const IV_LENGTH = 12;
12
+ const AUTH_TAG_LENGTH = 16;
13
+ const ALGORITHM = 'aes-256-gcm';
14
+ function getEncryptionKey() {
15
+ const key = process.env.ENCRYPTION_KEY;
16
+ if (!key) {
17
+ throw new Error('ENCRYPTION_KEY environment variable is not set');
18
+ }
19
+ if (key.length !== 64 || !/^[0-9a-fA-F]+$/.test(key)) {
20
+ throw new Error('ENCRYPTION_KEY must be exactly 64 hex characters (32 bytes)');
21
+ }
22
+ return Buffer.from(key, 'hex');
23
+ }
24
+ /**
25
+ * Encrypt plaintext using AES-256-GCM.
26
+ * @returns base64(iv[12] + ciphertext + authTag[16])
27
+ */
28
+ export function encrypt(plaintext) {
29
+ const key = getEncryptionKey();
30
+ const iv = randomBytes(IV_LENGTH);
31
+ const cipher = createCipheriv(ALGORITHM, key, iv);
32
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
33
+ const authTag = cipher.getAuthTag();
34
+ // Combine: iv + ciphertext + authTag
35
+ const combined = Buffer.concat([iv, encrypted, authTag]);
36
+ return combined.toString('base64');
37
+ }
38
+ /**
39
+ * Decrypt base64(iv[12] + ciphertext + authTag[16]) back to plaintext.
40
+ */
41
+ export function decrypt(base64str) {
42
+ const key = getEncryptionKey();
43
+ const combined = Buffer.from(base64str, 'base64');
44
+ if (combined.length < IV_LENGTH + AUTH_TAG_LENGTH) {
45
+ throw new Error('Invalid encrypted data: too short');
46
+ }
47
+ const iv = combined.subarray(0, IV_LENGTH);
48
+ const authTag = combined.subarray(combined.length - AUTH_TAG_LENGTH);
49
+ const ciphertext = combined.subarray(IV_LENGTH, combined.length - AUTH_TAG_LENGTH);
50
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
51
+ decipher.setAuthTag(authTag);
52
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
53
+ return decrypted.toString('utf8');
54
+ }
55
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE5E,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,eAAe,GAAG,EAAE,CAAC;AAC3B,MAAM,SAAS,GAAG,aAAa,CAAC;AAEhC,SAAS,gBAAgB;IACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACvC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;AACjC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,SAAiB;IACvC,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAC/B,MAAM,EAAE,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAElC,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAEpC,qCAAqC;IACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,OAAO,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACrC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAAC,SAAiB;IACvC,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAElD,IAAI,QAAQ,CAAC,MAAM,GAAG,SAAS,GAAG,eAAe,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;IACrE,MAAM,UAAU,GAAG,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,GAAG,eAAe,CAAC,CAAC;IAEnF,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACtD,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAE7B,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACjF,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export * from './schema.js';
2
+ export { createDatabase, createDatabaseFromEnv, type Database } from './client.js';
3
+ export * from './queries.js';
4
+ export { encrypt, decrypt } from './crypto.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAC;AAG5B,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGnF,cAAc,cAAc,CAAC;AAG7B,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Schema
2
+ export * from './schema.js';
3
+ // Client
4
+ export { createDatabase, createDatabaseFromEnv } from './client.js';
5
+ // Queries
6
+ export * from './queries.js';
7
+ // Crypto
8
+ export { encrypt, decrypt } from './crypto.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,SAAS;AACT,cAAc,aAAa,CAAC;AAE5B,SAAS;AACT,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAiB,MAAM,aAAa,CAAC;AAEnF,UAAU;AACV,cAAc,cAAc,CAAC;AAE7B,SAAS;AACT,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,229 @@
1
+ import type { Database } from './client.js';
2
+ import { type RepoSettings } from './schema.js';
3
+ export declare function upsertInstallation(db: Database, data: {
4
+ githubInstallationId: number;
5
+ accountLogin: string;
6
+ accountType: string;
7
+ }): Promise<{
8
+ id: number;
9
+ githubInstallationId: number;
10
+ accountLogin: string;
11
+ accountType: string;
12
+ isActive: boolean;
13
+ createdAt: Date;
14
+ updatedAt: Date;
15
+ }>;
16
+ export declare function deactivateInstallation(db: Database, githubInstallationId: number): Promise<void>;
17
+ export declare function upsertRepository(db: Database, data: {
18
+ githubRepoId: number;
19
+ installationId: number;
20
+ fullName: string;
21
+ }): Promise<{
22
+ id: number;
23
+ isActive: boolean;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ githubRepoId: number;
27
+ installationId: number;
28
+ fullName: string;
29
+ settings: RepoSettings;
30
+ encryptedApiKey: string | null;
31
+ llmProvider: string;
32
+ llmModel: string | null;
33
+ reviewMode: string;
34
+ }>;
35
+ export declare function getRepoByFullName(db: Database, fullName: string): Promise<{
36
+ id: number;
37
+ githubRepoId: number;
38
+ installationId: number;
39
+ fullName: string;
40
+ isActive: boolean;
41
+ settings: RepoSettings;
42
+ encryptedApiKey: string | null;
43
+ llmProvider: string;
44
+ llmModel: string | null;
45
+ reviewMode: string;
46
+ createdAt: Date;
47
+ updatedAt: Date;
48
+ }>;
49
+ export declare function getRepoByGithubId(db: Database, githubRepoId: number): Promise<{
50
+ id: number;
51
+ githubRepoId: number;
52
+ installationId: number;
53
+ fullName: string;
54
+ isActive: boolean;
55
+ settings: RepoSettings;
56
+ encryptedApiKey: string | null;
57
+ llmProvider: string;
58
+ llmModel: string | null;
59
+ reviewMode: string;
60
+ createdAt: Date;
61
+ updatedAt: Date;
62
+ }>;
63
+ export declare function updateRepoSettings(db: Database, repoId: number, updates: {
64
+ settings?: RepoSettings;
65
+ llmProvider?: string;
66
+ llmModel?: string;
67
+ reviewMode?: string;
68
+ }): Promise<void>;
69
+ export declare function saveRepoApiKey(db: Database, repoId: number, encryptedKey: string): Promise<void>;
70
+ export declare function removeRepoApiKey(db: Database, repoId: number): Promise<void>;
71
+ export declare function getReposByInstallationId(db: Database, installationId: number): Promise<{
72
+ id: number;
73
+ githubRepoId: number;
74
+ installationId: number;
75
+ fullName: string;
76
+ isActive: boolean;
77
+ settings: RepoSettings;
78
+ encryptedApiKey: string | null;
79
+ llmProvider: string;
80
+ llmModel: string | null;
81
+ reviewMode: string;
82
+ createdAt: Date;
83
+ updatedAt: Date;
84
+ }[]>;
85
+ export declare function saveReview(db: Database, data: {
86
+ repositoryId: number;
87
+ prNumber: number;
88
+ status: string;
89
+ mode: string;
90
+ summary?: string;
91
+ findings?: unknown[];
92
+ tokensUsed?: number;
93
+ executionTimeMs?: number;
94
+ metadata?: unknown;
95
+ }): Promise<{
96
+ id: number;
97
+ mode: string;
98
+ createdAt: Date;
99
+ repositoryId: number;
100
+ prNumber: number;
101
+ status: string;
102
+ summary: string | null;
103
+ findings: unknown[] | null;
104
+ tokensUsed: number | null;
105
+ executionTimeMs: number | null;
106
+ metadata: unknown;
107
+ }>;
108
+ export declare function getReviewsByRepoId(db: Database, repositoryId: number, options?: {
109
+ limit?: number;
110
+ offset?: number;
111
+ }): Promise<{
112
+ id: number;
113
+ repositoryId: number;
114
+ prNumber: number;
115
+ status: string;
116
+ mode: string;
117
+ summary: string | null;
118
+ findings: unknown[] | null;
119
+ tokensUsed: number | null;
120
+ executionTimeMs: number | null;
121
+ metadata: unknown;
122
+ createdAt: Date;
123
+ }[]>;
124
+ export declare function getReviewStats(db: Database, repositoryId: number): Promise<{
125
+ total: number;
126
+ passed: number;
127
+ failed: number;
128
+ skipped: number;
129
+ }>;
130
+ export declare function createMemorySession(db: Database, data: {
131
+ project: string;
132
+ prNumber?: number;
133
+ }): Promise<{
134
+ id: number;
135
+ prNumber: number | null;
136
+ summary: string | null;
137
+ project: string;
138
+ startedAt: Date;
139
+ endedAt: Date | null;
140
+ }>;
141
+ export declare function endMemorySession(db: Database, sessionId: number, summary: string): Promise<void>;
142
+ export declare function getSessionsByProject(db: Database, project: string, options?: {
143
+ limit?: number;
144
+ }): Promise<{
145
+ id: number;
146
+ project: string;
147
+ prNumber: number | null;
148
+ summary: string | null;
149
+ startedAt: Date;
150
+ endedAt: Date | null;
151
+ }[]>;
152
+ export declare function saveObservation(db: Database, data: {
153
+ sessionId?: number;
154
+ project: string;
155
+ type: string;
156
+ title: string;
157
+ content: string;
158
+ topicKey?: string;
159
+ filePaths?: string[];
160
+ }): Promise<{
161
+ id: number;
162
+ createdAt: Date;
163
+ updatedAt: Date;
164
+ project: string;
165
+ sessionId: number | null;
166
+ type: string;
167
+ title: string;
168
+ content: string;
169
+ topicKey: string | null;
170
+ filePaths: string[] | null;
171
+ contentHash: string | null;
172
+ revisionCount: number;
173
+ }>;
174
+ /**
175
+ * Full-text search observations using PostgreSQL tsvector.
176
+ * The search_observations SQL column is maintained by a trigger.
177
+ */
178
+ export declare function searchObservations(db: Database, project: string, query: string, options?: {
179
+ limit?: number;
180
+ type?: string;
181
+ }): Promise<{
182
+ id: number;
183
+ sessionId: number | null;
184
+ project: string;
185
+ type: string;
186
+ title: string;
187
+ content: string;
188
+ topicKey: string | null;
189
+ filePaths: string[] | null;
190
+ contentHash: string | null;
191
+ revisionCount: number;
192
+ createdAt: Date;
193
+ updatedAt: Date;
194
+ }[]>;
195
+ export declare function getObservationsBySession(db: Database, sessionId: number): Promise<{
196
+ id: number;
197
+ sessionId: number | null;
198
+ project: string;
199
+ type: string;
200
+ title: string;
201
+ content: string;
202
+ topicKey: string | null;
203
+ filePaths: string[] | null;
204
+ contentHash: string | null;
205
+ revisionCount: number;
206
+ createdAt: Date;
207
+ updatedAt: Date;
208
+ }[]>;
209
+ export declare function upsertUserMapping(db: Database, data: {
210
+ githubUserId: number;
211
+ githubLogin: string;
212
+ installationId: number;
213
+ }): Promise<{
214
+ id: number;
215
+ createdAt: Date;
216
+ installationId: number;
217
+ githubUserId: number;
218
+ githubLogin: string;
219
+ }>;
220
+ export declare function getInstallationsByUserId(db: Database, githubUserId: number): Promise<{
221
+ id: number;
222
+ githubInstallationId: number;
223
+ accountLogin: string;
224
+ accountType: string;
225
+ isActive: boolean;
226
+ createdAt: Date;
227
+ updatedAt: Date;
228
+ }[]>;
229
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,256 @@
1
+ import { eq, and, desc, sql } from 'drizzle-orm';
2
+ import { installations, repositories, reviews, memorySessions, memoryObservations, githubUserMappings, DEFAULT_REPO_SETTINGS, } from './schema.js';
3
+ import { createHash } from 'node:crypto';
4
+ // ─── Installations ──────────────────────────────────────────────
5
+ export async function upsertInstallation(db, data) {
6
+ const existing = await db
7
+ .select()
8
+ .from(installations)
9
+ .where(eq(installations.githubInstallationId, data.githubInstallationId))
10
+ .limit(1);
11
+ if (existing.length > 0) {
12
+ await db
13
+ .update(installations)
14
+ .set({
15
+ accountLogin: data.accountLogin,
16
+ accountType: data.accountType,
17
+ isActive: true,
18
+ updatedAt: new Date(),
19
+ })
20
+ .where(eq(installations.githubInstallationId, data.githubInstallationId));
21
+ return existing[0];
22
+ }
23
+ const [result] = await db.insert(installations).values(data).returning();
24
+ return result;
25
+ }
26
+ export async function deactivateInstallation(db, githubInstallationId) {
27
+ await db
28
+ .update(installations)
29
+ .set({ isActive: false, updatedAt: new Date() })
30
+ .where(eq(installations.githubInstallationId, githubInstallationId));
31
+ }
32
+ // ─── Repositories ───────────────────────────────────────────────
33
+ export async function upsertRepository(db, data) {
34
+ const existing = await db
35
+ .select()
36
+ .from(repositories)
37
+ .where(eq(repositories.githubRepoId, data.githubRepoId))
38
+ .limit(1);
39
+ if (existing.length > 0) {
40
+ await db
41
+ .update(repositories)
42
+ .set({ fullName: data.fullName, isActive: true, updatedAt: new Date() })
43
+ .where(eq(repositories.githubRepoId, data.githubRepoId));
44
+ return existing[0];
45
+ }
46
+ const [result] = await db
47
+ .insert(repositories)
48
+ .values({ ...data, settings: DEFAULT_REPO_SETTINGS })
49
+ .returning();
50
+ return result;
51
+ }
52
+ export async function getRepoByFullName(db, fullName) {
53
+ const [repo] = await db
54
+ .select()
55
+ .from(repositories)
56
+ .where(eq(repositories.fullName, fullName))
57
+ .limit(1);
58
+ return repo ?? null;
59
+ }
60
+ export async function getRepoByGithubId(db, githubRepoId) {
61
+ const [repo] = await db
62
+ .select()
63
+ .from(repositories)
64
+ .where(eq(repositories.githubRepoId, githubRepoId))
65
+ .limit(1);
66
+ return repo ?? null;
67
+ }
68
+ export async function updateRepoSettings(db, repoId, updates) {
69
+ await db
70
+ .update(repositories)
71
+ .set({ ...updates, updatedAt: new Date() })
72
+ .where(eq(repositories.id, repoId));
73
+ }
74
+ export async function saveRepoApiKey(db, repoId, encryptedKey) {
75
+ await db
76
+ .update(repositories)
77
+ .set({ encryptedApiKey: encryptedKey, updatedAt: new Date() })
78
+ .where(eq(repositories.id, repoId));
79
+ }
80
+ export async function removeRepoApiKey(db, repoId) {
81
+ await db
82
+ .update(repositories)
83
+ .set({ encryptedApiKey: null, updatedAt: new Date() })
84
+ .where(eq(repositories.id, repoId));
85
+ }
86
+ export async function getReposByInstallationId(db, installationId) {
87
+ return db
88
+ .select()
89
+ .from(repositories)
90
+ .where(and(eq(repositories.installationId, installationId), eq(repositories.isActive, true)));
91
+ }
92
+ // ─── Reviews ────────────────────────────────────────────────────
93
+ export async function saveReview(db, data) {
94
+ const [result] = await db.insert(reviews).values(data).returning();
95
+ return result;
96
+ }
97
+ export async function getReviewsByRepoId(db, repositoryId, options = {}) {
98
+ const { limit = 50, offset = 0 } = options;
99
+ return db
100
+ .select()
101
+ .from(reviews)
102
+ .where(eq(reviews.repositoryId, repositoryId))
103
+ .orderBy(desc(reviews.createdAt))
104
+ .limit(limit)
105
+ .offset(offset);
106
+ }
107
+ export async function getReviewStats(db, repositoryId) {
108
+ const result = await db
109
+ .select({
110
+ total: sql `count(*)::int`,
111
+ passed: sql `count(*) filter (where ${reviews.status} = 'PASSED')::int`,
112
+ failed: sql `count(*) filter (where ${reviews.status} = 'FAILED')::int`,
113
+ skipped: sql `count(*) filter (where ${reviews.status} = 'SKIPPED')::int`,
114
+ })
115
+ .from(reviews)
116
+ .where(eq(reviews.repositoryId, repositoryId));
117
+ return result[0];
118
+ }
119
+ // ─── Memory: Sessions ───────────────────────────────────────────
120
+ export async function createMemorySession(db, data) {
121
+ const [session] = await db.insert(memorySessions).values(data).returning();
122
+ return session;
123
+ }
124
+ export async function endMemorySession(db, sessionId, summary) {
125
+ await db
126
+ .update(memorySessions)
127
+ .set({ endedAt: new Date(), summary })
128
+ .where(eq(memorySessions.id, sessionId));
129
+ }
130
+ export async function getSessionsByProject(db, project, options = {}) {
131
+ const { limit = 20 } = options;
132
+ return db
133
+ .select()
134
+ .from(memorySessions)
135
+ .where(eq(memorySessions.project, project))
136
+ .orderBy(desc(memorySessions.startedAt))
137
+ .limit(limit);
138
+ }
139
+ // ─── Memory: Observations ───────────────────────────────────────
140
+ function computeContentHash(content, type, title) {
141
+ return createHash('sha256').update(`${type}:${title}:${content}`).digest('hex');
142
+ }
143
+ const DEDUP_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
144
+ export async function saveObservation(db, data) {
145
+ const contentHash = computeContentHash(data.content, data.type, data.title);
146
+ const windowStart = new Date(Date.now() - DEDUP_WINDOW_MS);
147
+ // Deduplication: check for same content hash within rolling window
148
+ const [existing] = await db
149
+ .select()
150
+ .from(memoryObservations)
151
+ .where(and(eq(memoryObservations.contentHash, contentHash), eq(memoryObservations.project, data.project), sql `${memoryObservations.createdAt} > ${windowStart}`))
152
+ .limit(1);
153
+ if (existing) {
154
+ return existing; // Skip duplicate
155
+ }
156
+ // Topic-key upsert: update existing observation with same topic_key
157
+ if (data.topicKey) {
158
+ const [existingByTopic] = await db
159
+ .select()
160
+ .from(memoryObservations)
161
+ .where(and(eq(memoryObservations.topicKey, data.topicKey), eq(memoryObservations.project, data.project)))
162
+ .limit(1);
163
+ if (existingByTopic) {
164
+ const [updated] = await db
165
+ .update(memoryObservations)
166
+ .set({
167
+ content: data.content,
168
+ title: data.title,
169
+ contentHash,
170
+ filePaths: data.filePaths ?? [],
171
+ revisionCount: sql `${memoryObservations.revisionCount} + 1`,
172
+ updatedAt: new Date(),
173
+ })
174
+ .where(eq(memoryObservations.id, existingByTopic.id))
175
+ .returning();
176
+ return updated;
177
+ }
178
+ }
179
+ // New observation
180
+ const [result] = await db
181
+ .insert(memoryObservations)
182
+ .values({
183
+ ...data,
184
+ contentHash,
185
+ filePaths: data.filePaths ?? [],
186
+ })
187
+ .returning();
188
+ return result;
189
+ }
190
+ /**
191
+ * Full-text search observations using PostgreSQL tsvector.
192
+ * The search_observations SQL column is maintained by a trigger.
193
+ */
194
+ export async function searchObservations(db, project, query, options = {}) {
195
+ const { limit = 10, type } = options;
196
+ // Sanitize query: wrap each word in quotes for tsquery
197
+ const sanitizedQuery = query
198
+ .trim()
199
+ .split(/\s+/)
200
+ .filter((w) => w.length > 0)
201
+ .map((w) => `'${w.replace(/'/g, "''")}'`)
202
+ .join(' & ');
203
+ if (!sanitizedQuery)
204
+ return [];
205
+ const conditions = [
206
+ eq(memoryObservations.project, project),
207
+ sql `search_observations @@ to_tsquery('english', ${sanitizedQuery})`,
208
+ ];
209
+ if (type) {
210
+ conditions.push(eq(memoryObservations.type, type));
211
+ }
212
+ return db
213
+ .select()
214
+ .from(memoryObservations)
215
+ .where(and(...conditions))
216
+ .orderBy(sql `ts_rank(search_observations, to_tsquery('english', ${sanitizedQuery})) DESC`)
217
+ .limit(limit);
218
+ }
219
+ export async function getObservationsBySession(db, sessionId) {
220
+ return db
221
+ .select()
222
+ .from(memoryObservations)
223
+ .where(eq(memoryObservations.sessionId, sessionId))
224
+ .orderBy(desc(memoryObservations.createdAt));
225
+ }
226
+ // ─── User Mappings ──────────────────────────────────────────────
227
+ export async function upsertUserMapping(db, data) {
228
+ const existing = await db
229
+ .select()
230
+ .from(githubUserMappings)
231
+ .where(eq(githubUserMappings.githubUserId, data.githubUserId))
232
+ .limit(1);
233
+ if (existing.length > 0) {
234
+ await db
235
+ .update(githubUserMappings)
236
+ .set({ githubLogin: data.githubLogin, installationId: data.installationId })
237
+ .where(eq(githubUserMappings.githubUserId, data.githubUserId));
238
+ return existing[0];
239
+ }
240
+ const [result] = await db.insert(githubUserMappings).values(data).returning();
241
+ return result;
242
+ }
243
+ export async function getInstallationsByUserId(db, githubUserId) {
244
+ const mappings = await db
245
+ .select()
246
+ .from(githubUserMappings)
247
+ .where(eq(githubUserMappings.githubUserId, githubUserId));
248
+ if (mappings.length === 0)
249
+ return [];
250
+ const installationIds = mappings.map((m) => m.installationId);
251
+ return db
252
+ .select()
253
+ .from(installations)
254
+ .where(and(sql `${installations.id} = ANY(${installationIds})`, eq(installations.isActive, true)));
255
+ }
256
+ //# sourceMappingURL=queries.js.map