@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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +103 -3
- package/dist/index.d.ts +103 -3
- package/dist/index.js +502 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +498 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/ChannelIdentityMappingStore.test.ts +219 -0
- package/src/__tests__/PostgreSQLChannelInstallationStore.test.ts +54 -0
- package/src/index.ts +11 -0
- package/src/migrations/channel_identity_mapping_migration.ts +59 -0
- package/src/migrations/channel_installation_migrations.ts +31 -0
- package/src/stores/ChannelIdentityMappingStore.ts +334 -0
- package/src/stores/PostgreSQLChannelInstallationStore.ts +304 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Pool, PoolConfig } from "pg";
|
|
2
|
+
import { MigrationManager } from "../migrations/migration";
|
|
3
|
+
import { createChannelIdentityMappingTables } from "../migrations/channel_identity_mapping_migration";
|
|
4
|
+
|
|
5
|
+
export interface ChannelIdentityMappingStoreOptions {
|
|
6
|
+
poolConfig: string | PoolConfig;
|
|
7
|
+
autoMigrate?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CreateChannelIdentityMappingInput {
|
|
11
|
+
channel: string;
|
|
12
|
+
channelAppId: string;
|
|
13
|
+
tenantId: string;
|
|
14
|
+
assistantId: string;
|
|
15
|
+
mappingMode: "user" | "group" | "hybrid";
|
|
16
|
+
externalSubjectType: "user" | "chat";
|
|
17
|
+
externalSubjectKey: string;
|
|
18
|
+
larkOpenId?: string;
|
|
19
|
+
larkChatId: string;
|
|
20
|
+
larkMessageId?: string;
|
|
21
|
+
threadId: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ChannelIdentityMappingRow {
|
|
25
|
+
id: string;
|
|
26
|
+
channel: string;
|
|
27
|
+
channel_app_id: string;
|
|
28
|
+
tenant_id: string;
|
|
29
|
+
assistant_id: string;
|
|
30
|
+
mapping_mode: "user" | "group" | "hybrid";
|
|
31
|
+
external_subject_type: "user" | "chat";
|
|
32
|
+
external_subject_key: string;
|
|
33
|
+
lark_open_id: string | null;
|
|
34
|
+
lark_chat_id: string;
|
|
35
|
+
lark_message_id: string | null;
|
|
36
|
+
thread_id: string;
|
|
37
|
+
created_at: Date;
|
|
38
|
+
updated_at: Date;
|
|
39
|
+
last_activity_at: Date;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ChannelIdentityMapping {
|
|
43
|
+
id: string;
|
|
44
|
+
channel: string;
|
|
45
|
+
channelAppId: string;
|
|
46
|
+
tenantId: string;
|
|
47
|
+
assistantId: string;
|
|
48
|
+
mappingMode: "user" | "group" | "hybrid";
|
|
49
|
+
externalSubjectType: "user" | "chat";
|
|
50
|
+
externalSubjectKey: string;
|
|
51
|
+
larkOpenId?: string;
|
|
52
|
+
larkChatId: string;
|
|
53
|
+
larkMessageId?: string;
|
|
54
|
+
threadId: string;
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
updatedAt: Date;
|
|
57
|
+
lastActivityAt: Date;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ChannelIdentityMappingStore {
|
|
61
|
+
private pool: Pool;
|
|
62
|
+
private migrationManager: MigrationManager;
|
|
63
|
+
private initialized = false;
|
|
64
|
+
private initPromise: Promise<void> | null = null;
|
|
65
|
+
|
|
66
|
+
constructor(options: ChannelIdentityMappingStoreOptions) {
|
|
67
|
+
this.pool =
|
|
68
|
+
typeof options.poolConfig === "string"
|
|
69
|
+
? new Pool({ connectionString: options.poolConfig })
|
|
70
|
+
: new Pool(options.poolConfig);
|
|
71
|
+
|
|
72
|
+
this.migrationManager = new MigrationManager(this.pool);
|
|
73
|
+
this.migrationManager.register(createChannelIdentityMappingTables);
|
|
74
|
+
|
|
75
|
+
if (options.autoMigrate !== false) {
|
|
76
|
+
this.initialize().catch((error) => {
|
|
77
|
+
console.error("Failed to initialize ChannelIdentityMappingStore:", error);
|
|
78
|
+
throw error;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async initialize(): Promise<void> {
|
|
84
|
+
if (this.initialized) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.initPromise) {
|
|
89
|
+
return this.initPromise;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.initPromise = (async () => {
|
|
93
|
+
try {
|
|
94
|
+
await this.migrationManager.migrate();
|
|
95
|
+
this.initialized = true;
|
|
96
|
+
} finally {
|
|
97
|
+
this.initPromise = null;
|
|
98
|
+
}
|
|
99
|
+
})();
|
|
100
|
+
|
|
101
|
+
return this.initPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async createMapping(
|
|
105
|
+
input: CreateChannelIdentityMappingInput,
|
|
106
|
+
): Promise<ChannelIdentityMapping> {
|
|
107
|
+
await this.ensureInitialized();
|
|
108
|
+
|
|
109
|
+
const result = await this.pool.query<ChannelIdentityMappingRow>(
|
|
110
|
+
`
|
|
111
|
+
INSERT INTO channel_identity_mappings (
|
|
112
|
+
channel,
|
|
113
|
+
channel_app_id,
|
|
114
|
+
tenant_id,
|
|
115
|
+
assistant_id,
|
|
116
|
+
mapping_mode,
|
|
117
|
+
external_subject_type,
|
|
118
|
+
external_subject_key,
|
|
119
|
+
lark_open_id,
|
|
120
|
+
lark_chat_id,
|
|
121
|
+
lark_message_id,
|
|
122
|
+
thread_id
|
|
123
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
124
|
+
RETURNING *
|
|
125
|
+
`,
|
|
126
|
+
[
|
|
127
|
+
input.channel,
|
|
128
|
+
input.channelAppId,
|
|
129
|
+
input.tenantId,
|
|
130
|
+
input.assistantId,
|
|
131
|
+
input.mappingMode,
|
|
132
|
+
input.externalSubjectType,
|
|
133
|
+
input.externalSubjectKey,
|
|
134
|
+
input.larkOpenId || null,
|
|
135
|
+
input.larkChatId,
|
|
136
|
+
input.larkMessageId || null,
|
|
137
|
+
input.threadId,
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return mapRowToChannelIdentityMapping(result.rows[0]);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getMappingBySubject(input: {
|
|
145
|
+
channel: string;
|
|
146
|
+
channelAppId: string;
|
|
147
|
+
tenantId: string;
|
|
148
|
+
assistantId: string;
|
|
149
|
+
externalSubjectKey: string;
|
|
150
|
+
}): Promise<ChannelIdentityMapping | null> {
|
|
151
|
+
await this.ensureInitialized();
|
|
152
|
+
|
|
153
|
+
const result = await this.pool.query<ChannelIdentityMappingRow>(
|
|
154
|
+
`
|
|
155
|
+
SELECT *
|
|
156
|
+
FROM channel_identity_mappings
|
|
157
|
+
WHERE channel = $1
|
|
158
|
+
AND channel_app_id = $2
|
|
159
|
+
AND tenant_id = $3
|
|
160
|
+
AND assistant_id = $4
|
|
161
|
+
AND external_subject_key = $5
|
|
162
|
+
LIMIT 1
|
|
163
|
+
`,
|
|
164
|
+
[
|
|
165
|
+
input.channel,
|
|
166
|
+
input.channelAppId,
|
|
167
|
+
input.tenantId,
|
|
168
|
+
input.assistantId,
|
|
169
|
+
input.externalSubjectKey,
|
|
170
|
+
],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (result.rows.length === 0) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return mapRowToChannelIdentityMapping(result.rows[0]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async claimInboundReceipt(input: {
|
|
181
|
+
channel: string;
|
|
182
|
+
channelAppId: string;
|
|
183
|
+
externalMessageId: string;
|
|
184
|
+
tenantId: string;
|
|
185
|
+
}): Promise<{ accepted: boolean; status: "processing" | "completed" }> {
|
|
186
|
+
await this.ensureInitialized();
|
|
187
|
+
|
|
188
|
+
const result = await this.pool.query<{
|
|
189
|
+
source: "inserted" | "existing" | "retried";
|
|
190
|
+
status: "processing" | "completed" | "failed";
|
|
191
|
+
}>(
|
|
192
|
+
`
|
|
193
|
+
WITH existing AS (
|
|
194
|
+
SELECT status
|
|
195
|
+
FROM channel_inbound_message_receipts
|
|
196
|
+
WHERE channel = $1
|
|
197
|
+
AND channel_app_id = $2
|
|
198
|
+
AND external_message_id = $3
|
|
199
|
+
AND tenant_id = $4
|
|
200
|
+
),
|
|
201
|
+
inserted AS (
|
|
202
|
+
INSERT INTO channel_inbound_message_receipts (
|
|
203
|
+
channel,
|
|
204
|
+
channel_app_id,
|
|
205
|
+
external_message_id,
|
|
206
|
+
tenant_id,
|
|
207
|
+
status
|
|
208
|
+
)
|
|
209
|
+
SELECT $1, $2, $3, $4, 'processing'
|
|
210
|
+
WHERE NOT EXISTS (SELECT 1 FROM existing)
|
|
211
|
+
RETURNING 'inserted'::text AS source, status
|
|
212
|
+
),
|
|
213
|
+
retried AS (
|
|
214
|
+
UPDATE channel_inbound_message_receipts
|
|
215
|
+
SET status = 'processing', updated_at = CURRENT_TIMESTAMP
|
|
216
|
+
WHERE channel = $1
|
|
217
|
+
AND channel_app_id = $2
|
|
218
|
+
AND external_message_id = $3
|
|
219
|
+
AND tenant_id = $4
|
|
220
|
+
AND EXISTS (SELECT 1 FROM existing WHERE status = 'failed')
|
|
221
|
+
RETURNING 'retried'::text AS source, status
|
|
222
|
+
)
|
|
223
|
+
SELECT source, status FROM inserted
|
|
224
|
+
UNION ALL
|
|
225
|
+
SELECT source, status FROM retried
|
|
226
|
+
UNION ALL
|
|
227
|
+
SELECT 'existing'::text AS source, status FROM existing
|
|
228
|
+
WHERE EXISTS (SELECT 1 FROM existing WHERE status IN ('processing', 'completed'))
|
|
229
|
+
LIMIT 1
|
|
230
|
+
`,
|
|
231
|
+
[
|
|
232
|
+
input.channel,
|
|
233
|
+
input.channelAppId,
|
|
234
|
+
input.externalMessageId,
|
|
235
|
+
input.tenantId,
|
|
236
|
+
],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const source = result.rows[0]?.source;
|
|
240
|
+
const status = result.rows[0]?.status || "processing";
|
|
241
|
+
|
|
242
|
+
if (source === "inserted" || source === "retried") {
|
|
243
|
+
return { accepted: true, status: "processing" };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (status === "completed") {
|
|
247
|
+
return { accepted: false, status: "completed" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return { accepted: false, status: "processing" };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async markInboundReceiptCompleted(input: {
|
|
254
|
+
channel: string;
|
|
255
|
+
channelAppId: string;
|
|
256
|
+
externalMessageId: string;
|
|
257
|
+
tenantId: string;
|
|
258
|
+
threadId: string;
|
|
259
|
+
}): Promise<void> {
|
|
260
|
+
await this.ensureInitialized();
|
|
261
|
+
|
|
262
|
+
await this.pool.query(
|
|
263
|
+
`
|
|
264
|
+
UPDATE channel_inbound_message_receipts
|
|
265
|
+
SET status = 'completed', thread_id = $1, updated_at = CURRENT_TIMESTAMP
|
|
266
|
+
WHERE channel = $2
|
|
267
|
+
AND channel_app_id = $3
|
|
268
|
+
AND external_message_id = $4
|
|
269
|
+
AND tenant_id = $5
|
|
270
|
+
`,
|
|
271
|
+
[
|
|
272
|
+
input.threadId,
|
|
273
|
+
input.channel,
|
|
274
|
+
input.channelAppId,
|
|
275
|
+
input.externalMessageId,
|
|
276
|
+
input.tenantId,
|
|
277
|
+
],
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async markInboundReceiptFailed(input: {
|
|
282
|
+
channel: string;
|
|
283
|
+
channelAppId: string;
|
|
284
|
+
externalMessageId: string;
|
|
285
|
+
tenantId: string;
|
|
286
|
+
}): Promise<void> {
|
|
287
|
+
await this.ensureInitialized();
|
|
288
|
+
|
|
289
|
+
await this.pool.query(
|
|
290
|
+
`
|
|
291
|
+
UPDATE channel_inbound_message_receipts
|
|
292
|
+
SET status = 'failed', updated_at = CURRENT_TIMESTAMP
|
|
293
|
+
WHERE channel = $1
|
|
294
|
+
AND channel_app_id = $2
|
|
295
|
+
AND external_message_id = $3
|
|
296
|
+
AND tenant_id = $4
|
|
297
|
+
`,
|
|
298
|
+
[
|
|
299
|
+
input.channel,
|
|
300
|
+
input.channelAppId,
|
|
301
|
+
input.externalMessageId,
|
|
302
|
+
input.tenantId,
|
|
303
|
+
],
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private async ensureInitialized(): Promise<void> {
|
|
308
|
+
if (!this.initialized) {
|
|
309
|
+
await this.initialize();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function mapRowToChannelIdentityMapping(
|
|
315
|
+
row: ChannelIdentityMappingRow,
|
|
316
|
+
): ChannelIdentityMapping {
|
|
317
|
+
return {
|
|
318
|
+
id: row.id,
|
|
319
|
+
channel: row.channel,
|
|
320
|
+
channelAppId: row.channel_app_id,
|
|
321
|
+
tenantId: row.tenant_id,
|
|
322
|
+
assistantId: row.assistant_id,
|
|
323
|
+
mappingMode: row.mapping_mode,
|
|
324
|
+
externalSubjectType: row.external_subject_type,
|
|
325
|
+
externalSubjectKey: row.external_subject_key,
|
|
326
|
+
larkOpenId: row.lark_open_id || undefined,
|
|
327
|
+
larkChatId: row.lark_chat_id,
|
|
328
|
+
larkMessageId: row.lark_message_id || undefined,
|
|
329
|
+
threadId: row.thread_id,
|
|
330
|
+
createdAt: row.created_at,
|
|
331
|
+
updatedAt: row.updated_at,
|
|
332
|
+
lastActivityAt: row.last_activity_at,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { Pool, PoolConfig } from "pg";
|
|
2
|
+
import {
|
|
3
|
+
ChannelInstallation,
|
|
4
|
+
ChannelInstallationStore,
|
|
5
|
+
CreateChannelInstallationRequest,
|
|
6
|
+
LarkChannelInstallationConfig,
|
|
7
|
+
UpdateChannelInstallationRequest,
|
|
8
|
+
} from "@axiom-lattice/protocols";
|
|
9
|
+
import { MigrationManager } from "../migrations/migration";
|
|
10
|
+
import { createChannelInstallationsTable } from "../migrations/channel_installation_migrations";
|
|
11
|
+
import { decrypt, encrypt } from "@axiom-lattice/core";
|
|
12
|
+
|
|
13
|
+
export interface PostgreSQLChannelInstallationStoreOptions {
|
|
14
|
+
poolConfig: string | PoolConfig;
|
|
15
|
+
autoMigrate?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type ChannelInstallationRow = {
|
|
19
|
+
id: string;
|
|
20
|
+
tenant_id: string;
|
|
21
|
+
channel: "lark";
|
|
22
|
+
name: string | null;
|
|
23
|
+
config: Record<string, unknown>;
|
|
24
|
+
created_at: Date;
|
|
25
|
+
updated_at: Date;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export class PostgreSQLChannelInstallationStore
|
|
29
|
+
implements ChannelInstallationStore
|
|
30
|
+
{
|
|
31
|
+
private pool: Pool;
|
|
32
|
+
private migrationManager: MigrationManager;
|
|
33
|
+
private initialized = false;
|
|
34
|
+
private initPromise: Promise<void> | null = null;
|
|
35
|
+
|
|
36
|
+
constructor(options: PostgreSQLChannelInstallationStoreOptions) {
|
|
37
|
+
this.pool =
|
|
38
|
+
typeof options.poolConfig === "string"
|
|
39
|
+
? new Pool({ connectionString: options.poolConfig })
|
|
40
|
+
: new Pool(options.poolConfig);
|
|
41
|
+
|
|
42
|
+
this.migrationManager = new MigrationManager(this.pool);
|
|
43
|
+
this.migrationManager.register(createChannelInstallationsTable);
|
|
44
|
+
|
|
45
|
+
if (options.autoMigrate !== false) {
|
|
46
|
+
this.initialize().catch((error) => {
|
|
47
|
+
console.error(
|
|
48
|
+
"Failed to initialize PostgreSQLChannelInstallationStore:",
|
|
49
|
+
error,
|
|
50
|
+
);
|
|
51
|
+
throw error;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async initialize(): Promise<void> {
|
|
57
|
+
if (this.initialized) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.initPromise) {
|
|
62
|
+
return this.initPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.initPromise = (async () => {
|
|
66
|
+
try {
|
|
67
|
+
await this.migrationManager.migrate();
|
|
68
|
+
this.initialized = true;
|
|
69
|
+
} finally {
|
|
70
|
+
this.initPromise = null;
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
return this.initPromise;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async getInstallationById(
|
|
78
|
+
installationId: string,
|
|
79
|
+
): Promise<ChannelInstallation | null> {
|
|
80
|
+
await this.ensureInitialized();
|
|
81
|
+
|
|
82
|
+
const result = await this.pool.query<ChannelInstallationRow>(
|
|
83
|
+
`
|
|
84
|
+
SELECT id, tenant_id, channel, name, config, created_at, updated_at
|
|
85
|
+
FROM lattice_channel_installations
|
|
86
|
+
WHERE id = $1
|
|
87
|
+
LIMIT 1
|
|
88
|
+
`,
|
|
89
|
+
[installationId],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (result.rows.length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return this.mapRowToInstallation(result.rows[0]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getInstallationsByTenant(
|
|
100
|
+
tenantId: string,
|
|
101
|
+
channel?: "lark",
|
|
102
|
+
): Promise<ChannelInstallation[]> {
|
|
103
|
+
await this.ensureInitialized();
|
|
104
|
+
|
|
105
|
+
const result = channel
|
|
106
|
+
? await this.pool.query<ChannelInstallationRow>(
|
|
107
|
+
`
|
|
108
|
+
SELECT id, tenant_id, channel, name, config, created_at, updated_at
|
|
109
|
+
FROM lattice_channel_installations
|
|
110
|
+
WHERE tenant_id = $1 AND channel = $2
|
|
111
|
+
ORDER BY created_at DESC
|
|
112
|
+
`,
|
|
113
|
+
[tenantId, channel],
|
|
114
|
+
)
|
|
115
|
+
: await this.pool.query<ChannelInstallationRow>(
|
|
116
|
+
`
|
|
117
|
+
SELECT id, tenant_id, channel, name, config, created_at, updated_at
|
|
118
|
+
FROM lattice_channel_installations
|
|
119
|
+
WHERE tenant_id = $1
|
|
120
|
+
ORDER BY created_at DESC
|
|
121
|
+
`,
|
|
122
|
+
[tenantId],
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return result.rows.map((row) => this.mapRowToInstallation(row));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async createInstallation(
|
|
129
|
+
tenantId: string,
|
|
130
|
+
installationId: string,
|
|
131
|
+
data: CreateChannelInstallationRequest,
|
|
132
|
+
): Promise<ChannelInstallation> {
|
|
133
|
+
await this.ensureInitialized();
|
|
134
|
+
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const encryptedConfig = this.encryptSecrets(data.config);
|
|
137
|
+
|
|
138
|
+
await this.pool.query(
|
|
139
|
+
`
|
|
140
|
+
INSERT INTO lattice_channel_installations (
|
|
141
|
+
id, tenant_id, channel, name, config, created_at, updated_at
|
|
142
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
143
|
+
`,
|
|
144
|
+
[
|
|
145
|
+
installationId,
|
|
146
|
+
tenantId,
|
|
147
|
+
data.channel,
|
|
148
|
+
data.name || null,
|
|
149
|
+
JSON.stringify(encryptedConfig),
|
|
150
|
+
now,
|
|
151
|
+
now,
|
|
152
|
+
],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
id: installationId,
|
|
157
|
+
tenantId,
|
|
158
|
+
channel: data.channel,
|
|
159
|
+
name: data.name,
|
|
160
|
+
config: data.config,
|
|
161
|
+
createdAt: now,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async updateInstallation(
|
|
167
|
+
tenantId: string,
|
|
168
|
+
installationId: string,
|
|
169
|
+
updates: UpdateChannelInstallationRequest,
|
|
170
|
+
): Promise<ChannelInstallation | null> {
|
|
171
|
+
await this.ensureInitialized();
|
|
172
|
+
|
|
173
|
+
const existing = await this.getInstallationById(installationId);
|
|
174
|
+
if (!existing || existing.tenantId !== tenantId) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const mergedConfig = {
|
|
179
|
+
...existing.config,
|
|
180
|
+
...(updates.config || {}),
|
|
181
|
+
};
|
|
182
|
+
const now = new Date();
|
|
183
|
+
|
|
184
|
+
await this.pool.query(
|
|
185
|
+
`
|
|
186
|
+
UPDATE lattice_channel_installations
|
|
187
|
+
SET name = $1,
|
|
188
|
+
config = $2,
|
|
189
|
+
updated_at = $3
|
|
190
|
+
WHERE tenant_id = $4 AND id = $5
|
|
191
|
+
`,
|
|
192
|
+
[
|
|
193
|
+
updates.name ?? existing.name ?? null,
|
|
194
|
+
JSON.stringify(this.encryptSecrets(mergedConfig)),
|
|
195
|
+
now,
|
|
196
|
+
tenantId,
|
|
197
|
+
installationId,
|
|
198
|
+
],
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
...existing,
|
|
203
|
+
name: updates.name ?? existing.name,
|
|
204
|
+
config: mergedConfig,
|
|
205
|
+
updatedAt: now,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async deleteInstallation(
|
|
210
|
+
tenantId: string,
|
|
211
|
+
installationId: string,
|
|
212
|
+
): Promise<boolean> {
|
|
213
|
+
await this.ensureInitialized();
|
|
214
|
+
|
|
215
|
+
const result = await this.pool.query(
|
|
216
|
+
`
|
|
217
|
+
DELETE FROM lattice_channel_installations
|
|
218
|
+
WHERE tenant_id = $1 AND id = $2
|
|
219
|
+
`,
|
|
220
|
+
[tenantId, installationId],
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return (result.rowCount || 0) > 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async ensureInitialized(): Promise<void> {
|
|
227
|
+
if (!this.initialized) {
|
|
228
|
+
await this.initialize();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private mapRowToInstallation(
|
|
233
|
+
row: ChannelInstallationRow,
|
|
234
|
+
): ChannelInstallation {
|
|
235
|
+
return {
|
|
236
|
+
id: row.id,
|
|
237
|
+
tenantId: row.tenant_id,
|
|
238
|
+
channel: row.channel,
|
|
239
|
+
name: row.name || undefined,
|
|
240
|
+
config: this.decryptSecrets(
|
|
241
|
+
typeof row.config === "string"
|
|
242
|
+
? JSON.parse(row.config)
|
|
243
|
+
: row.config,
|
|
244
|
+
),
|
|
245
|
+
createdAt: row.created_at,
|
|
246
|
+
updatedAt: row.updated_at,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private encryptSecrets(
|
|
251
|
+
config: LarkChannelInstallationConfig,
|
|
252
|
+
): Record<string, unknown> {
|
|
253
|
+
return {
|
|
254
|
+
...config,
|
|
255
|
+
appSecret:
|
|
256
|
+
typeof config.appSecret === "string"
|
|
257
|
+
? encrypt(config.appSecret)
|
|
258
|
+
: config.appSecret,
|
|
259
|
+
verificationToken:
|
|
260
|
+
typeof config.verificationToken === "string"
|
|
261
|
+
? encrypt(config.verificationToken)
|
|
262
|
+
: config.verificationToken,
|
|
263
|
+
encryptKey:
|
|
264
|
+
typeof config.encryptKey === "string"
|
|
265
|
+
? encrypt(config.encryptKey)
|
|
266
|
+
: config.encryptKey,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private decryptSecrets(
|
|
271
|
+
config: Record<string, unknown>,
|
|
272
|
+
): LarkChannelInstallationConfig {
|
|
273
|
+
return {
|
|
274
|
+
appId: String(config.appId || ""),
|
|
275
|
+
appSecret:
|
|
276
|
+
typeof config.appSecret === "string"
|
|
277
|
+
? decrypt(config.appSecret)
|
|
278
|
+
: "",
|
|
279
|
+
verificationToken:
|
|
280
|
+
typeof config.verificationToken === "string"
|
|
281
|
+
? decrypt(config.verificationToken)
|
|
282
|
+
: undefined,
|
|
283
|
+
encryptKey:
|
|
284
|
+
typeof config.encryptKey === "string"
|
|
285
|
+
? decrypt(config.encryptKey)
|
|
286
|
+
: undefined,
|
|
287
|
+
mappingMode:
|
|
288
|
+
config.mappingMode === "user" ||
|
|
289
|
+
config.mappingMode === "group" ||
|
|
290
|
+
config.mappingMode === "hybrid"
|
|
291
|
+
? config.mappingMode
|
|
292
|
+
: "hybrid",
|
|
293
|
+
assistantId: String(config.assistantId || ""),
|
|
294
|
+
workspaceId:
|
|
295
|
+
typeof config.workspaceId === "string"
|
|
296
|
+
? config.workspaceId
|
|
297
|
+
: undefined,
|
|
298
|
+
projectId:
|
|
299
|
+
typeof config.projectId === "string"
|
|
300
|
+
? config.projectId
|
|
301
|
+
: undefined,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|