@axiom-lattice/pg-stores 3.1.10 → 3.1.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/pg-stores",
3
- "version": "3.1.10",
3
+ "version": "3.1.11",
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": "4.2.10",
29
- "@axiom-lattice/protocols": "4.2.1"
28
+ "@axiom-lattice/core": "4.2.11",
29
+ "@axiom-lattice/protocols": "4.2.2"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/jest": "^29.5.14",
@@ -82,6 +82,19 @@ describe("PostgreSQLTaskWorkItemStore", () => {
82
82
  expect(result[0].eventKey).toBeUndefined();
83
83
  });
84
84
 
85
+ it("maps null optional columns to undefined for strict projection snapshots", async () => {
86
+ const query = jest.fn().mockResolvedValue({ rows: [row] });
87
+ const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
88
+
89
+ const result = await store.list({ tenantId: "tenant-1", taskId: "task-1" });
90
+
91
+ expect(result[0].threadId).toBeUndefined();
92
+ expect(result[0].attempt).toBeUndefined();
93
+ expect(result[0].workspaceId).toBeUndefined();
94
+ expect(result[0].projectId).toBeUndefined();
95
+ expect(result[0].detail).toBeUndefined();
96
+ });
97
+
85
98
  it.each([null, [], "legacy", 1] as const)("maps non-object detail %p to undefined", async (detail) => {
86
99
  const query = jest.fn().mockResolvedValue({ rows: [{ ...row, detail }] });
87
100
  const store = new PostgreSQLTaskWorkItemStore({ query } as unknown as Pool);
@@ -87,12 +87,14 @@ import { createAgentWebAppsTable } from "./migrations/agent_web_apps_migration";
87
87
  import { addAgentWebAppsIdentity } from "./migrations/add_agent_web_apps_identity_migration";
88
88
  import { createCapabilityBundlesTable } from "./migrations/capability_bundle_migration";
89
89
  import { createProjectRoomReadStates, createProjectRoomTables } from "./migrations/project_room_migration";
90
+ import { createUserPushSubscriptions } from "./migrations/user_push_subscription_migration";
90
91
  import { PostgreSQLCapabilityBundleStore } from "./stores/PostgreSQLCapabilityBundleStore";
91
92
  import { PostgreSQLProjectRoomStore } from "./stores/PostgreSQLProjectRoomStore";
92
93
  import { PostgreSQLProjectMembershipStore } from "./stores/PostgreSQLProjectMembershipStore";
93
94
  import { PostgreSQLProjectBotMembershipStore } from "./stores/PostgreSQLProjectBotMembershipStore";
94
95
  import { PostgreSQLProjectRoomMessageStore } from "./stores/PostgreSQLProjectRoomMessageStore";
95
96
  import { PostgreSQLProjectRoomReadStateStore } from "./stores/PostgreSQLProjectRoomReadStateStore";
97
+ import { PostgreSQLUserPushSubscriptionStore } from "./stores/PostgreSQLUserPushSubscriptionStore";
96
98
 
97
99
  export async function createPgStoreConfig(connectionString: string) {
98
100
  const pool = new Pool({ connectionString });
@@ -164,6 +166,7 @@ export async function createPgStoreConfig(connectionString: string) {
164
166
  mm.register(addProjectLifecycleEventIndex); // v175
165
167
  mm.register(addAgentWebAppsIdentity); // v176
166
168
  mm.register(createProjectRoomReadStates); // v177
169
+ mm.register(createUserPushSubscriptions); // v178
167
170
 
168
171
  await mm.migrate();
169
172
 
@@ -208,6 +211,7 @@ export async function createPgStoreConfig(connectionString: string) {
208
211
  projectBotMembership: new PostgreSQLProjectBotMembershipStore(opts),
209
212
  projectRoomMessage: new PostgreSQLProjectRoomMessageStore(opts),
210
213
  projectRoomReadState: new PostgreSQLProjectRoomReadStateStore(opts),
214
+ userPushSubscription: new PostgreSQLUserPushSubscriptionStore(opts),
211
215
  vectorStoreProvider: new PGVectorStoreProvider(pool, connectionString),
212
216
  checkpoint,
213
217
  };
@@ -0,0 +1,30 @@
1
+ import type { Migration } from "./migration";
2
+
3
+ /** Adds per-user Web Push subscriptions for PWA notifications. */
4
+ export const createUserPushSubscriptions: Migration = {
5
+ version: 178,
6
+ name: "create_user_push_subscriptions",
7
+ up: async (client) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_user_push_subscriptions (
10
+ id VARCHAR(255) PRIMARY KEY,
11
+ tenant_id VARCHAR(255) NOT NULL,
12
+ user_id VARCHAR(255) NOT NULL,
13
+ endpoint TEXT NOT NULL,
14
+ p256dh TEXT NOT NULL,
15
+ auth TEXT NOT NULL,
16
+ user_agent TEXT,
17
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18
+ UNIQUE (tenant_id, user_id, endpoint)
19
+ )
20
+ `);
21
+ await client.query(`
22
+ CREATE INDEX IF NOT EXISTS idx_lattice_user_push_subscriptions_user
23
+ ON lattice_user_push_subscriptions (tenant_id, user_id)
24
+ `);
25
+ },
26
+ down: async (client) => {
27
+ await client.query("DROP INDEX IF EXISTS idx_lattice_user_push_subscriptions_user");
28
+ await client.query("DROP TABLE IF EXISTS lattice_user_push_subscriptions");
29
+ },
30
+ };
@@ -284,12 +284,12 @@ export class PostgreSQLTaskWorkItemStore implements TaskWorkItemStore {
284
284
  tenantId: row.tenant_id as string,
285
285
  action: row.action as string,
286
286
  actor: row.actor as string,
287
- threadId: row.thread_id as string | undefined,
288
- summary: row.summary as string | undefined,
287
+ threadId: row.thread_id == null ? undefined : row.thread_id as string,
288
+ summary: row.summary == null ? undefined : row.summary as string,
289
289
  detail: isRecord(row.detail) ? row.detail : undefined,
290
- attempt: row.attempt as number | undefined,
291
- workspaceId: row.workspace_id as string | undefined,
292
- projectId: row.project_id as string | undefined,
290
+ attempt: row.attempt == null ? undefined : row.attempt as number,
291
+ workspaceId: row.workspace_id == null ? undefined : row.workspace_id as string,
292
+ projectId: row.project_id == null ? undefined : row.project_id as string,
293
293
  eventKey: row.event_key == null ? undefined : row.event_key as string,
294
294
  createdAt: new Date(row.created_at as string),
295
295
  };
@@ -0,0 +1,61 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { UserPushSubscription, UserPushSubscriptionStore } from "@axiom-lattice/protocols";
3
+ import type { Pool } from "pg";
4
+
5
+ type Row = {
6
+ id: unknown; tenant_id: unknown; user_id: unknown; endpoint: unknown;
7
+ p256dh: unknown; auth: unknown; user_agent: unknown; created_at: unknown;
8
+ };
9
+ const columns = "id, tenant_id, user_id, endpoint, p256dh, auth, user_agent, created_at";
10
+
11
+ function mapRow(row: Row): UserPushSubscription {
12
+ if (typeof row.id !== "string" || typeof row.tenant_id !== "string" || typeof row.user_id !== "string"
13
+ || typeof row.endpoint !== "string" || typeof row.p256dh !== "string" || typeof row.auth !== "string"
14
+ || !(row.created_at instanceof Date) || Number.isNaN(row.created_at.getTime())) {
15
+ throw new Error("Invalid user push subscription row");
16
+ }
17
+ return {
18
+ id: row.id, tenantId: row.tenant_id, userId: row.user_id, endpoint: row.endpoint,
19
+ keys: { p256dh: row.p256dh, auth: row.auth },
20
+ ...(typeof row.user_agent === "string" ? { userAgent: row.user_agent } : {}),
21
+ createdAt: row.created_at.toISOString(),
22
+ };
23
+ }
24
+
25
+ /** Persists per-user Web Push subscriptions in PostgreSQL. */
26
+ export class PostgreSQLUserPushSubscriptionStore implements UserPushSubscriptionStore {
27
+ /** Creates a store using an externally managed shared pool. */
28
+ constructor(options: { pool: Pool }) { this.pool = options.pool; }
29
+ private readonly pool: Pool;
30
+
31
+ /** Lists every subscription for a user. */
32
+ async list(tenantId: string, userId: string): Promise<UserPushSubscription[]> {
33
+ const result = await this.pool.query<Row>(
34
+ `SELECT ${columns} FROM lattice_user_push_subscriptions WHERE tenant_id = $1 AND user_id = $2`,
35
+ [tenantId, userId],
36
+ );
37
+ return result.rows.map(mapRow);
38
+ }
39
+
40
+ /** Upserts a subscription keyed by tenant + user + endpoint. */
41
+ async save(input: Omit<UserPushSubscription, "id" | "createdAt">): Promise<UserPushSubscription> {
42
+ const result = await this.pool.query<Row>(
43
+ `INSERT INTO lattice_user_push_subscriptions (${columns})
44
+ VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
45
+ ON CONFLICT (tenant_id, user_id, endpoint) DO UPDATE
46
+ SET p256dh = EXCLUDED.p256dh, auth = EXCLUDED.auth, user_agent = EXCLUDED.user_agent
47
+ RETURNING ${columns}`,
48
+ [randomUUID(), input.tenantId, input.userId, input.endpoint, input.keys.p256dh, input.keys.auth, input.userAgent ?? null],
49
+ );
50
+ return mapRow(result.rows[0]);
51
+ }
52
+
53
+ /** Deletes one subscription by endpoint. */
54
+ async delete(tenantId: string, userId: string, endpoint: string): Promise<boolean> {
55
+ const result = await this.pool.query(
56
+ `DELETE FROM lattice_user_push_subscriptions WHERE tenant_id = $1 AND user_id = $2 AND endpoint = $3`,
57
+ [tenantId, userId, endpoint],
58
+ );
59
+ return (result.rowCount ?? 0) > 0;
60
+ }
61
+ }