@axiom-lattice/pg-stores 1.0.85 → 1.0.87
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +18 -0
- package/dist/index.d.mts +83 -3
- package/dist/index.d.ts +83 -3
- package/dist/index.js +412 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +399 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/PGVectorStoreProvider.ts +113 -0
- package/src/createPgStoreConfig.ts +10 -0
- package/src/index.ts +8 -0
- package/src/migrations/collection_migrations.ts +41 -0
- package/src/migrations/connection_config_migrations.ts +50 -0
- package/src/stores/PostgreSQLCollectionStore.ts +165 -0
- package/src/stores/PostgreSQLConnectionStore.ts +128 -0
- package/src/stores/PostgreSQLMcpServerConfigStore.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/pg-stores",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.87",
|
|
4
4
|
"description": "PG stores implementation for Axiom Lattice framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -20,11 +20,13 @@
|
|
|
20
20
|
"author": "",
|
|
21
21
|
"license": "MIT",
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@langchain/community": "^0.3.40",
|
|
23
24
|
"@langchain/langgraph-checkpoint-postgres": "1.0.0",
|
|
25
|
+
"@langchain/core": "1.1.30",
|
|
24
26
|
"pg": "^8.16.3",
|
|
25
27
|
"uuid": "^9.0.1",
|
|
26
|
-
"@axiom-lattice/core": "2.1.
|
|
27
|
-
"@axiom-lattice/protocols": "2.1.
|
|
28
|
+
"@axiom-lattice/core": "2.1.96",
|
|
29
|
+
"@axiom-lattice/protocols": "2.1.50"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@types/node": "^20.11.24",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PGVectorStoreProvider
|
|
3
|
+
*
|
|
4
|
+
* VectorStoreProvider implementation using PostgreSQL with pgvector extension.
|
|
5
|
+
* Each created VectorStore maps to a separate table with the vector column.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Pool } from "pg";
|
|
9
|
+
import { PGVectorStore } from "@langchain/community/vectorstores/pgvector";
|
|
10
|
+
import { Document } from "@langchain/core/documents";
|
|
11
|
+
import { embeddingsLatticeManager } from "@axiom-lattice/core";
|
|
12
|
+
import type { VectorStoreProvider, VectorStoreCreateParams } from "@axiom-lattice/protocols";
|
|
13
|
+
import type { DocumentInterface } from "@langchain/core/documents";
|
|
14
|
+
|
|
15
|
+
function safeTableName(name: string): string {
|
|
16
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name)) {
|
|
17
|
+
throw new Error(`Invalid table name: ${name}`);
|
|
18
|
+
}
|
|
19
|
+
return name;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PGVectorStoreProvider implements VectorStoreProvider {
|
|
23
|
+
private pool: Pool;
|
|
24
|
+
private connectionString: string;
|
|
25
|
+
|
|
26
|
+
constructor(pool: Pool, connectionString: string) {
|
|
27
|
+
this.pool = pool;
|
|
28
|
+
this.connectionString = connectionString;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async create(params: VectorStoreCreateParams): Promise<any> {
|
|
32
|
+
const { tableName, embeddingKey } = params;
|
|
33
|
+
|
|
34
|
+
let embeddings;
|
|
35
|
+
try {
|
|
36
|
+
embeddings = embeddingsLatticeManager.getEmbeddingsClient(embeddingKey);
|
|
37
|
+
} catch {
|
|
38
|
+
try {
|
|
39
|
+
embeddings = embeddingsLatticeManager.getEmbeddingsClient("default");
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`No embedding model registered. Register one first:\n` +
|
|
43
|
+
` import { registerEmbeddingsLattice } from "@axiom-lattice/core";\n` +
|
|
44
|
+
` registerEmbeddingsLattice("default", new SomeEmbeddings(...));`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return PGVectorStore.initialize(embeddings, {
|
|
50
|
+
postgresConnectionOptions: { connectionString: this.connectionString },
|
|
51
|
+
tableName: safeTableName(tableName),
|
|
52
|
+
columns: {
|
|
53
|
+
idColumnName: "id",
|
|
54
|
+
vectorColumnName: "vector",
|
|
55
|
+
contentColumnName: "content",
|
|
56
|
+
metadataColumnName: "metadata",
|
|
57
|
+
},
|
|
58
|
+
distanceStrategy: "cosine" as any,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getEntryCount(tableName: string): Promise<number> {
|
|
63
|
+
const name = safeTableName(tableName);
|
|
64
|
+
try {
|
|
65
|
+
const result = await this.pool.query(`SELECT COUNT(*) as count FROM "${name}"`);
|
|
66
|
+
return parseInt(result.rows[0]?.count || "0", 10);
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async listEntries(tableName: string): Promise<DocumentInterface[]> {
|
|
73
|
+
const name = safeTableName(tableName);
|
|
74
|
+
try {
|
|
75
|
+
const result = await this.pool.query(
|
|
76
|
+
`SELECT content, metadata FROM "${name}" ORDER BY metadata->>'_created_at' DESC`
|
|
77
|
+
);
|
|
78
|
+
return result.rows.map((row: any) => new Document({
|
|
79
|
+
pageContent: row.content || "",
|
|
80
|
+
metadata: typeof row.metadata === "string" ? JSON.parse(row.metadata) : (row.metadata || {}),
|
|
81
|
+
}));
|
|
82
|
+
} catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async dropTable(tableName: string): Promise<void> {
|
|
88
|
+
const name = safeTableName(tableName);
|
|
89
|
+
await this.pool.query(`DROP TABLE IF EXISTS "${name}"`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async dispose(_name: string): Promise<void> {
|
|
93
|
+
// Pool is shared, don't close it
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a PGVectorStoreProvider from a connection string.
|
|
99
|
+
* Creates its own Pool — call pool.end() to clean up.
|
|
100
|
+
*
|
|
101
|
+
* @param connectionString PostgreSQL connection string
|
|
102
|
+
* @returns PGVectorStoreProvider instance
|
|
103
|
+
*/
|
|
104
|
+
export function createPGVectorStoreProvider(connectionString: string): {
|
|
105
|
+
provider: PGVectorStoreProvider;
|
|
106
|
+
pool: Pool;
|
|
107
|
+
} {
|
|
108
|
+
const pool = new Pool({ connectionString });
|
|
109
|
+
return {
|
|
110
|
+
provider: new PGVectorStoreProvider(pool, connectionString),
|
|
111
|
+
pool,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -3,6 +3,7 @@ import { MigrationManager } from "./migrations/migration";
|
|
|
3
3
|
import { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
|
|
4
4
|
import { PostgreSQLAssistantStore } from "./stores/PostgreSQLAssistantStore";
|
|
5
5
|
import { PostgreSQLDatabaseConfigStore } from "./stores/PostgreSQLDatabaseConfigStore";
|
|
6
|
+
import { PostgreSQLConnectionStore } from "./stores/PostgreSQLConnectionStore";
|
|
6
7
|
import { PostgreSQLMetricsServerConfigStore } from "./stores/PostgreSQLMetricsServerConfigStore";
|
|
7
8
|
import { PostgreSQLMcpServerConfigStore } from "./stores/PostgreSQLMcpServerConfigStore";
|
|
8
9
|
import { PostgreSQLWorkspaceStore } from "./stores/PostgreSQLWorkspaceStore";
|
|
@@ -20,7 +21,9 @@ import { PostgreSQLScheduleStorage } from "./stores/PostgreSQLScheduleStorage";
|
|
|
20
21
|
import { PostgreSQLTaskStore } from "./stores/PostgreSQLTaskStore";
|
|
21
22
|
import { MenuStore } from "./stores/MenuStore";
|
|
22
23
|
import { PostgresSharedResourceStore } from "./stores/PostgresSharedResourceStore";
|
|
24
|
+
import { PostgreSQLCollectionStore } from "./stores/PostgreSQLCollectionStore";
|
|
23
25
|
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
|
|
26
|
+
import { PGVectorStoreProvider } from "./PGVectorStoreProvider";
|
|
24
27
|
|
|
25
28
|
// Migration imports — all migrations registered centrally to guarantee
|
|
26
29
|
// ordering and prevent name collisions across features.
|
|
@@ -48,6 +51,7 @@ import { createChannelInstallationsTable } from "./migrations/channel_installati
|
|
|
48
51
|
import { alterChannelInstallationsTable } from "./migrations/channel_installations_alter_migration";
|
|
49
52
|
import { createChannelBindingsTable } from "./migrations/channel_bindings_migration";
|
|
50
53
|
import { createDatabaseConfigsTable } from "./migrations/database_config_migrations";
|
|
54
|
+
import { createConnectionConfigsTable } from "./migrations/connection_config_migrations";
|
|
51
55
|
import { createMetricsConfigsTable } from "./migrations/metrics_config_migrations";
|
|
52
56
|
import { createMcpServerConfigsTable } from "./migrations/mcp_server_config_migrations";
|
|
53
57
|
import { createThreadMessageQueueTable } from "./migrations/thread_message_queue_migrations";
|
|
@@ -62,6 +66,7 @@ import { createTasksTable } from "./migrations/task_migration";
|
|
|
62
66
|
import { createMenuItemsTable } from "./migrations/menu_items_migration";
|
|
63
67
|
import { addFileContentType } from "./migrations/menu_items_add_file_type";
|
|
64
68
|
import { createSharedResourcesTable } from "./migrations/shared_resources_migration";
|
|
69
|
+
import { createCollectionsTable } from "./migrations/collection_migrations";
|
|
65
70
|
|
|
66
71
|
export async function createPgStoreConfig(connectionString: string) {
|
|
67
72
|
const pool = new Pool({ connectionString });
|
|
@@ -112,6 +117,8 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
112
117
|
mm.register(createMenuItemsTable); // v134
|
|
113
118
|
mm.register(createSharedResourcesTable); // v135
|
|
114
119
|
mm.register(addFileContentType); // v136
|
|
120
|
+
mm.register(createCollectionsTable); // v137
|
|
121
|
+
mm.register(createConnectionConfigsTable); // v160
|
|
115
122
|
|
|
116
123
|
await mm.migrate();
|
|
117
124
|
|
|
@@ -133,6 +140,7 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
133
140
|
channelInstallation: new PostgreSQLChannelInstallationStore(opts),
|
|
134
141
|
thread: new PostgreSQLThreadStore(opts),
|
|
135
142
|
database: new PostgreSQLDatabaseConfigStore(opts),
|
|
143
|
+
connection: new PostgreSQLConnectionStore(pool),
|
|
136
144
|
metrics: new PostgreSQLMetricsServerConfigStore(opts),
|
|
137
145
|
mcp: new PostgreSQLMcpServerConfigStore(opts),
|
|
138
146
|
assistant: new PostgreSQLAssistantStore(opts),
|
|
@@ -143,6 +151,8 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
143
151
|
schedule: new PostgreSQLScheduleStorage(opts),
|
|
144
152
|
menu: new MenuStore(opts),
|
|
145
153
|
sharedResource: new PostgresSharedResourceStore(opts),
|
|
154
|
+
collection: new PostgreSQLCollectionStore(opts),
|
|
155
|
+
vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
|
|
146
156
|
checkpoint,
|
|
147
157
|
};
|
|
148
158
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from "./stores/PostgreSQLAssistantStore";
|
|
|
15
15
|
export * from "./stores/PostgreSQLScheduleStorage";
|
|
16
16
|
export * from "./stores/PostgreSQLSkillStore";
|
|
17
17
|
export * from "./stores/PostgreSQLDatabaseConfigStore";
|
|
18
|
+
export * from "./stores/PostgreSQLConnectionStore";
|
|
18
19
|
export * from "./stores/PostgreSQLMetricsServerConfigStore";
|
|
19
20
|
export * from "./stores/PostgreSQLMcpServerConfigStore";
|
|
20
21
|
export * from "./stores/PostgreSQLWorkspaceStore";
|
|
@@ -60,9 +61,13 @@ export * from "./stores/PostgreSQLEvalStore";
|
|
|
60
61
|
export * from "./migrations/eval_migrations";
|
|
61
62
|
export * from "./migrations/menu_items_migration";
|
|
62
63
|
export * from "./migrations/menu_items_add_file_type";
|
|
64
|
+
export * from "./migrations/collection_migrations";
|
|
63
65
|
export * from "./stores/MenuStore";
|
|
64
66
|
export * from "./stores/SharedResourceStore";
|
|
65
67
|
export * from "./stores/PostgresSharedResourceStore";
|
|
68
|
+
export * from "./stores/PostgreSQLCollectionStore";
|
|
69
|
+
|
|
70
|
+
export * from "./PGVectorStoreProvider";
|
|
66
71
|
|
|
67
72
|
// Re-export for convenience
|
|
68
73
|
export { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
|
|
@@ -70,6 +75,7 @@ export { PostgreSQLAssistantStore } from "./stores/PostgreSQLAssistantStore";
|
|
|
70
75
|
export { PostgreSQLScheduleStorage } from "./stores/PostgreSQLScheduleStorage";
|
|
71
76
|
export { PostgreSQLSkillStore } from "./stores/PostgreSQLSkillStore";
|
|
72
77
|
export { PostgreSQLDatabaseConfigStore } from "./stores/PostgreSQLDatabaseConfigStore";
|
|
78
|
+
export { PostgreSQLConnectionStore } from "./stores/PostgreSQLConnectionStore";
|
|
73
79
|
export { PostgreSQLMetricsServerConfigStore } from "./stores/PostgreSQLMetricsServerConfigStore";
|
|
74
80
|
export { PostgreSQLMcpServerConfigStore } from "./stores/PostgreSQLMcpServerConfigStore";
|
|
75
81
|
export { PostgreSQLWorkspaceStore } from "./stores/PostgreSQLWorkspaceStore";
|
|
@@ -107,6 +113,8 @@ export type {
|
|
|
107
113
|
UpdateDatabaseConfigRequest,
|
|
108
114
|
DatabaseConfig,
|
|
109
115
|
DatabaseType,
|
|
116
|
+
ConnectionStore,
|
|
117
|
+
ConnectionEntry,
|
|
110
118
|
Binding,
|
|
111
119
|
BindingRegistry,
|
|
112
120
|
CreateBindingInput,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collection table migrations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PoolClient } from "pg";
|
|
6
|
+
import { Migration } from "./migration";
|
|
7
|
+
|
|
8
|
+
export const createCollectionsTable: Migration = {
|
|
9
|
+
version: 137,
|
|
10
|
+
name: "create_collections_table",
|
|
11
|
+
up: async (client: PoolClient) => {
|
|
12
|
+
await client.query(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS lattice_collections (
|
|
14
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
15
|
+
name VARCHAR(255) NOT NULL,
|
|
16
|
+
tenant_id VARCHAR(255) NOT NULL DEFAULT 'default',
|
|
17
|
+
label VARCHAR(255) NOT NULL,
|
|
18
|
+
embedding_key VARCHAR(255) NOT NULL DEFAULT 'default',
|
|
19
|
+
schema JSONB DEFAULT '{"fields":[]}',
|
|
20
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
UNIQUE(tenant_id, name)
|
|
23
|
+
)
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
await client.query(`
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_collections_tenant
|
|
28
|
+
ON lattice_collections(tenant_id)
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
await client.query(`
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_collections_name
|
|
33
|
+
ON lattice_collections(name)
|
|
34
|
+
`);
|
|
35
|
+
},
|
|
36
|
+
down: async (client: PoolClient) => {
|
|
37
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_collections_name");
|
|
38
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_collections_tenant");
|
|
39
|
+
await client.query("DROP TABLE IF EXISTS lattice_collections");
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection configs table migration
|
|
3
|
+
*
|
|
4
|
+
* Generic connection storage for plugin connections (ERP, CRM, etc.).
|
|
5
|
+
* Uses JSONB for config/metadata to accommodate any plugin type's shape
|
|
6
|
+
* without schema changes.
|
|
7
|
+
*/
|
|
8
|
+
import { PoolClient } from "pg";
|
|
9
|
+
import { Migration } from "./migration";
|
|
10
|
+
|
|
11
|
+
export const createConnectionConfigsTable: Migration = {
|
|
12
|
+
version: 160,
|
|
13
|
+
name: "create_connection_configs_table",
|
|
14
|
+
up: async (client: PoolClient) => {
|
|
15
|
+
await client.query(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS lattice_connection_configs (
|
|
17
|
+
id VARCHAR(255) NOT NULL,
|
|
18
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
19
|
+
type VARCHAR(255) NOT NULL,
|
|
20
|
+
key VARCHAR(255) NOT NULL,
|
|
21
|
+
name VARCHAR(255) NOT NULL,
|
|
22
|
+
description TEXT,
|
|
23
|
+
config JSONB NOT NULL DEFAULT '{}',
|
|
24
|
+
status VARCHAR(32) NOT NULL DEFAULT 'active',
|
|
25
|
+
last_test_result JSONB,
|
|
26
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
27
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
28
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
29
|
+
|
|
30
|
+
PRIMARY KEY (tenant_id, id),
|
|
31
|
+
CONSTRAINT uk_lattice_connection_configs_tenant_type_key UNIQUE (tenant_id, type, key)
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
await client.query(`
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_connection_configs_tenant_id
|
|
37
|
+
ON lattice_connection_configs(tenant_id)
|
|
38
|
+
`);
|
|
39
|
+
|
|
40
|
+
await client.query(`
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_connection_configs_tenant_type
|
|
42
|
+
ON lattice_connection_configs(tenant_id, type)
|
|
43
|
+
`);
|
|
44
|
+
},
|
|
45
|
+
down: async (client: PoolClient) => {
|
|
46
|
+
await client.query("DROP INDEX IF EXISTS idx_connection_configs_tenant_type");
|
|
47
|
+
await client.query("DROP INDEX IF EXISTS idx_connection_configs_tenant_id");
|
|
48
|
+
await client.query("DROP TABLE IF EXISTS lattice_connection_configs");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL implementation of CollectionStore with tenant isolation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Pool } from "pg";
|
|
6
|
+
import type { PoolConfig } from "pg";
|
|
7
|
+
import {
|
|
8
|
+
type CollectionStore,
|
|
9
|
+
type Collection,
|
|
10
|
+
type CreateCollectionRequest,
|
|
11
|
+
type UpdateCollectionRequest,
|
|
12
|
+
} from "@axiom-lattice/protocols";
|
|
13
|
+
|
|
14
|
+
export interface PostgreSQLCollectionStoreOptions {
|
|
15
|
+
pool?: Pool;
|
|
16
|
+
poolConfig?: string | PoolConfig;
|
|
17
|
+
autoMigrate?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PostgreSQLCollectionStore implements CollectionStore {
|
|
21
|
+
private pool: Pool;
|
|
22
|
+
private initialized: boolean = false;
|
|
23
|
+
private ownsPool: boolean = true;
|
|
24
|
+
private initPromise: Promise<void> | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(options: PostgreSQLCollectionStoreOptions) {
|
|
27
|
+
if (options.pool) {
|
|
28
|
+
this.pool = options.pool;
|
|
29
|
+
this.ownsPool = false;
|
|
30
|
+
this.initialized = true;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof options.poolConfig === "string") {
|
|
35
|
+
this.pool = new Pool({ connectionString: options.poolConfig });
|
|
36
|
+
} else if (options.poolConfig) {
|
|
37
|
+
this.pool = new Pool(options.poolConfig as PoolConfig);
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error("Either pool or poolConfig must be provided");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.autoMigrate !== false) {
|
|
43
|
+
this.initialize().catch((error) => {
|
|
44
|
+
console.error("Failed to initialize PostgreSQLCollectionStore:", error);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async initialize(): Promise<void> {
|
|
50
|
+
if (this.initialized) return;
|
|
51
|
+
if (this.initPromise) return this.initPromise;
|
|
52
|
+
this.initPromise = (async () => {
|
|
53
|
+
// Table is created by the centralized migration in createPgStoreConfig
|
|
54
|
+
this.initialized = true;
|
|
55
|
+
})();
|
|
56
|
+
return this.initPromise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private ensureInit(): void {
|
|
60
|
+
if (!this.initialized) {
|
|
61
|
+
throw new Error("PostgreSQLCollectionStore not initialized");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getAllCollections(tenantId: string): Promise<Collection[]> {
|
|
66
|
+
this.ensureInit();
|
|
67
|
+
const result = await this.pool.query(
|
|
68
|
+
`SELECT id, name, tenant_id, label, embedding_key, schema, created_at, updated_at
|
|
69
|
+
FROM lattice_collections WHERE tenant_id = $1 ORDER BY name`,
|
|
70
|
+
[tenantId]
|
|
71
|
+
);
|
|
72
|
+
return result.rows.map(this.rowToCollection);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async getCollectionByName(tenantId: string, name: string): Promise<Collection | null> {
|
|
76
|
+
this.ensureInit();
|
|
77
|
+
const result = await this.pool.query(
|
|
78
|
+
`SELECT id, name, tenant_id, label, embedding_key, schema, created_at, updated_at
|
|
79
|
+
FROM lattice_collections WHERE tenant_id = $1 AND name = $2`,
|
|
80
|
+
[tenantId, name]
|
|
81
|
+
);
|
|
82
|
+
return result.rows.length > 0 ? this.rowToCollection(result.rows[0]) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async createCollection(tenantId: string, data: CreateCollectionRequest): Promise<Collection> {
|
|
86
|
+
this.ensureInit();
|
|
87
|
+
const schema = JSON.stringify(data.schema || { fields: [] });
|
|
88
|
+
const result = await this.pool.query(
|
|
89
|
+
`INSERT INTO lattice_collections (name, tenant_id, label, embedding_key, schema)
|
|
90
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
91
|
+
ON CONFLICT (tenant_id, name) DO NOTHING
|
|
92
|
+
RETURNING id, name, tenant_id, label, embedding_key, schema, created_at, updated_at`,
|
|
93
|
+
[data.name, tenantId, data.label, data.embeddingKey, schema]
|
|
94
|
+
);
|
|
95
|
+
if (result.rows.length === 0) {
|
|
96
|
+
throw new Error(`Collection "${data.name}" already exists`);
|
|
97
|
+
}
|
|
98
|
+
return this.rowToCollection(result.rows[0]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async updateCollection(
|
|
102
|
+
tenantId: string,
|
|
103
|
+
name: string,
|
|
104
|
+
updates: UpdateCollectionRequest
|
|
105
|
+
): Promise<Collection | null> {
|
|
106
|
+
this.ensureInit();
|
|
107
|
+
const setClauses: string[] = [];
|
|
108
|
+
const values: (string | number | boolean)[] = [];
|
|
109
|
+
let paramIndex = 1;
|
|
110
|
+
|
|
111
|
+
if (updates.label !== undefined) {
|
|
112
|
+
setClauses.push(`label = $${paramIndex++}`);
|
|
113
|
+
values.push(updates.label);
|
|
114
|
+
}
|
|
115
|
+
if (updates.embeddingKey !== undefined) {
|
|
116
|
+
setClauses.push(`embedding_key = $${paramIndex++}`);
|
|
117
|
+
values.push(updates.embeddingKey);
|
|
118
|
+
}
|
|
119
|
+
if (updates.schema !== undefined) {
|
|
120
|
+
setClauses.push(`schema = $${paramIndex++}`);
|
|
121
|
+
values.push(JSON.stringify(updates.schema));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (setClauses.length === 0) return this.getCollectionByName(tenantId, name);
|
|
125
|
+
|
|
126
|
+
setClauses.push(`updated_at = NOW()`);
|
|
127
|
+
values.push(tenantId, name);
|
|
128
|
+
|
|
129
|
+
const result = await this.pool.query(
|
|
130
|
+
`UPDATE lattice_collections SET ${setClauses.join(", ")}
|
|
131
|
+
WHERE tenant_id = $${paramIndex++} AND name = $${paramIndex}
|
|
132
|
+
RETURNING id, name, tenant_id, label, embedding_key, schema, created_at, updated_at`,
|
|
133
|
+
values
|
|
134
|
+
);
|
|
135
|
+
return result.rows.length > 0 ? this.rowToCollection(result.rows[0]) : null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async deleteCollection(tenantId: string, name: string): Promise<boolean> {
|
|
139
|
+
this.ensureInit();
|
|
140
|
+
const result = await this.pool.query(
|
|
141
|
+
`DELETE FROM lattice_collections WHERE tenant_id = $1 AND name = $2`,
|
|
142
|
+
[tenantId, name]
|
|
143
|
+
);
|
|
144
|
+
return (result.rowCount ?? 0) > 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async dispose(): Promise<void> {
|
|
148
|
+
if (this.ownsPool && this.pool) {
|
|
149
|
+
await this.pool.end();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private rowToCollection(row: any): Collection {
|
|
154
|
+
return {
|
|
155
|
+
id: row.id,
|
|
156
|
+
name: row.name,
|
|
157
|
+
tenantId: row.tenant_id,
|
|
158
|
+
label: row.label,
|
|
159
|
+
embeddingKey: row.embedding_key,
|
|
160
|
+
schema: typeof row.schema === "string" ? JSON.parse(row.schema) : row.schema,
|
|
161
|
+
createdAt: row.created_at,
|
|
162
|
+
updatedAt: row.updated_at,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL implementation of ConnectionStore
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Pool } from "pg";
|
|
6
|
+
import type { ConnectionEntry, ConnectionStore } from "@axiom-lattice/protocols";
|
|
7
|
+
import { v4 as uuid } from "uuid";
|
|
8
|
+
|
|
9
|
+
interface ConnectionRow {
|
|
10
|
+
id: string;
|
|
11
|
+
tenant_id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
key: string;
|
|
14
|
+
name: string;
|
|
15
|
+
description: string | null;
|
|
16
|
+
config: unknown;
|
|
17
|
+
status: string;
|
|
18
|
+
last_test_result: unknown;
|
|
19
|
+
metadata: unknown;
|
|
20
|
+
created_at: Date;
|
|
21
|
+
updated_at: Date;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TABLE = "lattice_connection_configs";
|
|
25
|
+
|
|
26
|
+
export class PostgreSQLConnectionStore implements ConnectionStore {
|
|
27
|
+
private db: Pool;
|
|
28
|
+
|
|
29
|
+
constructor(db: Pool) {
|
|
30
|
+
this.db = db;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async listByType(tenantId: string, type: string): Promise<ConnectionEntry[]> {
|
|
34
|
+
const result = await this.db.query<ConnectionRow>(
|
|
35
|
+
`SELECT * FROM ${TABLE} WHERE tenant_id = $1 AND type = $2 ORDER BY created_at`,
|
|
36
|
+
[tenantId, type],
|
|
37
|
+
);
|
|
38
|
+
return result.rows.map((row) => this.mapRow(row));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getByKey(tenantId: string, type: string, key: string): Promise<ConnectionEntry | null> {
|
|
42
|
+
const result = await this.db.query<ConnectionRow>(
|
|
43
|
+
`SELECT * FROM ${TABLE} WHERE tenant_id = $1 AND type = $2 AND key = $3`,
|
|
44
|
+
[tenantId, type, key],
|
|
45
|
+
);
|
|
46
|
+
if (result.rows.length === 0) return null;
|
|
47
|
+
return this.mapRow(result.rows[0]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async create(entry: Omit<ConnectionEntry, "id" | "createdAt" | "updatedAt">): Promise<ConnectionEntry> {
|
|
51
|
+
const id = uuid();
|
|
52
|
+
const now = new Date().toISOString();
|
|
53
|
+
|
|
54
|
+
await this.db.query(
|
|
55
|
+
`INSERT INTO ${TABLE} (id, tenant_id, type, key, name, description, config, status, created_at, updated_at)
|
|
56
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, 'active', $8::timestamp, $9::timestamp)`,
|
|
57
|
+
[
|
|
58
|
+
id, entry.tenantId, entry.type, entry.key, entry.name,
|
|
59
|
+
entry.description || null, JSON.stringify(entry.config), now, now,
|
|
60
|
+
],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return { id, ...entry, createdAt: now, updatedAt: now };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async update(
|
|
67
|
+
tenantId: string, type: string, key: string, updates: Partial<ConnectionEntry>,
|
|
68
|
+
): Promise<ConnectionEntry | null> {
|
|
69
|
+
const existing = await this.getByKey(tenantId, type, key);
|
|
70
|
+
if (!existing) return null;
|
|
71
|
+
|
|
72
|
+
const setClauses: string[] = [];
|
|
73
|
+
const values: unknown[] = [];
|
|
74
|
+
let paramIndex = 1;
|
|
75
|
+
|
|
76
|
+
if (updates.name !== undefined) {
|
|
77
|
+
setClauses.push(`name = $${paramIndex++}`);
|
|
78
|
+
values.push(updates.name);
|
|
79
|
+
}
|
|
80
|
+
if (updates.description !== undefined) {
|
|
81
|
+
setClauses.push(`description = $${paramIndex++}`);
|
|
82
|
+
values.push(updates.description || null);
|
|
83
|
+
}
|
|
84
|
+
if (updates.config !== undefined) {
|
|
85
|
+
setClauses.push(`config = $${paramIndex++}`);
|
|
86
|
+
values.push(JSON.stringify(updates.config));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (setClauses.length === 0) return existing;
|
|
90
|
+
|
|
91
|
+
setClauses.push(`updated_at = $${paramIndex++}::timestamp`);
|
|
92
|
+
values.push(new Date().toISOString());
|
|
93
|
+
|
|
94
|
+
values.push(tenantId, type, key);
|
|
95
|
+
const whereTenant = paramIndex++;
|
|
96
|
+
const whereType = paramIndex++;
|
|
97
|
+
const whereKey = paramIndex;
|
|
98
|
+
|
|
99
|
+
await this.db.query(
|
|
100
|
+
`UPDATE ${TABLE} SET ${setClauses.join(", ")} WHERE tenant_id = $${whereTenant} AND type = $${whereType} AND key = $${whereKey}`,
|
|
101
|
+
values,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return this.getByKey(tenantId, type, key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async delete(tenantId: string, type: string, key: string): Promise<boolean> {
|
|
108
|
+
const result = await this.db.query(
|
|
109
|
+
`DELETE FROM ${TABLE} WHERE tenant_id = $1 AND type = $2 AND key = $3`,
|
|
110
|
+
[tenantId, type, key],
|
|
111
|
+
);
|
|
112
|
+
return (result.rowCount ?? 0) > 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private mapRow(row: ConnectionRow): ConnectionEntry {
|
|
116
|
+
return {
|
|
117
|
+
id: row.id,
|
|
118
|
+
tenantId: row.tenant_id,
|
|
119
|
+
type: row.type,
|
|
120
|
+
key: row.key,
|
|
121
|
+
name: row.name,
|
|
122
|
+
description: row.description || undefined,
|
|
123
|
+
config: (typeof row.config === "string" ? JSON.parse(row.config) : row.config) as Record<string, unknown>,
|
|
124
|
+
createdAt: row.created_at instanceof Date ? row.created_at.toISOString() : String(row.created_at),
|
|
125
|
+
updatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -271,7 +271,7 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
271
271
|
data.name || null,
|
|
272
272
|
data.description || null,
|
|
273
273
|
JSON.stringify(configWithEncryptedEnv),
|
|
274
|
-
data.selectedTools || [],
|
|
274
|
+
JSON.stringify(data.selectedTools || []),
|
|
275
275
|
isEnvEncrypted,
|
|
276
276
|
'disconnected',
|
|
277
277
|
nowString,
|
|
@@ -331,7 +331,7 @@ export class PostgreSQLMcpServerConfigStore implements McpServerConfigStore {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
if (updates.selectedTools !== undefined) {
|
|
334
|
-
updateData.selected_tools = updates.selectedTools;
|
|
334
|
+
updateData.selected_tools = JSON.stringify(updates.selectedTools);
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
if (updates.status !== undefined) {
|