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 +17 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +28 -0
- package/dist/client.js.map +1 -0
- package/dist/crypto.d.ts +19 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +55 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/queries.d.ts +229 -0
- package/dist/queries.d.ts.map +1 -0
- package/dist/queries.js +256 -0
- package/dist/queries.js.map +1 -0
- package/dist/schema.d.ts +990 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +100 -0
- package/dist/schema.js.map +1 -0
- package/package.json +57 -0
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
|
package/dist/client.d.ts
ADDED
|
@@ -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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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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"}
|
package/dist/queries.js
ADDED
|
@@ -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
|