@axiom-lattice/pg-stores 1.0.37 → 1.0.39

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": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "PG stores implementation for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -21,8 +21,8 @@
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "pg": "^8.16.3",
24
- "@axiom-lattice/protocols": "2.1.25",
25
- "@axiom-lattice/core": "2.1.47"
24
+ "@axiom-lattice/protocols": "2.1.26",
25
+ "@axiom-lattice/core": "2.1.49"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^20.11.24",
@@ -0,0 +1,219 @@
1
+ import { ChannelIdentityMappingStore } from "../stores/ChannelIdentityMappingStore";
2
+
3
+ const mockQuery = jest.fn();
4
+
5
+ jest.mock("pg", () => ({
6
+ Pool: jest.fn().mockImplementation(() => ({
7
+ query: mockQuery,
8
+ })),
9
+ }));
10
+
11
+ describe("ChannelIdentityMappingStore", () => {
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ it("creates and retrieves a mapping by external subject key", async () => {
17
+ const store = new ChannelIdentityMappingStore({
18
+ poolConfig: "postgres://test:test@localhost:5432/test",
19
+ autoMigrate: false,
20
+ });
21
+
22
+ mockQuery.mockResolvedValueOnce({
23
+ rows: [
24
+ {
25
+ id: "mapping-1",
26
+ channel: "lark",
27
+ channel_app_id: "cli_app_1",
28
+ tenant_id: "tenant-a",
29
+ assistant_id: "assistant-a",
30
+ mapping_mode: "hybrid",
31
+ external_subject_type: "user",
32
+ external_subject_key:
33
+ "lark:cli_app_1:tenant:tenant-a:assistant:assistant-a:user:ou_xxx",
34
+ lark_open_id: "ou_xxx",
35
+ lark_chat_id: "oc_yyy",
36
+ lark_message_id: "om_1",
37
+ thread_id: "thread-1",
38
+ created_at: new Date("2026-04-10T00:00:00.000Z"),
39
+ updated_at: new Date("2026-04-10T00:00:00.000Z"),
40
+ last_activity_at: new Date("2026-04-10T00:00:00.000Z"),
41
+ },
42
+ ],
43
+ });
44
+
45
+ mockQuery.mockResolvedValueOnce({
46
+ rows: [
47
+ {
48
+ id: "mapping-1",
49
+ channel: "lark",
50
+ channel_app_id: "cli_app_1",
51
+ tenant_id: "tenant-a",
52
+ assistant_id: "assistant-a",
53
+ mapping_mode: "hybrid",
54
+ external_subject_type: "user",
55
+ external_subject_key:
56
+ "lark:cli_app_1:tenant:tenant-a:assistant:assistant-a:user:ou_xxx",
57
+ lark_open_id: "ou_xxx",
58
+ lark_chat_id: "oc_yyy",
59
+ lark_message_id: "om_1",
60
+ thread_id: "thread-1",
61
+ created_at: new Date("2026-04-10T00:00:00.000Z"),
62
+ updated_at: new Date("2026-04-10T00:00:00.000Z"),
63
+ last_activity_at: new Date("2026-04-10T00:00:00.000Z"),
64
+ },
65
+ ],
66
+ });
67
+
68
+ const created = await store.createMapping({
69
+ channel: "lark",
70
+ channelAppId: "cli_app_1",
71
+ tenantId: "tenant-a",
72
+ assistantId: "assistant-a",
73
+ mappingMode: "hybrid",
74
+ externalSubjectType: "user",
75
+ externalSubjectKey:
76
+ "lark:cli_app_1:tenant:tenant-a:assistant:assistant-a:user:ou_xxx",
77
+ larkOpenId: "ou_xxx",
78
+ larkChatId: "oc_yyy",
79
+ larkMessageId: "om_1",
80
+ threadId: "thread-1",
81
+ });
82
+
83
+ const found = await store.getMappingBySubject({
84
+ channel: "lark",
85
+ channelAppId: "cli_app_1",
86
+ tenantId: "tenant-a",
87
+ assistantId: "assistant-a",
88
+ externalSubjectKey:
89
+ "lark:cli_app_1:tenant:tenant-a:assistant:assistant-a:user:ou_xxx",
90
+ });
91
+
92
+ expect(created.threadId).toBe("thread-1");
93
+ expect(found?.threadId).toBe("thread-1");
94
+ });
95
+
96
+ it("returns completed when a duplicate inbound receipt is already completed", async () => {
97
+ const store = new ChannelIdentityMappingStore({
98
+ poolConfig: "postgres://test:test@localhost:5432/test",
99
+ autoMigrate: false,
100
+ });
101
+
102
+ mockQuery.mockResolvedValueOnce({ rows: [{ source: "inserted", status: "processing" }] });
103
+ mockQuery.mockResolvedValueOnce({ rows: [{ source: "existing", status: "completed" }] });
104
+
105
+ const first = await store.claimInboundReceipt({
106
+ channel: "lark",
107
+ channelAppId: "cli_app_1",
108
+ externalMessageId: "om_1",
109
+ tenantId: "tenant-a",
110
+ });
111
+
112
+ const second = await store.claimInboundReceipt({
113
+ channel: "lark",
114
+ channelAppId: "cli_app_1",
115
+ externalMessageId: "om_1",
116
+ tenantId: "tenant-a",
117
+ });
118
+
119
+ expect(first).toEqual({ accepted: true, status: "processing" });
120
+ expect(second).toEqual({ accepted: false, status: "completed" });
121
+ });
122
+
123
+ it("claims a new inbound receipt with processing status", async () => {
124
+ const store = new ChannelIdentityMappingStore({
125
+ poolConfig: "postgres://test:test@localhost:5432/test",
126
+ autoMigrate: false,
127
+ });
128
+
129
+ mockQuery.mockResolvedValueOnce({ rows: [{ source: "inserted", status: "processing" }] });
130
+
131
+ const result = await store.claimInboundReceipt({
132
+ channel: "lark",
133
+ channelAppId: "cli_app_1",
134
+ externalMessageId: "om_processing_1",
135
+ tenantId: "tenant-a",
136
+ });
137
+
138
+ expect(result).toEqual({ accepted: true, status: "processing" });
139
+ });
140
+
141
+ it("allows retry when an inbound receipt was previously marked failed", async () => {
142
+ const store = new ChannelIdentityMappingStore({
143
+ poolConfig: "postgres://test:test@localhost:5432/test",
144
+ autoMigrate: false,
145
+ });
146
+
147
+ mockQuery.mockResolvedValueOnce({ rows: [{ source: "retried", status: "processing" }] });
148
+
149
+ const result = await store.claimInboundReceipt({
150
+ channel: "lark",
151
+ channelAppId: "cli_app_1",
152
+ externalMessageId: "om_failed_1",
153
+ tenantId: "tenant-a",
154
+ });
155
+
156
+ expect(result).toEqual({ accepted: true, status: "processing" });
157
+ });
158
+
159
+ it("rejects a receipt that is already processing", async () => {
160
+ const store = new ChannelIdentityMappingStore({
161
+ poolConfig: "postgres://test:test@localhost:5432/test",
162
+ autoMigrate: false,
163
+ });
164
+
165
+ mockQuery.mockResolvedValueOnce({ rows: [{ source: "existing", status: "processing" }] });
166
+
167
+ const result = await store.claimInboundReceipt({
168
+ channel: "lark",
169
+ channelAppId: "cli_app_1",
170
+ externalMessageId: "om_processing_1",
171
+ tenantId: "tenant-a",
172
+ });
173
+
174
+ expect(result).toEqual({ accepted: false, status: "processing" });
175
+ });
176
+
177
+ it("marks an inbound receipt as failed", async () => {
178
+ const store = new ChannelIdentityMappingStore({
179
+ poolConfig: "postgres://test:test@localhost:5432/test",
180
+ autoMigrate: false,
181
+ });
182
+
183
+ mockQuery.mockResolvedValueOnce({ rowCount: 1 });
184
+
185
+ await store.markInboundReceiptFailed({
186
+ channel: "lark",
187
+ channelAppId: "cli_app_1",
188
+ externalMessageId: "om_failed_1",
189
+ tenantId: "tenant-a",
190
+ });
191
+
192
+ expect(mockQuery).toHaveBeenCalledWith(
193
+ expect.stringContaining("SET status = 'failed'"),
194
+ ["lark", "cli_app_1", "om_failed_1", "tenant-a"],
195
+ );
196
+ });
197
+
198
+ it("marks an inbound receipt as completed", async () => {
199
+ const store = new ChannelIdentityMappingStore({
200
+ poolConfig: "postgres://test:test@localhost:5432/test",
201
+ autoMigrate: false,
202
+ });
203
+
204
+ mockQuery.mockResolvedValueOnce({ rowCount: 1 });
205
+
206
+ await store.markInboundReceiptCompleted({
207
+ channel: "lark",
208
+ channelAppId: "cli_app_1",
209
+ externalMessageId: "om_done_1",
210
+ tenantId: "tenant-a",
211
+ threadId: "thread-1",
212
+ });
213
+
214
+ expect(mockQuery).toHaveBeenCalledWith(
215
+ expect.stringContaining("UPDATE channel_inbound_message_receipts"),
216
+ ["thread-1", "lark", "cli_app_1", "om_done_1", "tenant-a"],
217
+ );
218
+ });
219
+ });
@@ -0,0 +1,54 @@
1
+ import { PostgreSQLChannelInstallationStore } from "../stores/PostgreSQLChannelInstallationStore";
2
+
3
+ const mockQuery = jest.fn();
4
+
5
+ jest.mock("pg", () => ({
6
+ Pool: jest.fn().mockImplementation(() => ({
7
+ query: mockQuery,
8
+ })),
9
+ }));
10
+
11
+ jest.mock("@axiom-lattice/core", () => ({
12
+ encrypt: (value: string) => `enc:${value}`,
13
+ decrypt: (value: string) => value.replace(/^enc:/, ""),
14
+ }));
15
+
16
+ describe("PostgreSQLChannelInstallationStore", () => {
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ it("retrieves and decrypts an installation by id", async () => {
22
+ const store = new PostgreSQLChannelInstallationStore({
23
+ poolConfig: "postgres://test:test@localhost:5432/test",
24
+ autoMigrate: false,
25
+ });
26
+
27
+ mockQuery.mockResolvedValueOnce({
28
+ rows: [
29
+ {
30
+ id: "install-1",
31
+ tenant_id: "tenant-a",
32
+ channel: "lark",
33
+ name: "Tenant A Lark",
34
+ config: {
35
+ appId: "cli_app_1",
36
+ appSecret: "enc:secret",
37
+ verificationToken: "enc:token-1",
38
+ encryptKey: "enc:encrypt-key",
39
+ assistantId: "assistant-a",
40
+ mappingMode: "hybrid",
41
+ },
42
+ created_at: new Date("2026-04-10T00:00:00.000Z"),
43
+ updated_at: new Date("2026-04-10T00:00:00.000Z"),
44
+ },
45
+ ],
46
+ });
47
+
48
+ const installation = await store.getInstallationById("install-1");
49
+
50
+ expect(installation?.tenantId).toBe("tenant-a");
51
+ expect(installation?.config.appSecret).toBe("secret");
52
+ expect(installation?.config.verificationToken).toBe("token-1");
53
+ });
54
+ });
package/src/index.ts CHANGED
@@ -39,7 +39,11 @@ export * from "./migrations/project_migrations";
39
39
  export * from "./migrations/user_migrations";
