@axiom-lattice/pg-stores 1.0.68 → 1.0.69
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 +9 -0
- package/dist/index.d.mts +71 -2
- package/dist/index.d.ts +71 -2
- package/dist/index.js +234 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +231 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/createPgStoreConfig.ts +2 -0
- package/src/index.ts +4 -0
- package/src/migrations/a2a_api_key_migration.ts +36 -0
- package/src/migrations/add_project_config_column.ts +34 -0
- package/src/migrations/migration.ts +25 -0
- package/src/stores/PostgreSQLA2AApiKeyStore.ts +187 -0
- package/src/stores/PostgreSQLProjectStore.ts +22 -5
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.69",
|
|
4
4
|
"description": "PG stores implementation for Axiom Lattice framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"pg": "^8.16.3",
|
|
24
24
|
"uuid": "^9.0.1",
|
|
25
|
-
"@axiom-lattice/core": "2.1.
|
|
26
|
-
"@axiom-lattice/protocols": "2.1.
|
|
25
|
+
"@axiom-lattice/core": "2.1.78",
|
|
26
|
+
"@axiom-lattice/protocols": "2.1.40"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^20.11.24",
|
|
@@ -20,6 +20,7 @@ import { PostgreSQLEvalStore } from "./stores/PostgreSQLEvalStore";
|
|
|
20
20
|
import { ThreadMessageQueueStore } from "./stores/ThreadMessageQueueStore";
|
|
21
21
|
import { ChannelBindingStore } from "./stores/ChannelBindingStore";
|
|
22
22
|
import { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
|
|
23
|
+
import { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
|
|
23
24
|
import { PostgreSQLScheduleStorage } from "./stores/PostgreSQLScheduleStorage";
|
|
24
25
|
|
|
25
26
|
export function createPgStoreConfig(connectionString: string) {
|
|
@@ -42,6 +43,7 @@ export function createPgStoreConfig(connectionString: string) {
|
|
|
42
43
|
assistant: new PostgreSQLAssistantStore(opts),
|
|
43
44
|
workflowTracking: new PostgreSQLWorkflowTrackingStore(optsAuto),
|
|
44
45
|
threadMessageQueue: new ThreadMessageQueueStore(optsAuto),
|
|
46
|
+
a2aApiKey: new PostgreSQLA2AApiKeyStore(optsAuto),
|
|
45
47
|
schedule: new PostgreSQLScheduleStorage(opts),
|
|
46
48
|
};
|
|
47
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export * from "./migrations/metrics_config_migrations";
|
|
|
39
39
|
export * from "./migrations/mcp_server_config_migrations";
|
|
40
40
|
export * from "./migrations/workspace_migrations";
|
|
41
41
|
export * from "./migrations/project_migrations";
|
|
42
|
+
export * from "./migrations/add_project_config_column";
|
|
42
43
|
export * from "./migrations/user_migrations";
|
|
43
44
|
export * from "./migrations/tenant_migrations";
|
|
44
45
|
export * from "./migrations/thread_message_queue_migrations";
|
|
@@ -46,11 +47,13 @@ export * from "./migrations/channel_identity_mapping_migration";
|
|
|
46
47
|
export * from "./migrations/channel_installation_migrations";
|
|
47
48
|
export * from "./migrations/channel_installations_alter_migration";
|
|
48
49
|
export * from "./migrations/channel_bindings_migration";
|
|
50
|
+
export * from "./migrations/a2a_api_key_migration";
|
|
49
51
|
export * from "./migrations/workflow_tracking_migrations";
|
|
50
52
|
export * from "./stores/ChannelBindingStore";
|
|
51
53
|
export * from "./stores/ThreadMessageQueueStore";
|
|
52
54
|
export * from "./stores/ChannelIdentityMappingStore";
|
|
53
55
|
export * from "./stores/PostgreSQLChannelInstallationStore";
|
|
56
|
+
export * from "./stores/PostgreSQLA2AApiKeyStore";
|
|
54
57
|
export * from "./stores/PostgreSQLWorkflowTrackingStore";
|
|
55
58
|
export * from "./stores/PostgreSQLEvalStore";
|
|
56
59
|
export * from "./migrations/eval_migrations";
|
|
@@ -71,6 +74,7 @@ export { PostgreSQLUserTenantLinkStore } from "./stores/PostgreSQLUserTenantLink
|
|
|
71
74
|
export { ChannelBindingStore } from "./stores/ChannelBindingStore";
|
|
72
75
|
export { ChannelIdentityMappingStore } from "./stores/ChannelIdentityMappingStore";
|
|
73
76
|
export { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
|
|
77
|
+
export { PostgreSQLA2AApiKeyStore } from "./stores/PostgreSQLA2AApiKeyStore";
|
|
74
78
|
export { PostgreSQLWorkflowTrackingStore } from "./stores/PostgreSQLWorkflowTrackingStore";
|
|
75
79
|
export { PostgreSQLEvalStore } from "./stores/PostgreSQLEvalStore";
|
|
76
80
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
import type { PoolClient } from "pg";
|
|
3
|
+
|
|
4
|
+
export const createA2AApiKeysTable: Migration = {
|
|
5
|
+
version: 130,
|
|
6
|
+
name: "create_a2a_api_keys_table",
|
|
7
|
+
up: async (client: PoolClient) => {
|
|
8
|
+
await client.query(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS lattice_a2a_api_keys (
|
|
10
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
11
|
+
key_value TEXT NOT NULL UNIQUE,
|
|
12
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
13
|
+
project_id VARCHAR(255),
|
|
14
|
+
workspace_id VARCHAR(255),
|
|
15
|
+
label VARCHAR(255),
|
|
16
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
17
|
+
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
18
|
+
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
19
|
+
)
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
await client.query(`
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_a2a_keys_tenant
|
|
24
|
+
ON lattice_a2a_api_keys(tenant_id)
|
|
25
|
+
`);
|
|
26
|
+
|
|
27
|
+
await client.query(`
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_a2a_keys_value
|
|
29
|
+
ON lattice_a2a_api_keys(key_value)
|
|
30
|
+
WHERE enabled = true
|
|
31
|
+
`);
|
|
32
|
+
},
|
|
33
|
+
down: async (client: PoolClient) => {
|
|
34
|
+
await client.query("DROP TABLE IF EXISTS lattice_a2a_api_keys");
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: Add config column to lattice_projects
|
|
3
|
+
*
|
|
4
|
+
* Adds a JSONB `config` column to the `lattice_projects` table for storing
|
|
5
|
+
* application-specific project configuration. Also creates a GIN index for
|
|
6
|
+
* efficient JSONB queries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PoolClient } from "pg";
|
|
10
|
+
import { Migration } from "./migration";
|
|
11
|
+
|
|
12
|
+
/** Adds config JSONB column + GIN index to lattice_projects */
|
|
13
|
+
export const addProjectConfigColumn: Migration = {
|
|
14
|
+
version: 24,
|
|
15
|
+
name: "add_project_config_column",
|
|
16
|
+
up: async (client: PoolClient) => {
|
|
17
|
+
await client.query(`
|
|
18
|
+
ALTER TABLE lattice_projects
|
|
19
|
+
ADD COLUMN IF NOT EXISTS config JSONB
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
await client.query(`
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_projects_config
|
|
24
|
+
ON lattice_projects USING GIN (config)
|
|
25
|
+
`);
|
|
26
|
+
},
|
|
27
|
+
down: async (client: PoolClient) => {
|
|
28
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_projects_config");
|
|
29
|
+
await client.query(`
|
|
30
|
+
ALTER TABLE lattice_projects
|
|
31
|
+
DROP COLUMN IF EXISTS config
|
|
32
|
+
`);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Migration system for database schema management
|
|
3
|
+
*
|
|
4
|
+
* ## Migration Version Rules
|
|
5
|
+
*
|
|
6
|
+
* Version numbers are globally unique across all migrations applied to the same
|
|
7
|
+
* database. They are NOT auto-assigned — you must pick one manually.
|
|
8
|
+
*
|
|
9
|
+
* ### Numbering scheme
|
|
10
|
+
*
|
|
11
|
+
* | Range | Purpose |
|
|
12
|
+
* |---------|--------------------------------------|
|
|
13
|
+
* | 1–99 | Core table creation + column changes |
|
|
14
|
+
* | 100+ | Independent feature modules |
|
|
15
|
+
*
|
|
16
|
+
* ### How to pick a version
|
|
17
|
+
*
|
|
18
|
+
* 1. Run `grep -rn "version:" src/migrations/ | sort -t: -k3 -n` to list all versions
|
|
19
|
+
* 2. For a core column change: find the next unused number in the 1–99 range
|
|
20
|
+
* 3. For a new feature module: find the next unused number in the 100+ range
|
|
21
|
+
* 4. Verify your chosen version is NOT in the grep output
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* - Migrations are sorted and applied in version order on startup
|
|
25
|
+
* - The version must be strictly greater than any migration it depends on
|
|
26
|
+
* - Use `IF NOT EXISTS` / `IF EXISTS` in SQL to keep migrations idempotent
|
|
27
|
+
* - A version number collision will cause one migration to be silently skipped
|
|
3
28
|
*/
|
|
4
29
|
|
|
5
30
|
import { Pool, PoolClient } from "pg";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
import type { PoolConfig } from "pg";
|
|
3
|
+
import type {
|
|
4
|
+
A2AApiKeyStore,
|
|
5
|
+
A2AApiKeyRecord,
|
|
6
|
+
CreateA2AApiKeyInput,
|
|
7
|
+
A2AApiKeyEntry,
|
|
8
|
+
} from "@axiom-lattice/protocols";
|
|
9
|
+
import { MigrationManager } from "../migrations/migration";
|
|
10
|
+
import { createA2AApiKeysTable } from "../migrations/a2a_api_key_migration";
|
|
11
|
+
import { encrypt, decrypt } from "@axiom-lattice/core";
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
|
|
14
|
+
export interface PostgreSQLA2AApiKeyStoreOptions {
|
|
15
|
+
poolConfig: string | PoolConfig;
|
|
16
|
+
autoMigrate?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type KeyRow = {
|
|
20
|
+
id: string;
|
|
21
|
+
key_value: string;
|
|
22
|
+
tenant_id: string;
|
|
23
|
+
project_id: string | null;
|
|
24
|
+
workspace_id: string | null;
|
|
25
|
+
label: string | null;
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
created_at: Date;
|
|
28
|
+
updated_at: Date;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function generateApiKey(): string {
|
|
32
|
+
return `a2a_${randomUUID().replace(/-/g, "")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mapRowToRecord(row: KeyRow): A2AApiKeyRecord {
|
|
36
|
+
return {
|
|
37
|
+
id: row.id,
|
|
38
|
+
key: decrypt(row.key_value),
|
|
39
|
+
tenantId: row.tenant_id,
|
|
40
|
+
projectId: row.project_id || undefined,
|
|
41
|
+
workspaceId: row.workspace_id || undefined,
|
|
42
|
+
label: row.label || undefined,
|
|
43
|
+
enabled: row.enabled,
|
|
44
|
+
createdAt: row.created_at,
|
|
45
|
+
updatedAt: row.updated_at,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class PostgreSQLA2AApiKeyStore implements A2AApiKeyStore {
|
|
50
|
+
private pool: Pool;
|
|
51
|
+
private migrationManager: MigrationManager;
|
|
52
|
+
private initialized = false;
|
|
53
|
+
private initPromise: Promise<void> | null = null;
|
|
54
|
+
|
|
55
|
+
constructor(options: PostgreSQLA2AApiKeyStoreOptions) {
|
|
56
|
+
this.pool =
|
|
57
|
+
typeof options.poolConfig === "string"
|
|
58
|
+
? new Pool({ connectionString: options.poolConfig })
|
|
59
|
+
: new Pool(options.poolConfig);
|
|
60
|
+
|
|
61
|
+
this.migrationManager = new MigrationManager(this.pool);
|
|
62
|
+
this.migrationManager.register(createA2AApiKeysTable);
|
|
63
|
+
|
|
64
|
+
if (options.autoMigrate !== false) {
|
|
65
|
+
this.initialize().catch((error) => {
|
|
66
|
+
console.error("Failed to initialize PostgreSQLA2AApiKeyStore:", error);
|
|
67
|
+
throw error;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async initialize(): Promise<void> {
|
|
73
|
+
if (this.initialized) return;
|
|
74
|
+
if (this.initPromise) return this.initPromise;
|
|
75
|
+
this.initPromise = (async () => {
|
|
76
|
+
try {
|
|
77
|
+
await this.migrationManager.migrate();
|
|
78
|
+
this.initialized = true;
|
|
79
|
+
} finally {
|
|
80
|
+
this.initPromise = null;
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
return this.initPromise;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async ensureInitialized(): Promise<void> {
|
|
87
|
+
if (!this.initialized) await this.initialize();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async findByKey(key: string): Promise<A2AApiKeyRecord | null> {
|
|
91
|
+
await this.ensureInitialized();
|
|
92
|
+
const all = await this.pool.query<KeyRow>(
|
|
93
|
+
`SELECT * FROM lattice_a2a_api_keys WHERE enabled = true`,
|
|
94
|
+
);
|
|
95
|
+
for (const row of all.rows) {
|
|
96
|
+
if (decrypt(row.key_value) === key) return mapRowToRecord(row);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async list(params: { tenantId?: string; limit?: number; offset?: number }): Promise<A2AApiKeyRecord[]> {
|
|
102
|
+
await this.ensureInitialized();
|
|
103
|
+
const limit = params.limit || 100;
|
|
104
|
+
const offset = params.offset || 0;
|
|
105
|
+
if (params.tenantId) {
|
|
106
|
+
const result = await this.pool.query<KeyRow>(
|
|
107
|
+
`SELECT * FROM lattice_a2a_api_keys WHERE tenant_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3`,
|
|
108
|
+
[params.tenantId, limit, offset],
|
|
109
|
+
);
|
|
110
|
+
return result.rows.map(mapRowToRecord);
|
|
111
|
+
}
|
|
112
|
+
const result = await this.pool.query<KeyRow>(
|
|
113
|
+
`SELECT * FROM lattice_a2a_api_keys ORDER BY created_at DESC LIMIT $1 OFFSET $2`,
|
|
114
|
+
[limit, offset],
|
|
115
|
+
);
|
|
116
|
+
return result.rows.map(mapRowToRecord);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async create(input: CreateA2AApiKeyInput): Promise<A2AApiKeyRecord> {
|
|
120
|
+
await this.ensureInitialized();
|
|
121
|
+
const key = generateApiKey();
|
|
122
|
+
const result = await this.pool.query<KeyRow>(
|
|
123
|
+
`INSERT INTO lattice_a2a_api_keys (key_value, tenant_id, project_id, workspace_id, label)
|
|
124
|
+
VALUES ($1, $2, $3, $4, $5) RETURNING *`,
|
|
125
|
+
[encrypt(key), input.tenantId, input.projectId || null, input.workspaceId || null, input.label || null],
|
|
126
|
+
);
|
|
127
|
+
const record = mapRowToRecord(result.rows[0]);
|
|
128
|
+
record.key = key;
|
|
129
|
+
return record;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async disable(id: string): Promise<A2AApiKeyRecord> {
|
|
133
|
+
await this.ensureInitialized();
|
|
134
|
+
const result = await this.pool.query<KeyRow>(
|
|
135
|
+
`UPDATE lattice_a2a_api_keys SET enabled = false, updated_at = NOW() WHERE id = $1 RETURNING *`,
|
|
136
|
+
[id],
|
|
137
|
+
);
|
|
138
|
+
if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
|
|
139
|
+
return mapRowToRecord(result.rows[0]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async enable(id: string): Promise<A2AApiKeyRecord> {
|
|
143
|
+
await this.ensureInitialized();
|
|
144
|
+
const result = await this.pool.query<KeyRow>(
|
|
145
|
+
`UPDATE lattice_a2a_api_keys SET enabled = true, updated_at = NOW() WHERE id = $1 RETURNING *`,
|
|
146
|
+
[id],
|
|
147
|
+
);
|
|
148
|
+
if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
|
|
149
|
+
return mapRowToRecord(result.rows[0]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async rotate(id: string): Promise<A2AApiKeyRecord> {
|
|
153
|
+
await this.ensureInitialized();
|
|
154
|
+
const key = generateApiKey();
|
|
155
|
+
const result = await this.pool.query<KeyRow>(
|
|
156
|
+
`UPDATE lattice_a2a_api_keys SET key_value = $1, updated_at = NOW() WHERE id = $2 RETURNING *`,
|
|
157
|
+
[encrypt(key), id],
|
|
158
|
+
);
|
|
159
|
+
if (result.rows.length === 0) throw new Error(`A2A API key not found: ${id}`);
|
|
160
|
+
const record = mapRowToRecord(result.rows[0]);
|
|
161
|
+
record.key = key;
|
|
162
|
+
return record;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async delete(id: string): Promise<void> {
|
|
166
|
+
await this.ensureInitialized();
|
|
167
|
+
await this.pool.query(`DELETE FROM lattice_a2a_api_keys WHERE id = $1`, [id]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async loadIntoMap(): Promise<Map<string, A2AApiKeyEntry>> {
|
|
171
|
+
await this.ensureInitialized();
|
|
172
|
+
const result = await this.pool.query<KeyRow>(
|
|
173
|
+
`SELECT * FROM lattice_a2a_api_keys WHERE enabled = true`,
|
|
174
|
+
);
|
|
175
|
+
const map = new Map<string, A2AApiKeyEntry>();
|
|
176
|
+
for (const row of result.rows) {
|
|
177
|
+
const key = decrypt(row.key_value);
|
|
178
|
+
map.set(key, {
|
|
179
|
+
key,
|
|
180
|
+
tenantId: row.tenant_id,
|
|
181
|
+
projectId: row.project_id || undefined,
|
|
182
|
+
workspaceId: row.workspace_id || undefined,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return map;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "@axiom-lattice/protocols";
|
|
13
13
|
import { MigrationManager } from "../migrations/migration";
|
|
14
14
|
import { createProjectsTable } from "../migrations/project_migrations";
|
|
15
|
+
import { addProjectConfigColumn } from "../migrations/add_project_config_column";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* PostgreSQL ProjectStore options
|
|
@@ -49,6 +50,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
49
50
|
|
|
50
51
|
this.migrationManager = new MigrationManager(this.pool);
|
|
51
52
|
this.migrationManager.register(createProjectsTable);
|
|
53
|
+
this.migrationManager.register(addProjectConfigColumn);
|
|
52
54
|
|
|
53
55
|
// Auto-migrate by default
|
|
54
56
|
if (options.autoMigrate !== false) {
|
|
@@ -99,6 +101,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
99
101
|
workspace_id: string;
|
|
100
102
|
name: string;
|
|
101
103
|
description: string | null;
|
|
104
|
+
config: unknown | null;
|
|
102
105
|
created_at: Date;
|
|
103
106
|
updated_at: Date;
|
|
104
107
|
}): Project {
|
|
@@ -108,6 +111,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
108
111
|
workspaceId: row.workspace_id,
|
|
109
112
|
name: row.name,
|
|
110
113
|
description: row.description || undefined,
|
|
114
|
+
config: row.config as Record<string, unknown> || undefined,
|
|
111
115
|
createdAt: row.created_at,
|
|
112
116
|
updatedAt: row.updated_at,
|
|
113
117
|
};
|
|
@@ -125,11 +129,12 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
125
129
|
workspace_id: string;
|
|
126
130
|
name: string;
|
|
127
131
|
description: string | null;
|
|
132
|
+
config: unknown | null;
|
|
128
133
|
created_at: Date;
|
|
129
134
|
updated_at: Date;
|
|
130
135
|
}>(
|
|
131
136
|
`
|
|
132
|
-
SELECT id, tenant_id, workspace_id, name, description, created_at, updated_at
|
|
137
|
+
SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
|
|
133
138
|
FROM lattice_projects
|
|
134
139
|
WHERE tenant_id = $1 AND workspace_id = $2
|
|
135
140
|
ORDER BY created_at DESC
|
|
@@ -152,11 +157,12 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
152
157
|
workspace_id: string;
|
|
153
158
|
name: string;
|
|
154
159
|
description: string | null;
|
|
160
|
+
config: unknown | null;
|
|
155
161
|
created_at: Date;
|
|
156
162
|
updated_at: Date;
|
|
157
163
|
}>(
|
|
158
164
|
`
|
|
159
|
-
SELECT id, tenant_id, workspace_id, name, description, created_at, updated_at
|
|
165
|
+
SELECT id, tenant_id, workspace_id, name, description, config, created_at, updated_at
|
|
160
166
|
FROM lattice_projects
|
|
161
167
|
WHERE id = $1 AND tenant_id = $2
|
|
162
168
|
`,
|
|
@@ -185,15 +191,16 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
185
191
|
|
|
186
192
|
await this.pool.query(
|
|
187
193
|
`
|
|
188
|
-
INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, created_at, updated_at)
|
|
189
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
194
|
+
INSERT INTO lattice_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
|
|
195
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
190
196
|
ON CONFLICT (id, tenant_id) DO UPDATE SET
|
|
191
197
|
workspace_id = EXCLUDED.workspace_id,
|
|
192
198
|
name = EXCLUDED.name,
|
|
193
199
|
description = EXCLUDED.description,
|
|
200
|
+
config = EXCLUDED.config,
|
|
194
201
|
updated_at = EXCLUDED.updated_at
|
|
195
202
|
`,
|
|
196
|
-
[id, tenantId, workspaceId, data.name, data.description || null, now, now]
|
|
203
|
+
[id, tenantId, workspaceId, data.name, data.description || null, data.config || null, now, now]
|
|
197
204
|
);
|
|
198
205
|
|
|
199
206
|
return {
|
|
@@ -202,6 +209,7 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
202
209
|
workspaceId,
|
|
203
210
|
name: data.name,
|
|
204
211
|
description: data.description,
|
|
212
|
+
config: data.config,
|
|
205
213
|
createdAt: now,
|
|
206
214
|
updatedAt: now,
|
|
207
215
|
};
|
|
@@ -209,6 +217,10 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
209
217
|
|
|
210
218
|
/**
|
|
211
219
|
* Update an existing project
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* - The `config` field uses **replace** semantics: if provided, it completely
|
|
223
|
+
* overwrites the existing config. To preserve the current config, omit this field.
|
|
212
224
|
*/
|
|
213
225
|
async updateProject(
|
|
214
226
|
tenantId: string,
|
|
@@ -238,6 +250,11 @@ export class PostgreSQLProjectStore implements ProjectStore {
|
|
|
238
250
|
updateValues.push(updates.description || null);
|
|
239
251
|
}
|
|
240
252
|
|
|
253
|
+
if (updates.config !== undefined) {
|
|
254
|
+
updateFields.push(`config = $${paramIndex++}`);
|
|
255
|
+
updateValues.push(updates.config || null);
|
|
256
|
+
}
|
|
257
|
+
|
|
241
258
|
if (updateFields.length === 0) {
|
|
242
259
|
// No fields to update
|
|
243
260
|
return existing;
|