@axiom-lattice/pg-stores 1.0.83 → 1.0.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +9 -0
  3. package/dist/index.d.mts +124 -44
  4. package/dist/index.d.ts +124 -44
  5. package/dist/index.js +472 -162
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +472 -162
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +3 -3
  10. package/scripts/check-migration-versions.mjs +42 -9
  11. package/src/createPgStoreConfig.ts +97 -3
  12. package/src/migrations/migration.ts +91 -60
  13. package/src/stores/ChannelBindingStore.ts +21 -3
  14. package/src/stores/ChannelIdentityMappingStore.ts +21 -3
  15. package/src/stores/MenuStore.ts +21 -3
  16. package/src/stores/PostgreSQLA2AApiKeyStore.ts +21 -3
  17. package/src/stores/PostgreSQLAssistantStore.ts +19 -3
  18. package/src/stores/PostgreSQLChannelInstallationStore.ts +21 -3
  19. package/src/stores/PostgreSQLDatabaseConfigStore.ts +23 -4
  20. package/src/stores/PostgreSQLEvalStore.ts +15 -3
  21. package/src/stores/PostgreSQLMcpServerConfigStore.ts +23 -4
  22. package/src/stores/PostgreSQLMetricsServerConfigStore.ts +23 -4
  23. package/src/stores/PostgreSQLProjectStore.ts +19 -3
  24. package/src/stores/PostgreSQLScheduleStorage.ts +21 -4
  25. package/src/stores/PostgreSQLSkillStore.ts +14 -3
  26. package/src/stores/PostgreSQLTaskStore.ts +14 -3
  27. package/src/stores/PostgreSQLTenantStore.ts +19 -3
  28. package/src/stores/PostgreSQLThreadStore.ts +19 -3
  29. package/src/stores/PostgreSQLUserStore.ts +18 -4
  30. package/src/stores/PostgreSQLUserTenantLinkStore.ts +18 -4
  31. package/src/stores/PostgreSQLWorkflowTrackingStore.ts +18 -4
  32. package/src/stores/PostgreSQLWorkspaceStore.ts +19 -3
  33. package/src/stores/PostgresSharedResourceStore.ts +21 -3
  34. package/src/stores/ThreadMessageQueueStore.ts +14 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "1.0.83",
3
+ "version": "1.0.84",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -23,8 +23,8 @@
23
23
  "@langchain/langgraph-checkpoint-postgres": "1.0.0",
24
24
  "pg": "^8.16.3",
25
25
  "uuid": "^9.0.1",