40
40
  export * from "./migrations/tenant_migrations";
41
41
  export * from "./migrations/thread_message_queue_migrations";
42
+ export * from "./migrations/channel_identity_mapping_migration";
43
+ export * from "./migrations/channel_installation_migrations";
42
44
  export * from "./stores/ThreadMessageQueueStore";
45
+ export * from "./stores/ChannelIdentityMappingStore";
46
+ export * from "./stores/PostgreSQLChannelInstallationStore";
43
47
 
44
48
  // Re-export for convenience
45
49
  export { PostgreSQLThreadStore } from "./stores/PostgreSQLThreadStore";
@@ -54,6 +58,8 @@ export { PostgreSQLProjectStore } from "./stores/PostgreSQLProjectStore";
54
58
  export { PostgreSQLUserStore } from "./stores/PostgreSQLUserStore";
55
59
  export { PostgreSQLTenantStore } from "./stores/PostgreSQLTenantStore";
56
60
  export { PostgreSQLUserTenantLinkStore } from "./stores/PostgreSQLUserTenantLinkStore";
61
+ export { ChannelIdentityMappingStore } from "./stores/ChannelIdentityMappingStore";
62
+ export { PostgreSQLChannelInstallationStore } from "./stores/PostgreSQLChannelInstallationStore";
57
63
 
