@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.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +19 -0
- package/dist/index.d.mts +124 -44
- package/dist/index.d.ts +124 -44
- package/dist/index.js +477 -162
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +477 -162
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/scripts/check-migration-versions.mjs +42 -9
- package/src/createPgStoreConfig.ts +97 -3
- package/src/migrations/migration.ts +100 -60
- package/src/stores/ChannelBindingStore.ts +21 -3
- package/src/stores/ChannelIdentityMappingStore.ts +21 -3
- package/src/stores/MenuStore.ts +21 -3
- package/src/stores/PostgreSQLA2AApiKeyStore.ts +21 -3
- package/src/stores/PostgreSQLAssistantStore.ts +19 -3
- package/src/stores/PostgreSQLChannelInstallationStore.ts +21 -3
- package/src/stores/PostgreSQLDatabaseConfigStore.ts +23 -4
- package/src/stores/PostgreSQLEvalStore.ts +15 -3
- package/src/stores/PostgreSQLMcpServerConfigStore.ts +23 -4
- package/src/stores/PostgreSQLMetricsServerConfigStore.ts +23 -4
- package/src/stores/PostgreSQLProjectStore.ts +19 -3
- package/src/stores/PostgreSQLScheduleStorage.ts +21 -4
- package/src/stores/PostgreSQLSkillStore.ts +14 -3
- package/src/stores/PostgreSQLTaskStore.ts +14 -3
- package/src/stores/PostgreSQLTenantStore.ts +19 -3
- package/src/stores/PostgreSQLThreadStore.ts +19 -3
- package/src/stores/PostgreSQLUserStore.ts +18 -4
- package/src/stores/PostgreSQLUserTenantLinkStore.ts +18 -4
- package/src/stores/PostgreSQLWorkflowTrackingStore.ts +18 -4
- package/src/stores/PostgreSQLWorkspaceStore.ts +19 -3
- package/src/stores/PostgresSharedResourceStore.ts +21 -3
- 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.
|
|
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.
|
|
27
|
-
"@axiom-lattice/protocols": "2.1.
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
* ##
|
|
4
|
+
* ## Identity vs Ordering
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
* -
|
|
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
|
-
*
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
133
|
+
* Get the set of already-applied migration names from the tracking table.
|
|
86
134
|
*/
|
|
87
|
-
private async
|
|
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
|
|
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
|
|
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) => !
|
|
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 (
|
|
146
|
-
[migration.
|
|
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
|
-
|
|
173
|
-
|
|
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
|
|
219
|
+
const lastApplied = result.rows[0];
|
|
180
220
|
const migration = this.migrations.find(
|
|
181
|
-
(m) => m.
|
|
221
|
+
(m) => m.name === lastApplied.name
|
|
182
222
|
);
|
|
183
223
|
|
|
184
224
|
if (!migration || !migration.down) {
|
|
185
225
|
throw new Error(
|
|
186
|
-
`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 ${
|
|
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
|
|
196
|
-
[
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
:
|
|
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> {
|
package/src/stores/MenuStore.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
:
|
|
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"];
|