26
- "@axiom-lattice/core": "2.1.92",
27
- "@axiom-lattice/protocols": "2.1.46"
26
+ "@axiom-lattice/core": "2.1.93",
27
+ "@axiom-lattice/protocols": "2.1.47"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^20.11.24",
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Check for duplicate migration version numbers.
3
- * Exits with code 1 if any version collision is found.
2
+ * Migration identity checker.
3
+ *
4
+ * Migration identity is now based on **name**, not version.
5
+ * Duplicate names are ERRORS (same name = same migration = can't register twice).
6
+ * Duplicate versions are WARNINGS (allowed but should be intentional).
7
+ *
4
8
  * Run as part of lint pipeline.
5
9
  */
6
10
  import { readFileSync, readdirSync } from "node:fs";
@@ -10,22 +14,47 @@ import { fileURLToPath } from "node:url";
10
14
  const MIGRATIONS_DIR = join(dirname(fileURLToPath(import.meta.url)), "../src/migrations");
11
15
 
12
16
  const files = readdirSync(MIGRATIONS_DIR).filter(f => f.endsWith(".ts"));
17
+ const nameMap = new Map();
13
18
  const versionMap = new Map();
14
19
 
15
20
  let hasError = false;
16
21
 
17
22
  for (const file of files) {
18
23
  const content = readFileSync(join(MIGRATIONS_DIR, file), "utf-8");
19
- const matches = content.matchAll(/^\s*version:\s*(\d+)/gm);
20
- for (const m of matches) {
21
- const version = parseInt(m[1]);
22
- if (versionMap.has(version)) {
24
+
25
+ // Extract name and version from migration definitions
26
+ // Matches patterns like:
27
+ // name: "create_threads_table",
28
+ // version: 1,
29
+ const nameMatches = content.matchAll(/^\s*name:\s*"([^"]+)"/gm);
30
+ const versionMatches = content.matchAll(/^\s*version:\s*(\d+)/gm);
31
+
32
+ const names = [...nameMatches].map(m => m[1]);
33
+ const versions = [...versionMatches].map(m => parseInt(m[1]));
34
+
35
+ for (let i = 0; i < names.length; i++) {
36
+ const name = names[i];
37
+ const version = versions[i];
38
+
39
+ // NAME conflict = ERROR (identity collision)
40
+ if (nameMap.has(name)) {
23
41
  console.error(
24
- `\x1b[31mMIGRATION VERSION CONFLICT:\x1b[0m v${version} in both "${versionMap.get(version)}" and "${file}"`
42
+ `\x1b[31mMIGRATION NAME CONFLICT:\x1b[0m "${name}" (v${version} in "${file}") ` +
43
+ `collides with "${name}" in "${nameMap.get(name)}"`
25
44
  );
26
45
  hasError = true;
27
46
  } else {
28
- versionMap.set(version, file);
47
+ nameMap.set(name, file);
48
+ }
49
+
50
+ // VERSION duplicate = WARNING (allowed but flag for review)
51
+ if (versionMap.has(version)) {
52
+ console.warn(
53
+ `\x1b[33mDuplicate version:\x1b[0m v${version} used by "${name}" (${file}) ` +
54
+ `and in "${versionMap.get(version)}" — ensure no ordering dependency`
55
+ );
56
+ } else {
57
+ versionMap.set(version, `${name} (${file})`);
29
58
  }
30
59
  }
31
60
  }
@@ -34,4 +63,8 @@ if (hasError) {
34
63
  process.exit(1);
35
64
  }
36
65
 
37
- console.log(`\x1b[32mMigration versions OK\x1b[0m (${versionMap.size} unique versions in ${files.length} files)`);
66
+ console.log(`\x1b[32mMigration names OK\x1b[0m (${nameMap.size} unique names in ${files.length} files)`);
67
+ const dupVersions = [...versionMap.keys()].length;
68
+ if (dupVersions !== nameMap.size) {
69
+ console.log(`\x1b[33m${nameMap.size - dupVersions} version(s) shared across migrations\x1b[0m`);
70
+ }
@@ -1,3 +1,5 @@
1
+ import { Pool } from "pg";
2
+ import { MigrationManager } from "./migrations/migration";
1
3
  import { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
2
4
  import { PostgreSQLAssistantStore } from "./stores/PostgreSQLAssistantStore";
3
5
  import { PostgreSQLDatabaseConfigStore } from "./stores/PostgreSQLDatabaseConfigStore";
@@ -20,14 +22,106 @@ import { MenuStore } from "./stores/MenuStore";
20
22
  import { PostgresSharedResourceStore } from "./stores/PostgresSharedResourceStore";
21
23
  import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
22
24
 
23
- export function createPgStoreConfig(connectionString: string) {
24
- const opts = { poolConfig: connectionString, autoMigrate: true } as const;
25
+ // Migration imports all migrations registered centrally to guarantee
26
+ // ordering and prevent name collisions across features.
27
+ import { createThreadsTable } from "./migrations/thread_migrations";
28
+ import { addThreadTenantId } from "./migrations/thread_tenant_migration";
29
+ import { changeThreadPrimaryKey } from "./migrations/thread_pk_migration";
30
+ import { createScheduledTasksTable } from "./migrations/schedule_migrations";
31
+ import { addScheduleTenantId } from "./migrations/schedule_tenant_migration";
32
+ import { createAssistantsTable } from "./migrations/assistant_migrations";
33
+ import { addAssistantTenantId } from "./migrations/assistant_tenant_migration";
34
+ import { changeAssistantPrimaryKey } from "./migrations/assistant_pk_migration";
35
+ import { addAssistantOwnerUserId } from "./migrations/assistant_owner_user_migration";
36
+ import { createSkillsTable } from "./migrations/skill_migrations";
37
+ import { addSkillTenantId } from "./migrations/skill_tenant_migration";
38
+ import { changeSkillPrimaryKey } from "./migrations/skill_pk_migration";
39
+ import { createWorkspacesTable } from "./migrations/workspace_migrations";
40
+ import { createProjectsTable } from "./migrations/project_migrations";
41
+ import { addProjectConfigColumn } from "./migrations/add_project_config_column";
42
+ import { createUsersTable } from "./migrations/user_migrations";
43
+ import { addUserStatusColumn } from "./migrations/user_status_migration";
44
+ import { createTenantsTable } from "./migrations/tenant_migrations";
45
+ import { createUserTenantLinkTable, migrateRemoveTenantIdFromUsers } from "./migrations/user_tenant_link_migrations";
46
+ import { createChannelIdentityMappingTables } from "./migrations/channel_identity_mapping_migration";
47
+ import { createChannelInstallationsTable } from "./migrations/channel_installation_migrations";
48
+ import { alterChannelInstallationsTable } from "./migrations/channel_installations_alter_migration";
49
+ import { createChannelBindingsTable } from "./migrations/channel_bindings_migration";
50
+ import { createDatabaseConfigsTable } from "./migrations/database_config_migrations";
51
+ import { createMetricsConfigsTable } from "./migrations/metrics_config_migrations";
52
+ import { createMcpServerConfigsTable } from "./migrations/mcp_server_config_migrations";
53
+ import { createThreadMessageQueueTable } from "./migrations/thread_message_queue_migrations";
54
+ import { addPriorityAndCommandColumns } from "./migrations/add_priority_command_columns";
55
+ import { addCustomRunConfigColumn } from "./migrations/add_custom_run_config_column";
56
+ import { alterMessageQueueIdColumn } from "./migrations/alter_message_queue_id_column";
57
+ import { addWorkspaceProjectToQueue } from "./migrations/add_workspace_project_to_queue";
58
+ import { createWorkflowTrackingTables, addStepThreadId } from "./migrations/workflow_tracking_migrations";
59
+ import { evalMigrations } from "./migrations/eval_migrations";
60
+ import { createA2AApiKeysTable } from "./migrations/a2a_api_key_migration";
61
+ import { createTasksTable } from "./migrations/task_migration";
62
+ import { createMenuItemsTable } from "./migrations/menu_items_migration";
63
+ import { addFileContentType } from "./migrations/menu_items_add_file_type";
64
+ import { createSharedResourcesTable } from "./migrations/shared_resources_migration";
65
+
66
+ export async function createPgStoreConfig(connectionString: string) {
67
+ const pool = new Pool({ connectionString });
68
+
69
+ // Centralized migration manager — all migrations registered here
70
+ // in one place. Name conflicts throw immediately. Execution order
71
+ // follows version numbers globally.
72
+ const mm = new MigrationManager(pool);
73
+ mm.register(createThreadsTable); // v1
74
+ mm.register(createScheduledTasksTable); // v2
75
+ mm.register(createAssistantsTable); // v3
76
+ mm.register(createSkillsTable); // v4
77
+ mm.register(createWorkspacesTable); // v5
78
+ mm.register(createProjectsTable); // v6
79
+ mm.register(createUsersTable); // v7
80
+ mm.register(createTenantsTable); // v8
81
+ mm.register(addUserStatusColumn); // v9
82
+ mm.register(createUserTenantLinkTable); // v10
83
+ mm.register(migrateRemoveTenantIdFromUsers); // v11
84
+ mm.register(createMetricsConfigsTable); // v12
85
+ mm.register(createMcpServerConfigsTable); // v13
86
+ mm.register(addAssistantTenantId); // v14
87
+ mm.register(addSkillTenantId); // v16
88
+ mm.register(changeSkillPrimaryKey); // v17
89
+ mm.register(addScheduleTenantId); // v18
90
+ mm.register(changeThreadPrimaryKey); // v19
91
+ mm.register(createChannelIdentityMappingTables); // v20
92
+ mm.register(createChannelInstallationsTable); // v21
93
+ mm.register(addThreadTenantId); // v23
94
+ mm.register(addProjectConfigColumn); // v24
95
+ mm.register(createThreadMessageQueueTable); // v100
96
+ mm.register(addPriorityAndCommandColumns); // v101
97
+ mm.register(alterMessageQueueIdColumn); // v102
98
+ mm.register(createWorkflowTrackingTables); // v103
99
+ for (const m of evalMigrations) { // v104-v108
100
+ mm.register(m);
101
+ }
102
+ mm.register(alterChannelInstallationsTable); // v109
103
+ mm.register(createChannelBindingsTable); // v110
104
+ mm.register(createDatabaseConfigsTable); // v111
105
+ mm.register(changeAssistantPrimaryKey); // v112
106
+ mm.register(addStepThreadId); // v113
107
+ mm.register(addCustomRunConfigColumn); // v114
108
+ mm.register(createA2AApiKeysTable); // v130
109
+ mm.register(addWorkspaceProjectToQueue); // v131
110
+ mm.register(addAssistantOwnerUserId); // v132
111
+ mm.register(createTasksTable); // v133
112
+ mm.register(createMenuItemsTable); // v134
113
+ mm.register(createSharedResourcesTable); // v135
114
+ mm.register(addFileContentType); // v136
115
+
116
+ await mm.migrate();
25
117
 
26
118
  const checkpoint = PostgresSaver.fromConnString(connectionString);
27
119
  checkpoint.setup().catch((err) => {
28
120
  console.error("[pg-stores] Failed to setup checkpoint table:", err.message || err);
29
121
  });
30
122
 
123
+ const opts = { pool };
124
+
31
125
  return {
32
126
  workspace: new PostgreSQLWorkspaceStore(opts),
33
127
  project: new PostgreSQLProjectStore(opts),
@@ -51,4 +145,4 @@ export function createPgStoreConfig(connectionString: string) {
51
145
  sharedResource: new PostgresSharedResourceStore(opts),
52
146
  checkpoint,
53
147
  };
54
- }
148
+ }
@@ -1,30 +1,29 @@
1
1
  /**
2
2
  * Migration system for database schema management
3
3
  *
4
- * ## Migration Version Rules
4
+ * ## Identity vs Ordering
5
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.
6
+ * **name = identity** each migration is uniquely identified by its name.
7
+ * Two migrations MAY share the same version number; they are tracked
8
+ * independently by name and will both execute.
8
9
  *
9
- * ### Numbering scheme
10
+ * **version = ordering** — migrations are sorted and applied in ascending
11
+ * version order. Sharing a version simply means two migrations are at the
12
+ * same position in the dependency chain (they operate on different tables).
13
+ *
14
+ * ### Version numbering scheme
10
15
  *
11
16
  * | Range | Purpose |
12
17
  * |---------|--------------------------------------|
13
18
  * | 1–99 | Core table creation + column changes |
14
19
  * | 100+ | Independent feature modules |
15
20
  *
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
21
  * @remarks
24
- * - Migrations are sorted and applied in version order on startup
22
+ * - Migrations are applied in version order on startup
25
23
  * - The version must be strictly greater than any migration it depends on
26
24
  * - 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
25
+ * - Duplicate **names** are rejected at registration time
26
+ * - Duplicate **versions** are allowed (different features, different tables)
28
27
  */
29
28
 
30
29
  import { Pool, PoolClient } from "pg";
@@ -33,8 +32,8 @@ import { Pool, PoolClient } from "pg";
33
32
  * Migration record stored in database
34
33
  */
35
34
  interface MigrationRecord {
36
- version: number;
37
35
  name: string;
36
+ version: number;
38
37
  applied_at: Date;
39
38
  }
40
39
 
@@ -60,45 +59,75 @@ export class MigrationManager {
60
59
  }
61
60
 
62
61
  /**
63
- * Register a migration
62
+ * Register a migration.
63
+ * Duplicate names are rejected immediately (name = identity).
64
+ * Duplicate versions are allowed (version = ordering only).
64
65
  */
65
66
  register(migration: Migration): void {
67
+ const existing = this.migrations.find((m) => m.name === migration.name);
68
+ if (existing) {
69
+ throw new Error(
70
+ `Migration name conflict: "${migration.name}" v${migration.version} ` +
71
+ `collides with existing registration at v${existing.version}`
72
+ );
73
+ }
66
74
  this.migrations.push(migration);
67
- // Sort migrations by version
68
75
  this.migrations.sort((a, b) => a.version - b.version);
69
76
  }
70
77
 
71
78
  /**
72
- * Initialize migrations table if it doesn't exist
79
+ * Ensure the tracking table exists with the correct schema.
80
+ * Handles migration from old schema (PK on version) to new schema (PK on name).
73
81
  */
74
82
  private async ensureMigrationsTable(client: PoolClient): Promise<void> {
75
- await client.query(`
76
- CREATE TABLE IF NOT EXISTS lattice_schema_migrations (
77
- version INTEGER PRIMARY KEY,
78
- name VARCHAR(255) NOT NULL,
79
- applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
83
+ const tableExists = await client.query(`
84
+ SELECT EXISTS (
85
+ SELECT FROM information_schema.tables
86
+ WHERE table_name = 'lattice_schema_migrations'
80
87
  )
81
88
  `);
89
+
90
+ if (!tableExists.rows[0].exists) {
91
+ await client.query(`
92
+ CREATE TABLE lattice_schema_migrations (
93
+ name VARCHAR(255) PRIMARY KEY,
94
+ version INTEGER NOT NULL,
95
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
96
+ )
97
+ `);
98
+ return;
99
+ }
100
+
101
+ // Check if we need to migrate PK from version to name
102
+ const pkCheck = await client.query<{ column_name: string; constraint_name: string }>(`
103
+ SELECT a.attname AS column_name, con.conname AS constraint_name
104
+ FROM pg_index i
105
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
106
+ JOIN pg_constraint con ON con.conindid = i.indexrelid
107
+ WHERE i.indrelid = 'lattice_schema_migrations'::regclass
108
+ AND i.indisprimary
109
+ `);
110
+
111
+ const pkColumn = pkCheck.rows[0]?.column_name;
112
+ const constraintName = pkCheck.rows[0]?.constraint_name;
113
+ if (pkColumn === 'version' && constraintName) {
114
+ await client.query(
115
+ `ALTER TABLE lattice_schema_migrations DROP CONSTRAINT ${constraintName}`
116
+ );
117
+ await client.query(
118
+ `ALTER TABLE lattice_schema_migrations ADD PRIMARY KEY (name)`
119
+ );
120
+ }
82
121
  }
83
122
 
84
123
  /**
85
- * Get applied migrations from database
124
+ * Get the set of already-applied migration names from the tracking table.
86
125
  */
87
- private async getAppliedMigrations(
88
- client: PoolClient
89
- ): Promise<MigrationRecord[]> {
90
- await client.query(`
91
- CREATE TABLE IF NOT EXISTS lattice_schema_migrations (
92
- version INTEGER PRIMARY KEY,
93
- name VARCHAR(255) NOT NULL,
94
- applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
95
- )
96
- `);
97
-
126
+ private async getAppliedNames(client: PoolClient): Promise<Set<string>> {
98
127
  const result = await client.query<MigrationRecord>(
99
- "SELECT version, name, applied_at FROM lattice_schema_migrations ORDER BY version"
128
+ "SELECT name, version, applied_at FROM lattice_schema_migrations ORDER BY version"
100
129
  );
101
- return result.rows;
130
+ return new Set(result.rows.map((r) => r.name));
102
131
  }
103
132
 
104
133
  /**
@@ -120,12 +149,11 @@ export class MigrationManager {
120
149
  await client.query("BEGIN");
121
150
 
122
151
  await this.ensureMigrationsTable(client);
123
- const appliedMigrations = await this.getAppliedMigrations(client);
124
- const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
152
+ const appliedNames = await this.getAppliedNames(client);
125
153
 
126
- // Find pending migrations
154
+ // Find pending migrations by name (not version)
127
155
  const pendingMigrations = this.migrations.filter(
128
- (m) => !appliedVersions.has(m.version)
156
+ (m) => !appliedNames.has(m.name)
129
157
  );
130
158
 
131
159
  if (pendingMigrations.length === 0) {
@@ -134,16 +162,15 @@ export class MigrationManager {
134
162
  return;
135
163
  }
136
164
 
137
- // Apply pending migrations
165
+ // Apply pending migrations (already sorted by version from register)
138
166
  for (const migration of pendingMigrations) {
139
167
  console.log(
140
- `Applying migration ${migration.version}: ${migration.name}`
168
+ `Applying migration v${migration.version}: ${migration.name}`
141
169
  );
142
170
  await migration.up(client);
143
- // Use ON CONFLICT DO NOTHING to handle race conditions gracefully
144
171
  await client.query(
145
- "INSERT INTO lattice_schema_migrations (version, name) VALUES ($1, $2) ON CONFLICT (version) DO NOTHING",
146
- [migration.version, migration.name]
172
+ "INSERT INTO lattice_schema_migrations (name, version) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING",
173
+ [migration.name, migration.version]
147
174
  );
148
175
  }
149
176
 
@@ -162,38 +189,42 @@ export class MigrationManager {
162
189
  }
163
190
 
164
191
  /**
165
- * Rollback last migration
192
+ * Rollback the last-applied migration (highest version).
166
193
  */
167
194
  async rollback(): Promise<void> {
168
195
  const client = await this.pool.connect();
169
196
  try {
170
197
  await client.query("BEGIN");
171
198
 
172
- const appliedMigrations = await this.getAppliedMigrations(client);
173
- if (appliedMigrations.length === 0) {
199
+ await this.ensureMigrationsTable(client);
200
+ const result = await client.query<MigrationRecord>(
201
+ "SELECT name, version FROM lattice_schema_migrations ORDER BY version DESC LIMIT 1"
202
+ );
203
+
204
+ if (result.rows.length === 0) {
174
205
  console.log("No migrations to rollback");
175
206
  await client.query("COMMIT");
176
207
  return;
177
208
  }
178
209
 
179
- const lastMigration = appliedMigrations[appliedMigrations.length - 1];
210
+ const lastApplied = result.rows[0];
180
211
  const migration = this.migrations.find(
181
- (m) => m.version === lastMigration.version
212
+ (m) => m.name === lastApplied.name
182
213
  );
183
214
 
184
215
  if (!migration || !migration.down) {
185
216
  throw new Error(
186
- `Migration ${lastMigration.version} does not have a down migration`
217
+ `Migration "${lastApplied.name}" (v${lastApplied.version}) does not have a down migration`
187
218
  );
188
219
  }
189
220
 
190
221
  console.log(
191
- `Rolling back migration ${lastMigration.version}: ${lastMigration.name}`
222
+ `Rolling back migration v${lastApplied.version}: ${lastApplied.name}`
192
223
  );
193
224
  await migration.down(client);
194
225
  await client.query(
195
- "DELETE FROM lattice_schema_migrations WHERE version = $1",
196
- [lastMigration.version]
226
+ "DELETE FROM lattice_schema_migrations WHERE name = $1",
227
+ [lastApplied.name]
197
228
  );
198
229
 
199
230
  await client.query("COMMIT");
@@ -207,16 +238,16 @@ export class MigrationManager {
207
238
  }
208
239
 
209
240
  /**
210
- * Get current migration version
241
+ * Get the highest applied migration version.
211
242
  */
212
243
  async getCurrentVersion(): Promise<number> {
213
244
  const client = await this.pool.connect();
214
245
  try {
215
- const appliedMigrations = await this.getAppliedMigrations(client);
216
- if (appliedMigrations.length === 0) {
217
- return 0;
218
- }
219
- return Math.max(...appliedMigrations.map((m) => m.version));
246
+ await this.ensureMigrationsTable(client);
247
+ const result = await client.query<{ max: number }>(
248
+ "SELECT COALESCE(MAX(version), 0) AS max FROM lattice_schema_migrations"
249
+ );
250
+ return result.rows[0]?.max ?? 0;
220
251
  } finally {
221
252
  client.release();
222
253
  }
@@ -9,7 +9,8 @@ import { MigrationManager } from "../migrations/migration";
9
9
  import { createChannelBindingsTable } from "../migrations/channel_bindings_migration";
10
10
 
11
11
  export interface ChannelBindingStoreOptions {
12
- poolConfig: string | PoolConfig;
12
+ pool?: Pool;
13
+ poolConfig?: string | PoolConfig;
13
14
  autoMigrate?: boolean;
14
15
  }
15
16
 
@@ -33,15 +34,26 @@ type BindingRow = {
33
34
 
34
35
  export class ChannelBindingStore implements BindingRegistry {
35
36
  private pool: Pool;
36
- private migrationManager: MigrationManager;
37
+ private migrationManager!: MigrationManager;
37
38
  private initialized = false;
39
+ private ownsPool: boolean = true;
38
40
  private initPromise: Promise<void> | null = null;
39
41
 
40
42
  constructor(options: ChannelBindingStoreOptions) {
43
+ // Use externally-managed pool if provided
44
+ if (options.pool) {
45
+ this.pool = options.pool;
46
+ this.ownsPool = false;
47
+ this.initialized = true;
48
+ return;
49
+ }
50
+
41
51
  this.pool =
42
52
  typeof options.poolConfig === "string"
43
53
  ? new Pool({ connectionString: options.poolConfig })
44
- : new Pool(options.poolConfig);
54
+ : options.poolConfig
55
+ ? new Pool(options.poolConfig as PoolConfig)
56
+ : (() => { throw new Error("Either pool or poolConfig must be provided"); })();
45
57
 
46
58
  this.migrationManager = new MigrationManager(this.pool);
47
59
  this.migrationManager.register(createChannelBindingsTable);
@@ -68,6 +80,12 @@ export class ChannelBindingStore implements BindingRegistry {
68
80
  return this.initPromise;
69
81
  }
70
82
 
83
+ async dispose(): Promise<void> {
84
+ if (this.ownsPool && this.pool) {
85
+ await this.pool.end();
86
+ }
87
+ }
88
+
71
89
  async resolve(params: {
72
90
  channel: string;
73
91
  senderId: string;
@@ -4,7 +4,8 @@ import { MigrationManager } from "../migrations/migration";
4
4
  import { createChannelIdentityMappingTables } from "../migrations/channel_identity_mapping_migration";
5
5
 
6
6
  export interface ChannelIdentityMappingStoreOptions {
7
- poolConfig: string | PoolConfig;
7
+ pool?: Pool;
8
+ poolConfig?: string | PoolConfig;
8
9
  autoMigrate?: boolean;
9
10
  }
10
11
 
@@ -60,15 +61,26 @@ export interface ChannelIdentityMapping {
60
61
 
61
62
  export class ChannelIdentityMappingStore {
62
63
  private pool: Pool;
63
- private migrationManager: MigrationManager;
64
+ private migrationManager!: MigrationManager;
64
65
  private initialized = false;
66
+ private ownsPool: boolean = true;
65
67
  private initPromise: Promise<void> | null = null;
66
68
 
67
69
  constructor(options: ChannelIdentityMappingStoreOptions) {
70
+ // Use externally-managed pool if provided
71
+ if (options.pool) {
72
+ this.pool = options.pool;
73
+ this.ownsPool = false;
74
+ this.initialized = true;
75
+ return;
76
+ }
77
+
68
78
  this.pool =
69
79
  typeof options.poolConfig === "string"
70
80
  ? new Pool({ connectionString: options.poolConfig })
71
- : new Pool(options.poolConfig);
81
+ : options.poolConfig
82
+ ? new Pool(options.poolConfig as PoolConfig)
83
+ : (() => { throw new Error("Either pool or poolConfig must be provided"); })();
72
84
 
73
85
  this.migrationManager = new MigrationManager(this.pool);
74
86
  this.migrationManager.register(createChannelIdentityMappingTables);
@@ -102,6 +114,12 @@ export class ChannelIdentityMappingStore {
102
114
  return this.initPromise;
103
115
  }
104
116
 
117
+ async dispose(): Promise<void> {
118
+ if (this.ownsPool && this.pool) {
119
+ await this.pool.end();
120
+ }
121
+ }
122
+
105
123
  async createMapping(
106
124
  input: CreateChannelIdentityMappingInput,
107
125
  ): Promise<ChannelIdentityMapping> {
@@ -12,7 +12,8 @@ import { createMenuItemsTable } from "../migrations/menu_items_migration";
12
12
  import { addFileContentType } from "../migrations/menu_items_add_file_type";
13
13
 
14
14
  export interface MenuStoreOptions {
15
- poolConfig: string | PoolConfig;
15
+ pool?: Pool;
16
+ poolConfig?: string | PoolConfig;
16
17
  autoMigrate?: boolean;
17
18
  }
18
19
 
@@ -33,15 +34,26 @@ type MenuItemRow = {
33
34
 
34
35
  export class MenuStore implements MenuRegistry {
35
36
  private pool: Pool;
36
- private migrationManager: MigrationManager;
37
+ private migrationManager!: MigrationManager;
37
38
  private initialized = false;
39
+ private ownsPool: boolean = true;
38
40
  private initPromise: Promise<void> | null = null;
39
41
 
40
42
  constructor(options: MenuStoreOptions) {
43
+ // Use externally-managed pool if provided
44
+ if (options.pool) {
45
+ this.pool = options.pool;
46
+ this.ownsPool = false;
47
+ this.initialized = true;
48
+ return;
49
+ }
50
+
41
51
  this.pool =
42
52
  typeof options.poolConfig === "string"
43
53
  ? new Pool({ connectionString: options.poolConfig })
44
- : new Pool(options.poolConfig);
54
+ : options.poolConfig
55
+ ? new Pool(options.poolConfig as PoolConfig)
56
+ : (() => { throw new Error("Either pool or poolConfig must be provided"); })();
45
57
 
46
58
  this.migrationManager = new MigrationManager(this.pool);
47
59
  this.migrationManager.register(createMenuItemsTable);
@@ -69,6 +81,12 @@ export class MenuStore implements MenuRegistry {
69
81
  return this.initPromise;
70
82
  }
71
83
 
84
+ async dispose(): Promise<void> {
85
+ if (this.ownsPool && this.pool) {
86
+ await this.pool.end();
87
+ }
88
+ }
89
+
72
90
  async list(params: { tenantId: string; menuTarget?: string }): Promise<MenuItem[]> {
73
91
  await this.ensureInitialized();
74
92
  const conditions: string[] = ["tenant_id = $1"];