@axiom-lattice/pg-stores 3.3.3 → 3.3.4
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 +37 -2
- package/dist/index.d.ts +37 -2
- package/dist/index.js +206 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +203 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/PostgreSQLPluginDeploymentStore.test.ts +139 -0
- package/src/createPgStoreConfig.ts +8 -0
- package/src/index.ts +2 -0
- package/src/migrations/plugin_deployment_migration.ts +67 -0
- package/src/stores/PostgreSQLPluginDeploymentStore.ts +189 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/pg-stores",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.4",
|
|
4
4
|
"description": "PG stores implementation for Axiom Lattice framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@langchain/core": "1.1.30",
|
|
26
26
|
"pg": "^8.16.3",
|
|
27
27
|
"uuid": "^9.0.1",
|
|
28
|
-
"@axiom-lattice/core": "6.
|
|
29
|
-
"@axiom-lattice/protocols": "4.
|
|
28
|
+
"@axiom-lattice/core": "6.4.0",
|
|
29
|
+
"@axiom-lattice/protocols": "4.8.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/jest": "^29.5.14",
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, it } from "@jest/globals";
|
|
3
|
+
import { Pool } from "pg";
|
|
4
|
+
import type { PluginDeployment } from "@axiom-lattice/protocols";
|
|
5
|
+
|
|
6
|
+
import { MigrationManager } from "../migrations/migration";
|
|
7
|
+
import {
|
|
8
|
+
createPluginDeployments,
|
|
9
|
+
dropPluginDeploymentAuthToken,
|
|
10
|
+
} from "../migrations/plugin_deployment_migration";
|
|
11
|
+
import { PostgreSQLPluginDeploymentStore } from "../stores/PostgreSQLPluginDeploymentStore";
|
|
12
|
+
|
|
13
|
+
const databaseUrl = process.env.TEST_DATABASE_URL;
|
|
14
|
+
const describeIfDatabase = databaseUrl ? describe : describe.skip;
|
|
15
|
+
|
|
16
|
+
function record(
|
|
17
|
+
tenantId: string,
|
|
18
|
+
pluginType: string,
|
|
19
|
+
overrides: Partial<PluginDeployment> = {},
|
|
20
|
+
): PluginDeployment {
|
|
21
|
+
const now = new Date().toISOString();
|
|
22
|
+
return {
|
|
23
|
+
id: randomUUID(),
|
|
24
|
+
tenantId,
|
|
25
|
+
pluginType,
|
|
26
|
+
pluginVersion: "1.0.0",
|
|
27
|
+
mode: "onDemand",
|
|
28
|
+
status: "running",
|
|
29
|
+
provider: "microsandbox-remote",
|
|
30
|
+
sandboxName: `plugin-${pluginType}`,
|
|
31
|
+
port: 8080,
|
|
32
|
+
idleTimeoutMs: 600_000,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
updatedAt: now,
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describeIfDatabase("PostgreSQLPluginDeploymentStore", () => {
|
|
40
|
+
let pool: Pool;
|
|
41
|
+
let store: PostgreSQLPluginDeploymentStore;
|
|
42
|
+
const tenantId = `plugin-deploy-${randomUUID()}`;
|
|
43
|
+
const otherTenantId = `plugin-deploy-${randomUUID()}`;
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
pool = new Pool({ connectionString: databaseUrl });
|
|
47
|
+
const mm = new MigrationManager(pool);
|
|
48
|
+
mm.register(createPluginDeployments);
|
|
49
|
+
mm.register(dropPluginDeploymentAuthToken);
|
|
50
|
+
await mm.migrate();
|
|
51
|
+
store = new PostgreSQLPluginDeploymentStore({ pool });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(async () => {
|
|
55
|
+
await pool.query("DELETE FROM lattice_plugin_deployments WHERE tenant_id = ANY($1)", [
|
|
56
|
+
[tenantId, otherTenantId],
|
|
57
|
+
]);
|
|
58
|
+
await pool.end();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("upserts and gets a deployment", async () => {
|
|
62
|
+
const saved = await store.upsert(record(tenantId, "weather"));
|
|
63
|
+
expect(saved.pluginType).toBe("weather");
|
|
64
|
+
|
|
65
|
+
const got = await store.get(tenantId, "weather");
|
|
66
|
+
expect(got?.pluginType).toBe("weather");
|
|
67
|
+
expect(await store.get(tenantId, "missing")).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("preserves id and createdAt on conflict while applying mutable fields", async () => {
|
|
71
|
+
const first = await store.upsert(record(tenantId, "conflict", { status: "requested" }));
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
73
|
+
const second = await store.upsert({
|
|
74
|
+
...record(tenantId, "conflict"),
|
|
75
|
+
id: randomUUID(),
|
|
76
|
+
status: "running",
|
|
77
|
+
pluginVersion: "2.0.0",
|
|
78
|
+
});
|
|
79
|
+
expect(second.id).toBe(first.id);
|
|
80
|
+
expect(second.createdAt).toBe(first.createdAt);
|
|
81
|
+
expect(second.status).toBe("running");
|
|
82
|
+
expect(second.pluginVersion).toBe("2.0.0");
|
|
83
|
+
expect(second.updatedAt).not.toBe(first.updatedAt);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("maps NULL optional columns to undefined", async () => {
|
|
87
|
+
await store.upsert(record(tenantId, "nulls"));
|
|
88
|
+
const got = await store.get(tenantId, "nulls");
|
|
89
|
+
expect(got?.endpoint).toBeUndefined();
|
|
90
|
+
expect(got?.imageRef).toBeUndefined();
|
|
91
|
+
expect(got?.lastHealthAt).toBeUndefined();
|
|
92
|
+
expect(got?.lastError).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("patches allowed fields and returns undefined for missing rows", async () => {
|
|
96
|
+
await store.upsert(record(tenantId, "patch"));
|
|
97
|
+
const updated = await store.updateStatus(tenantId, "patch", {
|
|
98
|
+
status: "paused",
|
|
99
|
+
mode: "alwaysOn",
|
|
100
|
+
endpoint: "srv:1",
|
|
101
|
+
imageRef: "img:1",
|
|
102
|
+
lastHealthAt: "2026-01-02T00:00:00.000Z",
|
|
103
|
+
lastError: "boom",
|
|
104
|
+
pluginVersion: "1.2.0",
|
|
105
|
+
provider: "opensandbox",
|
|
106
|
+
sandboxName: "plugin-patch-2",
|
|
107
|
+
port: 9090,
|
|
108
|
+
idleTimeoutMs: 1000,
|
|
109
|
+
});
|
|
110
|
+
expect(updated?.status).toBe("paused");
|
|
111
|
+
expect(updated?.mode).toBe("alwaysOn");
|
|
112
|
+
expect(updated?.endpoint).toBe("srv:1");
|
|
113
|
+
expect(updated?.imageRef).toBe("img:1");
|
|
114
|
+
expect(updated?.lastHealthAt).toBe("2026-01-02T00:00:00.000Z");
|
|
115
|
+
expect(updated?.lastError).toBe("boom");
|
|
116
|
+
expect(updated?.pluginVersion).toBe("1.2.0");
|
|
117
|
+
expect(updated?.provider).toBe("opensandbox");
|
|
118
|
+
expect(updated?.sandboxName).toBe("plugin-patch-2");
|
|
119
|
+
expect(updated?.port).toBe(9090);
|
|
120
|
+
expect(updated?.idleTimeoutMs).toBe(1000);
|
|
121
|
+
|
|
122
|
+
expect(await store.updateStatus(tenantId, "nope", { status: "paused" })).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("isolates tenants, lists ordered by plugin type, and deletes", async () => {
|
|
126
|
+
await store.upsert(record(tenantId, "list-b"));
|
|
127
|
+
await store.upsert(record(tenantId, "list-a"));
|
|
128
|
+
await store.upsert(record(otherTenantId, "list-a"));
|
|
129
|
+
|
|
130
|
+
const listed = (await store.listByTenant(tenantId)).filter((entry) =>
|
|
131
|
+
entry.pluginType.startsWith("list-"),
|
|
132
|
+
);
|
|
133
|
+
expect(listed.map((entry) => entry.pluginType)).toEqual(["list-a", "list-b"]);
|
|
134
|
+
expect((await store.listByTenant(otherTenantId)).every((entry) => entry.tenantId === otherTenantId)).toBe(true);
|
|
135
|
+
|
|
136
|
+
expect(await store.delete(tenantId, "list-a")).toBe(true);
|
|
137
|
+
expect(await store.delete(tenantId, "list-a")).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -87,6 +87,10 @@ import { addProjectRoomMessageRetentionIndex } from "./migrations/project_room_m
|
|
|
87
87
|
import { createUserPushSubscriptions } from "./migrations/user_push_subscription_migration";
|
|
88
88
|
import { createDistributedLeases } from "./migrations/distributed_lease_migration";
|
|
89
89
|
import { createOutboxTable } from "./migrations/outbox_migration";
|
|
90
|
+
import {
|
|
91
|
+
createPluginDeployments,
|
|
92
|
+
dropPluginDeploymentAuthToken,
|
|
93
|
+
} from "./migrations/plugin_deployment_migration";
|
|
90
94
|
import {
|
|
91
95
|
createModelProvidersTable,
|
|
92
96
|
createModelProviderModelsTable,
|
|
@@ -103,6 +107,7 @@ import { PostgreSQLUserPushSubscriptionStore } from "./stores/PostgreSQLUserPush
|
|
|
103
107
|
import { PostgreSQLModelProviderStore } from "./stores/PostgreSQLModelProviderStore";
|
|
104
108
|
import { PostgreSQLLeaseStore } from "./stores/PostgreSQLLeaseStore";
|
|
105
109
|
import { PostgreSQLOutboxStore } from "./stores/PostgreSQLOutboxStore";
|
|
110
|
+
import { PostgreSQLPluginDeploymentStore } from "./stores/PostgreSQLPluginDeploymentStore";
|
|
106
111
|
|
|
107
112
|
export async function createPgStoreConfig(connectionString: string) {
|
|
108
113
|
const pool = new Pool({ connectionString });
|
|
@@ -179,6 +184,8 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
179
184
|
mm.register(createDistributedLeases); // v185
|
|
180
185
|
mm.register(createOutboxTable); // v187
|
|
181
186
|
mm.register(addConnectionSecretFields); // v188
|
|
187
|
+
mm.register(createPluginDeployments); // v189
|
|
188
|
+
mm.register(dropPluginDeploymentAuthToken); // v190
|
|
182
189
|
|
|
183
190
|
await mm.migrate();
|
|
184
191
|
|
|
@@ -227,6 +234,7 @@ export async function createPgStoreConfig(connectionString: string) {
|
|
|
227
234
|
userPushSubscription: new PostgreSQLUserPushSubscriptionStore(opts),
|
|
228
235
|
lease: new PostgreSQLLeaseStore(opts),
|
|
229
236
|
outbox: new PostgreSQLOutboxStore(opts),
|
|
237
|
+
pluginDeployment: new PostgreSQLPluginDeploymentStore(opts),
|
|
230
238
|
vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
|
|
231
239
|
checkpoint,
|
|
232
240
|
};
|
package/src/index.ts
CHANGED
|
@@ -80,6 +80,8 @@ export * from "./stores/PostgreSQLProjectBotMembershipStore";
|
|
|
80
80
|
export * from "./stores/PostgreSQLProjectRoomMessageStore";
|
|
81
81
|
export * from "./stores/PostgreSQLProjectRoomReadStateStore";
|
|
82
82
|
export * from "./migrations/project_room_migration";
|
|
83
|
+
export * from "./stores/PostgreSQLPluginDeploymentStore";
|
|
84
|
+
export * from "./migrations/plugin_deployment_migration";
|
|
83
85
|
|
|
84
86
|
export * from "./PGVectorStoreProvider";
|
|
85
87
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Migration } from "./migration";
|
|
2
|
+
|
|
3
|
+
/** Creates the durable plugin-deployment table. */
|
|
4
|
+
export const createPluginDeployments: Migration = {
|
|
5
|
+
version: 189,
|
|
6
|
+
name: "create_plugin_deployments",
|
|
7
|
+
up: async (client) => {
|
|
8
|
+
await client.query(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS lattice_plugin_deployments (
|
|
10
|
+
id VARCHAR(255) PRIMARY KEY,
|
|
11
|
+
tenant_id VARCHAR(255) NOT NULL,
|
|
12
|
+
plugin_type VARCHAR(255) NOT NULL,
|
|
13
|
+
plugin_version VARCHAR(255) NOT NULL,
|
|
14
|
+
mode VARCHAR(16) NOT NULL,
|
|
15
|
+
status VARCHAR(32) NOT NULL,
|
|
16
|
+
provider VARCHAR(64) NOT NULL,
|
|
17
|
+
sandbox_name VARCHAR(255) NOT NULL,
|
|
18
|
+
port INTEGER NOT NULL,
|
|
19
|
+
endpoint TEXT,
|
|
20
|
+
image_ref TEXT,
|
|
21
|
+
auth_token TEXT NOT NULL,
|
|
22
|
+
idle_timeout_ms INTEGER NOT NULL,
|
|
23
|
+
last_health_at TIMESTAMPTZ,
|
|
24
|
+
last_error TEXT,
|
|
25
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
26
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
27
|
+
UNIQUE (tenant_id, plugin_type)
|
|
28
|
+
)
|
|
29
|
+
`);
|
|
30
|
+
await client.query(`
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_lattice_plugin_deployments_tenant
|
|
32
|
+
ON lattice_plugin_deployments (tenant_id)
|
|
33
|
+
`);
|
|
34
|
+
},
|
|
35
|
+
down: async (client) => {
|
|
36
|
+
await client.query("DROP INDEX IF EXISTS idx_lattice_plugin_deployments_tenant");
|
|
37
|
+
await client.query("DROP TABLE IF EXISTS lattice_plugin_deployments");
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Drops the now-unused `auth_token` column introduced by
|
|
43
|
+
* {@link createPluginDeployments}.
|
|
44
|
+
*
|
|
45
|
+
* Resident plugin hosts no longer receive a per-deployment bearer token; the
|
|
46
|
+
* sandbox service key plus loopback transport replace it. This is a separate
|
|
47
|
+
* migration (rather than editing v189 in place) because migration identity is
|
|
48
|
+
* the name: databases that already applied the released v189 keep its
|
|
49
|
+
* `auth_token NOT NULL` column, so the token removal must be applied as a new
|
|
50
|
+
* forward migration.
|
|
51
|
+
*/
|
|
52
|
+
export const dropPluginDeploymentAuthToken: Migration = {
|
|
53
|
+
version: 190,
|
|
54
|
+
name: "drop_plugin_deployment_auth_token",
|
|
55
|
+
up: async (client) => {
|
|
56
|
+
await client.query(`
|
|
57
|
+
ALTER TABLE lattice_plugin_deployments
|
|
58
|
+
DROP COLUMN IF EXISTS auth_token
|
|
59
|
+
`);
|
|
60
|
+
},
|
|
61
|
+
down: async (client) => {
|
|
62
|
+
await client.query(`
|
|
63
|
+
ALTER TABLE lattice_plugin_deployments
|
|
64
|
+
ADD COLUMN IF NOT EXISTS auth_token TEXT NOT NULL DEFAULT ''
|
|
65
|
+
`);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PluginDeployment,
|
|
3
|
+
PluginDeploymentStatus,
|
|
4
|
+
PluginDeploymentStore,
|
|
5
|
+
} from "@axiom-lattice/protocols";
|
|
6
|
+
import type { Pool } from "pg";
|
|
7
|
+
|
|
8
|
+
type Row = {
|
|
9
|
+
id: unknown; tenant_id: unknown; plugin_type: unknown; plugin_version: unknown;
|
|
10
|
+
mode: unknown; status: unknown; provider: unknown; sandbox_name: unknown;
|
|
11
|
+
port: unknown; endpoint: unknown; image_ref: unknown;
|
|
12
|
+
idle_timeout_ms: unknown; last_health_at: unknown; last_error: unknown;
|
|
13
|
+
created_at: unknown; updated_at: unknown;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const columns =
|
|
17
|
+
"id, tenant_id, plugin_type, plugin_version, mode, status, provider, sandbox_name, " +
|
|
18
|
+
"port, endpoint, image_ref, idle_timeout_ms, last_health_at, last_error, " +
|
|
19
|
+
"created_at, updated_at";
|
|
20
|
+
|
|
21
|
+
const statuses: readonly PluginDeploymentStatus[] = [
|
|
22
|
+
"requested",
|
|
23
|
+
"provisioning",
|
|
24
|
+
"running",
|
|
25
|
+
"paused",
|
|
26
|
+
"stopping",
|
|
27
|
+
"stopped",
|
|
28
|
+
"failed",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function isStatus(value: unknown): value is PluginDeploymentStatus {
|
|
32
|
+
return typeof value === "string" && (statuses as readonly string[]).includes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mapRow(row: Row): PluginDeployment {
|
|
36
|
+
if (
|
|
37
|
+
typeof row.id !== "string" ||
|
|
38
|
+
typeof row.tenant_id !== "string" ||
|
|
39
|
+
typeof row.plugin_type !== "string" ||
|
|
40
|
+
typeof row.plugin_version !== "string" ||
|
|
41
|
+
(row.mode !== "onDemand" && row.mode !== "alwaysOn") ||
|
|
42
|
+
!isStatus(row.status) ||
|
|
43
|
+
typeof row.provider !== "string" ||
|
|
44
|
+
typeof row.sandbox_name !== "string" ||
|
|
45
|
+
typeof row.port !== "number" ||
|
|
46
|
+
typeof row.idle_timeout_ms !== "number" ||
|
|
47
|
+
!(row.created_at instanceof Date) || Number.isNaN(row.created_at.getTime()) ||
|
|
48
|
+
!(row.updated_at instanceof Date) || Number.isNaN(row.updated_at.getTime())
|
|
49
|
+
) {
|
|
50
|
+
throw new Error("Invalid plugin deployment row");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
id: row.id,
|
|
54
|
+
tenantId: row.tenant_id,
|
|
55
|
+
pluginType: row.plugin_type,
|
|
56
|
+
pluginVersion: row.plugin_version,
|
|
57
|
+
mode: row.mode,
|
|
58
|
+
status: row.status,
|
|
59
|
+
provider: row.provider,
|
|
60
|
+
sandboxName: row.sandbox_name,
|
|
61
|
+
port: row.port,
|
|
62
|
+
...(typeof row.endpoint === "string" ? { endpoint: row.endpoint } : {}),
|
|
63
|
+
...(typeof row.image_ref === "string" ? { imageRef: row.image_ref } : {}),
|
|
64
|
+
idleTimeoutMs: row.idle_timeout_ms,
|
|
65
|
+
...(row.last_health_at instanceof Date && !Number.isNaN(row.last_health_at.getTime())
|
|
66
|
+
? { lastHealthAt: row.last_health_at.toISOString() }
|
|
67
|
+
: {}),
|
|
68
|
+
...(typeof row.last_error === "string" ? { lastError: row.last_error } : {}),
|
|
69
|
+
createdAt: row.created_at.toISOString(),
|
|
70
|
+
updatedAt: row.updated_at.toISOString(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Persists tenant-scoped plugin deployments in PostgreSQL. */
|
|
75
|
+
export class PostgreSQLPluginDeploymentStore implements PluginDeploymentStore {
|
|
76
|
+
/** Creates a store using an externally managed shared pool. */
|
|
77
|
+
constructor(options: { pool: Pool }) {
|
|
78
|
+
this.pool = options.pool;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private readonly pool: Pool;
|
|
82
|
+
|
|
83
|
+
/** Lists every deployment for a tenant, ordered by plugin type. */
|
|
84
|
+
async listByTenant(tenantId: string): Promise<PluginDeployment[]> {
|
|
85
|
+
const result = await this.pool.query<Row>(
|
|
86
|
+
`SELECT ${columns} FROM lattice_plugin_deployments WHERE tenant_id = $1 ORDER BY plugin_type`,
|
|
87
|
+
[tenantId],
|
|
88
|
+
);
|
|
89
|
+
return result.rows.map(mapRow);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Returns the deployment for a tenant + plugin type, or undefined. */
|
|
93
|
+
async get(tenantId: string, pluginType: string): Promise<PluginDeployment | undefined> {
|
|
94
|
+
const result = await this.pool.query<Row>(
|
|
95
|
+
`SELECT ${columns} FROM lattice_plugin_deployments WHERE tenant_id = $1 AND plugin_type = $2`,
|
|
96
|
+
[tenantId, pluginType],
|
|
97
|
+
);
|
|
98
|
+
return result.rows[0] ? mapRow(result.rows[0]) : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Upserts a deployment keyed by tenant + plugin type, preserving the original id and createdAt. */
|
|
102
|
+
async upsert(deployment: PluginDeployment): Promise<PluginDeployment> {
|
|
103
|
+
const result = await this.pool.query<Row>(
|
|
104
|
+
`INSERT INTO lattice_plugin_deployments (${columns})
|
|
105
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
|
106
|
+
ON CONFLICT (tenant_id, plugin_type) DO UPDATE SET
|
|
107
|
+
plugin_version = EXCLUDED.plugin_version,
|
|
108
|
+
mode = EXCLUDED.mode,
|
|
109
|
+
status = EXCLUDED.status,
|
|
110
|
+
provider = EXCLUDED.provider,
|
|
111
|
+
sandbox_name = EXCLUDED.sandbox_name,
|
|
112
|
+
port = EXCLUDED.port,
|
|
113
|
+
endpoint = EXCLUDED.endpoint,
|
|
114
|
+
image_ref = EXCLUDED.image_ref,
|
|
115
|
+
idle_timeout_ms = EXCLUDED.idle_timeout_ms,
|
|
116
|
+
last_health_at = EXCLUDED.last_health_at,
|
|
117
|
+
last_error = EXCLUDED.last_error,
|
|
118
|
+
updated_at = NOW()
|
|
119
|
+
RETURNING ${columns}`,
|
|
120
|
+
[
|
|
121
|
+
deployment.id,
|
|
122
|
+
deployment.tenantId,
|
|
123
|
+
deployment.pluginType,
|
|
124
|
+
deployment.pluginVersion,
|
|
125
|
+
deployment.mode,
|
|
126
|
+
deployment.status,
|
|
127
|
+
deployment.provider,
|
|
128
|
+
deployment.sandboxName,
|
|
129
|
+
deployment.port,
|
|
130
|
+
deployment.endpoint ?? null,
|
|
131
|
+
deployment.imageRef ?? null,
|
|
132
|
+
deployment.idleTimeoutMs,
|
|
133
|
+
deployment.lastHealthAt ? new Date(deployment.lastHealthAt) : null,
|
|
134
|
+
deployment.lastError ?? null,
|
|
135
|
+
new Date(deployment.createdAt),
|
|
136
|
+
new Date(deployment.updatedAt),
|
|
137
|
+
],
|
|
138
|
+
);
|
|
139
|
+
return mapRow(result.rows[0]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Applies a partial update to mutable columns; returns undefined when no row exists. */
|
|
143
|
+
async updateStatus(
|
|
144
|
+
tenantId: string,
|
|
145
|
+
pluginType: string,
|
|
146
|
+
patch: Partial<Omit<PluginDeployment, "id" | "tenantId" | "pluginType">>,
|
|
147
|
+
): Promise<PluginDeployment | undefined> {
|
|
148
|
+
const assignments: string[] = [];
|
|
149
|
+
const params: unknown[] = [];
|
|
150
|
+
const assign = (column: string, value: unknown): void => {
|
|
151
|
+
params.push(value);
|
|
152
|
+
assignments.push(`${column} = $${params.length}`);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (patch.status !== undefined) assign("status", patch.status);
|
|
156
|
+
if (patch.mode !== undefined) assign("mode", patch.mode);
|
|
157
|
+
if (patch.endpoint !== undefined) assign("endpoint", patch.endpoint);
|
|
158
|
+
if (patch.imageRef !== undefined) assign("image_ref", patch.imageRef);
|
|
159
|
+
if (patch.idleTimeoutMs !== undefined) assign("idle_timeout_ms", patch.idleTimeoutMs);
|
|
160
|
+
if (patch.lastHealthAt !== undefined) assign("last_health_at", new Date(patch.lastHealthAt));
|
|
161
|
+
if (patch.lastError !== undefined) assign("last_error", patch.lastError);
|
|
162
|
+
if (patch.pluginVersion !== undefined) assign("plugin_version", patch.pluginVersion);
|
|
163
|
+
if (patch.provider !== undefined) assign("provider", patch.provider);
|
|
164
|
+
if (patch.sandboxName !== undefined) assign("sandbox_name", patch.sandboxName);
|
|
165
|
+
if (patch.port !== undefined) assign("port", patch.port);
|
|
166
|
+
assignments.push("updated_at = NOW()");
|
|
167
|
+
|
|
168
|
+
params.push(tenantId, pluginType);
|
|
169
|
+
const tenantParam = params.length - 1;
|
|
170
|
+
const pluginParam = params.length;
|
|
171
|
+
|
|
172
|
+
const result = await this.pool.query<Row>(
|
|
173
|
+
`UPDATE lattice_plugin_deployments SET ${assignments.join(", ")}
|
|
174
|
+
WHERE tenant_id = $${tenantParam} AND plugin_type = $${pluginParam}
|
|
175
|
+
RETURNING ${columns}`,
|
|
176
|
+
params,
|
|
177
|
+
);
|
|
178
|
+
return result.rows[0] ? mapRow(result.rows[0]) : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Deletes a deployment by tenant + plugin type; returns whether a row was removed. */
|
|
182
|
+
async delete(tenantId: string, pluginType: string): Promise<boolean> {
|
|
183
|
+
const result = await this.pool.query(
|
|
184
|
+
`DELETE FROM lattice_plugin_deployments WHERE tenant_id = $1 AND plugin_type = $2`,
|
|
185
|
+
[tenantId, pluginType],
|
|
186
|
+
);
|
|
187
|
+
return (result.rowCount ?? 0) > 0;
|
|
188
|
+
}
|
|
189
|
+
}
|