58
64
  // Re-export types from protocols
59
65
  export type {
@@ -76,6 +82,11 @@ export type {
76
82
  UpdateDatabaseConfigRequest,
77
83
  DatabaseConfig,
78
84
  DatabaseType,
85
+ ChannelInstallationStore,
86
+ ChannelInstallation,
87
+ CreateChannelInstallationRequest,
88
+ UpdateChannelInstallationRequest,
89
+ LarkChannelInstallationConfig,
79
90
  MetricsServerConfigStore,
80
91
  MetricsServerConfigEntry,
81
92
  CreateMetricsServerConfigRequest,
@@ -0,0 +1,59 @@
1
+ import { PoolClient } from "pg";
2
+ import { Migration } from "./migration";
3
+
4
+ export const createChannelIdentityMappingTables: Migration = {
5
+ version: 20,
6
+ name: "create_channel_identity_mapping_tables",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS channel_identity_mappings (
10
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
11
+ channel VARCHAR(50) NOT NULL,
12
+ channel_app_id VARCHAR(255) NOT NULL,
13
+ tenant_id VARCHAR(255) NOT NULL,
14
+ assistant_id VARCHAR(255) NOT NULL,
15
+ mapping_mode VARCHAR(20) NOT NULL CHECK (mapping_mode IN ('user', 'group', 'hybrid')),
16
+ external_subject_type VARCHAR(20) NOT NULL CHECK (external_subject_type IN ('user', 'chat')),
17
+ external_subject_key VARCHAR(512) NOT NULL,
18
+ lark_open_id VARCHAR(255),
19
+ lark_chat_id VARCHAR(255) NOT NULL,
20
+ lark_message_id VARCHAR(255),
21
+ thread_id VARCHAR(255) NOT NULL,
22
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
23
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+ last_activity_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
25
+ UNIQUE (channel, channel_app_id, tenant_id, assistant_id, external_subject_key)
26
+ )
27
+ `);
28
+
29
+ await client.query(`
30
+ CREATE INDEX IF NOT EXISTS idx_channel_identity_lookup
31
+ ON channel_identity_mappings(channel, channel_app_id, tenant_id, assistant_id, external_subject_key)
32
+ `);
33
+
34
+ await client.query(`
35
+ CREATE INDEX IF NOT EXISTS idx_channel_identity_thread
36
+ ON channel_identity_mappings(thread_id, tenant_id)
37
+ `);
38
+
39
+ await client.query(`
40
+ CREATE TABLE IF NOT EXISTS channel_inbound_message_receipts (
41
+ channel VARCHAR(50) NOT NULL,
42
+ channel_app_id VARCHAR(255) NOT NULL,
43
+ external_message_id VARCHAR(255) NOT NULL,
44
+ tenant_id VARCHAR(255) NOT NULL,
45
+ status VARCHAR(20) NOT NULL DEFAULT 'processing' CHECK (status IN ('processing', 'completed', 'failed')),
46
+ thread_id VARCHAR(255),
47
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
48
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
49
+ PRIMARY KEY (channel, channel_app_id, external_message_id)
50
+ )
51
+ `);
52
+ },
53
+ down: async (client: PoolClient) => {
54
+ await client.query("DROP TABLE IF EXISTS channel_inbound_message_receipts");
55
+ await client.query("DROP INDEX IF EXISTS idx_channel_identity_thread");
56
+ await client.query("DROP INDEX IF EXISTS idx_channel_identity_lookup");
57
+ await client.query("DROP TABLE IF EXISTS channel_identity_mappings");
58
+ },
59
+ };
@@ -0,0 +1,31 @@
1
+ import { PoolClient } from "pg";
2
+ import { Migration } from "./migration";
3
+
4
+ export const createChannelInstallationsTable: Migration = {
5
+ version: 21,
6
+ name: "create_channel_installations_table",
7
+ up: async (client: PoolClient) => {
8
+ await client.query(`
9
+ CREATE TABLE IF NOT EXISTS lattice_channel_installations (
10
+ id UUID PRIMARY KEY,
11
+ tenant_id VARCHAR(255) NOT NULL,
12
+ channel VARCHAR(50) NOT NULL,
13
+ name VARCHAR(255),
14
+ config JSONB NOT NULL,
15
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
16
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
17
+ )
18
+ `);
19
+
20
+ await client.query(`
21
+ CREATE INDEX IF NOT EXISTS idx_channel_installations_tenant_channel
22
+ ON lattice_channel_installations(tenant_id, channel)
23
+ `);
24
+ },
25
+ down: async (client: PoolClient) => {
26
+ await client.query(
27
+ "DROP INDEX IF EXISTS idx_channel_installations_tenant_channel",
28
+ );
29
+ await client.query("DROP TABLE IF EXISTS lattice_channel_installations");
30
+ },
31
+ };