@axiom-lattice/pg-stores 1.0.83 → 1.0.85

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 +19 -0
  3. package/dist/index.d.mts +124 -44
  4. package/dist/index.d.ts +124 -44
  5. package/dist/index.js +477 -162
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +477 -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 +100 -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.85",
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.94",
27
+ "@axiom-lattice/protocols": "2.1.48"
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,84 @@ 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
+ // Deduplicate names before changing PK. Under the old schema, the
115
+ // same migration name could be inserted at multiple versions by
116
+ // different MigrationManagers. Keep only the highest version.
117
+ await client.query(`
118
+ DELETE FROM lattice_schema_migrations a
119
+ USING lattice_schema_migrations b
120
+ WHERE a.name = b.name AND a.version < b.version
121
+ `);
122
+
123
+ await client.query(
124
+ `ALTER TABLE lattice_schema_migrations DROP CONSTRAINT ${constraintName}`
125
+ );
126
+ await client.query(
127
+ `ALTER TABLE lattice_schema_migrations ADD PRIMARY KEY (name)`
128
+ );
129
+ }
82
130
  }
83
131
 
84
132
  /**
85
- * Get applied migrations from database
133
+ * Get the set of already-applied migration names from the tracking table.
86
134
  */
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
-
135
+ private async getAppliedNames(client: PoolClient): Promise<Set<string>> {
98
136
  const result = await client.query<MigrationRecord>(
99
- "SELECT version, name, applied_at FROM lattice_schema_migrations ORDER BY version"
137
+ "SELECT name, version, applied_at FROM lattice_schema_migrations ORDER BY version"
100
138
  );
101
- return result.rows;
139
+ return new Set(result.rows.map((r) => r.name));
102
140
  }
103
141
 
104
142
  /**
@@ -120,12 +158,11 @@ export class MigrationManager {
120
158
  await client.query("BEGIN");
121
159
 
122
160
  await this.ensureMigrationsTable(client);
123
- const appliedMigrations = await this.getAppliedMigrations(client);
124
- const appliedVersions = new Set(appliedMigrations.map((m) => m.version));
161
+ const appliedNames = await this.getAppliedNames(client);
125
162
 
126
- // Find pending migrations
163
+ // Find pending migrations by name (not version)
127
164
  const pendingMigrations = this.migrations.filter(
128
- (m) => !appliedVersions.has(m.version)
165
+ (m) => !appliedNames.has(m.name)
129
166
  );
130
167
 
131
168
  if (pendingMigrations.length === 0) {
@@ -134,16 +171,15 @@ export class MigrationManager {
134
171
  return;
135
172
  }
136
173
 
137
- // Apply pending migrations
174
+ // Apply pending migrations (already sorted by version from register)
138
175
  for (const migration of pendingMigrations) {
139
176
  console.log(
140
- `Applying migration ${migration.version}: ${migration.name}`
177
+ `Applying migration v${migration.version}: ${migration.name}`
141
178
  );
142
179
  await migration.up(client);
143
- // Use ON CONFLICT DO NOTHING to handle race conditions gracefully
144
180
  await client.query(
145
- "INSERT INTO lattice_schema_migrations (version, name) VALUES ($1, $2) ON CONFLICT (version) DO NOTHING",
146
- [migration.version, migration.name]
181
+ "INSERT INTO lattice_schema_migrations (name, version) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING",
182
+ [migration.name, migration.version]
147
183
  );
148
184
  }
149
185
 
@@ -162,38 +198,42 @@ export class MigrationManager {
162
198
  }
163
199
 
164
200
  /**
165
- * Rollback last migration
201
+ * Rollback the last-applied migration (highest version).
166
202
  */
167
203
  async rollback(): Promise<void> {
168
204
  const client = await this.pool.connect();
169
205
  try {
170
206
  await client.query("BEGIN");
171
207
 
172
- const appliedMigrations = await this.getAppliedMigrations(client);
173
- if (appliedMigrations.length === 0) {
208
+ await this.ensureMigrationsTable(client);
209
+ const result = await client.query<MigrationRecord>(
210
+ "SELECT name, version FROM lattice_schema_migrations ORDER BY version DESC LIMIT 1"
211
+ );
212
+
213
+ if (result.rows.length === 0) {
174
214
  console.log("No migrations to rollback");
175
215
  await client.query("COMMIT");
176
216
  return;
177
217
  }
178
218
 
179
- const lastMigration = appliedMigrations[appliedMigrations.length - 1];
219
+ const lastApplied = result.rows[0];
180
220
  const migration = this.migrations.find(
181
- (m) => m.version === lastMigration.version
221
+ (m) => m.name === lastApplied.name
182
222
  );
183
223
 
184
224
  if (!migration || !migration.down) {
185
225
  throw new Error(
186
- `Migration ${lastMigration.version} does not have a down migration`
226
+ `Migration "${lastApplied.name}" (v${lastApplied.version}) does not have a down migration`
187
227
  );
188
228
  }
189
229
 
190
230
  console.log(
191
- `Rolling back migration ${lastMigration.version}: ${lastMigration.name}`
231
+ `Rolling back migration v${lastApplied.version}: ${lastApplied.name}`
192
232
  );
193
233
  await migration.down(client);
194
234
  await client.query(
195
- "DELETE FROM lattice_schema_migrations WHERE version = $1",
196
- [lastMigration.version]
235
+ "DELETE FROM lattice_schema_migrations WHERE name = $1",
236
+ [lastApplied.name]
197
237
  );
198
238
 
199
239
  await client.query("COMMIT");
@@ -207,16 +247,16 @@ export class MigrationManager {
207
247
  }
208
248
 
209
249
  /**
210
- * Get current migration version
250
+ * Get the highest applied migration version.
211
251
  */
212
252
  async getCurrentVersion(): Promise<number> {
213
253
  const client = await this.pool.connect();
214
254
  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));
255
+ await this.ensureMigrationsTable(client);
256
+ const result = await client.query<{ max: number }>(
257
+ "SELECT COALESCE(MAX(version), 0) AS max FROM lattice_schema_migrations"
258
+ );
259
+ return result.rows[0]?.max ?? 0;
220
260
  } finally {
221
261
  client.release();
222
262
  }
@@ -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"];