@recombine-ai/platform 0.2.6-test.7 → 0.2.6-test.8
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/build/analytics.d.ts +24 -12
- package/build/analytics.d.ts.map +1 -1
- package/build/analytics.js +48 -26
- package/build/postgres-analytics-query.d.ts +3 -3
- package/build/postgres-analytics-query.d.ts.map +1 -1
- package/build/postgres-analytics-query.js +8 -12
- package/build/postgres-analytics.d.ts +6 -5
- package/build/postgres-analytics.d.ts.map +1 -1
- package/build/postgres-analytics.js +44 -22
- package/package.json +1 -1
package/build/analytics.d.ts
CHANGED
|
@@ -2,29 +2,41 @@ import { z } from 'zod';
|
|
|
2
2
|
export type AnalyticsJsonValue = boolean | number | string | null | AnalyticsJsonValue[] | {
|
|
3
3
|
[key: string]: AnalyticsJsonValue;
|
|
4
4
|
};
|
|
5
|
-
export
|
|
6
|
-
|
|
5
|
+
export type AnalyticsFieldType = 'boolean' | 'json' | 'number' | 'string';
|
|
6
|
+
export interface AnalyticsFieldDefinition<T> {
|
|
7
|
+
readonly type: AnalyticsFieldType;
|
|
7
8
|
readonly schema: z.ZodType<T>;
|
|
8
9
|
}
|
|
9
|
-
export
|
|
10
|
+
export interface AnalyticsField<T> extends AnalyticsFieldDefinition<T> {
|
|
11
|
+
readonly agentId: string;
|
|
12
|
+
readonly key: string;
|
|
13
|
+
}
|
|
14
|
+
export type AnalyticsFieldDefinitions = Readonly<Record<string, AnalyticsFieldDefinition<unknown>>>;
|
|
15
|
+
export type RegisteredAnalyticsFields<TFields extends AnalyticsFieldDefinitions> = {
|
|
16
|
+
readonly [TKey in keyof TFields]: AnalyticsField<AnalyticsValue<TFields[TKey]>>;
|
|
17
|
+
};
|
|
18
|
+
export type AnalyticsValue<TField extends AnalyticsFieldDefinition<unknown>> = TField extends AnalyticsFieldDefinition<infer TValue> ? TValue : never;
|
|
10
19
|
export interface ConversationAnalyticsSummary {
|
|
11
20
|
conversationId: string;
|
|
21
|
+
agentId: string;
|
|
12
22
|
channel: string;
|
|
13
23
|
startedAt: Date;
|
|
14
24
|
}
|
|
15
25
|
export interface Analytics {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
registerAgent<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
|
|
27
|
+
set<TValue>(conversationId: string, field: AnalyticsField<TValue>, value: TValue): Promise<void>;
|
|
28
|
+
clear(conversationId: string, field: AnalyticsField<unknown>): Promise<void>;
|
|
29
|
+
get<TValue>(conversationId: string, field: AnalyticsField<TValue>): Promise<TValue | undefined>;
|
|
19
30
|
getSummary(conversationId: string): Promise<ConversationAnalyticsSummary | undefined>;
|
|
20
31
|
}
|
|
21
|
-
export declare const
|
|
22
|
-
boolean(
|
|
23
|
-
number(
|
|
24
|
-
string(
|
|
25
|
-
enum<const TValues extends readonly [string, ...string[]]>(
|
|
26
|
-
json<T extends AnalyticsJsonValue>(
|
|
32
|
+
export declare const analyticsField: {
|
|
33
|
+
boolean(): AnalyticsFieldDefinition<boolean>;
|
|
34
|
+
number(): AnalyticsFieldDefinition<number>;
|
|
35
|
+
string(): AnalyticsFieldDefinition<string>;
|
|
36
|
+
enum<const TValues extends readonly [string, ...string[]]>(values: TValues): AnalyticsFieldDefinition<TValues[number]>;
|
|
37
|
+
json<T extends AnalyticsJsonValue>(schema: z.ZodType<T>): AnalyticsFieldDefinition<T>;
|
|
27
38
|
};
|
|
39
|
+
export declare function registerAnalyticsFields<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
|
|
28
40
|
export declare function createInMemoryAnalytics(summaries?: ConversationAnalyticsSummary[]): Analytics;
|
|
29
41
|
export declare function assertAnalyticsJsonValue(value: unknown): asserts value is AnalyticsJsonValue;
|
|
30
42
|
//# sourceMappingURL=analytics.d.ts.map
|
package/build/analytics.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,MAAM,MAAM,kBAAkB,GACxB,OAAO,GACP,MAAM,GACN,MAAM,GACN,IAAI,GACJ,kBAAkB,EAAE,GACpB;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
|
package/build/analytics.js
CHANGED
|
@@ -1,57 +1,79 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.analyticsField = void 0;
|
|
4
|
+
exports.registerAnalyticsFields = registerAnalyticsFields;
|
|
4
5
|
exports.createInMemoryAnalytics = createInMemoryAnalytics;
|
|
5
6
|
exports.assertAnalyticsJsonValue = assertAnalyticsJsonValue;
|
|
6
7
|
const zod_1 = require("zod");
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
throw new Error('Analytics key contains unsupported characters');
|
|
10
|
-
}
|
|
11
|
-
return Object.freeze({ key, schema });
|
|
8
|
+
function defineField(type, schema) {
|
|
9
|
+
return Object.freeze({ type, schema });
|
|
12
10
|
}
|
|
13
|
-
exports.
|
|
14
|
-
boolean(
|
|
15
|
-
return
|
|
11
|
+
exports.analyticsField = {
|
|
12
|
+
boolean() {
|
|
13
|
+
return defineField('boolean', zod_1.z.boolean());
|
|
16
14
|
},
|
|
17
|
-
number(
|
|
18
|
-
return
|
|
15
|
+
number() {
|
|
16
|
+
return defineField('number', zod_1.z.number().finite());
|
|
19
17
|
},
|
|
20
|
-
string(
|
|
21
|
-
return
|
|
18
|
+
string() {
|
|
19
|
+
return defineField('string', zod_1.z.string());
|
|
22
20
|
},
|
|
23
|
-
enum(
|
|
24
|
-
return
|
|
21
|
+
enum(values) {
|
|
22
|
+
return defineField('string', zod_1.z.enum(values));
|
|
25
23
|
},
|
|
26
|
-
json(
|
|
27
|
-
return
|
|
24
|
+
json(schema) {
|
|
25
|
+
return defineField('json', schema);
|
|
28
26
|
},
|
|
29
27
|
};
|
|
28
|
+
function registerAnalyticsFields(agentId, fields) {
|
|
29
|
+
if (agentId.trim() === '') {
|
|
30
|
+
throw new Error('Analytics agent ID must not be empty');
|
|
31
|
+
}
|
|
32
|
+
const registered = Object.fromEntries(Object.entries(fields).map(([key, definition]) => {
|
|
33
|
+
if (!/^[A-Za-z0-9_.:-]{1,128}$/.test(key)) {
|
|
34
|
+
throw new Error('Analytics field name contains unsupported characters');
|
|
35
|
+
}
|
|
36
|
+
return [key, Object.freeze({ ...definition, agentId, key })];
|
|
37
|
+
}));
|
|
38
|
+
return Object.freeze(registered);
|
|
39
|
+
}
|
|
30
40
|
function createInMemoryAnalytics(summaries = []) {
|
|
31
41
|
const conversations = new Map();
|
|
42
|
+
const conversationAgents = new Map(summaries.map((summary) => [summary.conversationId, summary.agentId]));
|
|
32
43
|
const summariesById = new Map(summaries.map((summary) => [summary.conversationId, structuredClone(summary)]));
|
|
33
44
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
registerAgent: registerAnalyticsFields,
|
|
46
|
+
async set(conversationId, field, value) {
|
|
47
|
+
const existingAgentId = conversationAgents.get(conversationId);
|
|
48
|
+
if (existingAgentId !== undefined && existingAgentId !== field.agentId) {
|
|
49
|
+
throw new Error(`Conversation '${conversationId}' belongs to another agent`);
|
|
50
|
+
}
|
|
51
|
+
const parsed = field.schema.parse(value);
|
|
36
52
|
assertAnalyticsJsonValue(parsed);
|
|
37
53
|
const values = conversations.get(conversationId) ?? new Map();
|
|
38
|
-
values.set(
|
|
54
|
+
values.set(field.key, structuredClone(parsed));
|
|
39
55
|
conversations.set(conversationId, values);
|
|
56
|
+
conversationAgents.set(conversationId, field.agentId);
|
|
40
57
|
},
|
|
41
|
-
async clear(conversationId,
|
|
58
|
+
async clear(conversationId, field) {
|
|
59
|
+
if (conversationAgents.get(conversationId) !== field.agentId) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
42
62
|
const values = conversations.get(conversationId);
|
|
43
|
-
values?.delete(
|
|
63
|
+
values?.delete(field.key);
|
|
44
64
|
if (values?.size === 0) {
|
|
45
65
|
conversations.delete(conversationId);
|
|
46
66
|
}
|
|
47
67
|
},
|
|
48
|
-
async get(conversationId,
|
|
68
|
+
async get(conversationId, field) {
|
|
69
|
+
if (conversationAgents.get(conversationId) !== field.agentId) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
49
72
|
const values = conversations.get(conversationId);
|
|
50
|
-
if (!values?.has(
|
|
73
|
+
if (!values?.has(field.key)) {
|
|
51
74
|
return undefined;
|
|
52
75
|
}
|
|
53
|
-
|
|
54
|
-
return key.schema.parse(structuredClone(value));
|
|
76
|
+
return field.schema.parse(structuredClone(values.get(field.key)));
|
|
55
77
|
},
|
|
56
78
|
async getSummary(conversationId) {
|
|
57
79
|
const summary = summariesById.get(conversationId);
|
|
@@ -24,7 +24,7 @@ export type AnalyticsQueryParameters = Readonly<Record<string, unknown>>;
|
|
|
24
24
|
export interface TelescopeConversation {
|
|
25
25
|
conversationId: string;
|
|
26
26
|
medium: string;
|
|
27
|
-
agentId
|
|
27
|
+
agentId: string;
|
|
28
28
|
region?: string;
|
|
29
29
|
phoneNumber?: string;
|
|
30
30
|
duration?: number;
|
|
@@ -42,14 +42,14 @@ export interface AnalyticsFieldValue {
|
|
|
42
42
|
value: AnalyticsFilterScalar;
|
|
43
43
|
count: number;
|
|
44
44
|
}
|
|
45
|
-
export interface
|
|
45
|
+
export interface AnalyticsFieldMetadata {
|
|
46
46
|
key: string;
|
|
47
47
|
type: AnalyticsFilterValueType;
|
|
48
48
|
variantCount: number;
|
|
49
49
|
values: AnalyticsFieldValue[];
|
|
50
50
|
}
|
|
51
51
|
export interface AnalyticsFields {
|
|
52
|
-
items:
|
|
52
|
+
items: AnalyticsFieldMetadata[];
|
|
53
53
|
}
|
|
54
54
|
export interface AnalyticsTimeSeriesItem {
|
|
55
55
|
startAt: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-analytics-query.d.ts","sourceRoot":"","sources":["../src/postgres-analytics-query.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAElD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAIhD,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAA;AAC7D,MAAM,MAAM,wBAAwB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAA;AACtE,MAAM,MAAM,uBAAuB,GAAG,IAAI,GAAG,QAAQ,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,CAAA;AAE1F,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,wBAAwB,CAAA;IAC9B,QAAQ,EAAE,uBAAuB,CAAA;IACjC,KAAK,CAAC,EAAE,qBAAqB,CAAA;IAC7B,MAAM,CAAC,EAAE,qBAAqB,EAAE,CAAA;CACnC;AAED,MAAM,MAAM,eAAe,GACrB;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GACtB;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,GAC9C;IAAE,SAAS,EAAE,SAAS,GAAG,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnD,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;AACxE,MAAM,MAAM,wBAAwB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAIxE,MAAM,WAAW,qBAAqB;IAClC,cAAc,EAAE,MAAM,CAAA;IACtB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,
|
|
1
|
+
{"version":3,"file":"postgres-analytics-query.d.ts","sourceRoot":"","sources":["../src/postgres-analytics-query.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,7 +20,7 @@ class PostgresAnalyticsQueryService {
|
|
|
20
20
|
LEFT JOIN phone_calls p ON p.conversation_id = c.conversation_id
|
|
21
21
|
WHERE c.conversation_id = $1`, [conversationId]);
|
|
22
22
|
const row = result.rows[0];
|
|
23
|
-
return row ? conversationRowToJson(row) : undefined;
|
|
23
|
+
return row?.agent_id ? conversationRowToJson(row) : undefined;
|
|
24
24
|
}
|
|
25
25
|
async getConversations(query) {
|
|
26
26
|
const parsedLimit = Number.parseInt(optionalString(query.limit, 'limit') ?? '10', 10);
|
|
@@ -51,13 +51,10 @@ class PostgresAnalyticsQueryService {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
async getAnalyticsFields(query) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
values.push(agentId);
|
|
59
|
-
where += ` AND c.agent_id = $${values.length}`;
|
|
60
|
-
}
|
|
54
|
+
const agentId = requiredString(query.agentId, 'agentId');
|
|
55
|
+
const values = [agentId];
|
|
56
|
+
const where = "WHERE jsonb_typeof(entry.value) IN ('boolean', 'number', 'string')" +
|
|
57
|
+
' AND c.agent_id = $1';
|
|
61
58
|
const result = await this.database.query(`WITH value_counts AS (
|
|
62
59
|
SELECT entry.key,
|
|
63
60
|
jsonb_typeof(entry.value) AS type,
|
|
@@ -297,7 +294,7 @@ function conversationRowToJson(row) {
|
|
|
297
294
|
return {
|
|
298
295
|
conversationId: row.conversation_id,
|
|
299
296
|
medium: row.channel,
|
|
300
|
-
agentId: row.agent_id
|
|
297
|
+
agentId: row.agent_id,
|
|
301
298
|
region: row.region || undefined,
|
|
302
299
|
phoneNumber: row.phone_number || undefined,
|
|
303
300
|
duration: row.duration_seconds ?? undefined,
|
|
@@ -339,12 +336,11 @@ function buildConversationConditions(query) {
|
|
|
339
336
|
if (from && to && from >= to) {
|
|
340
337
|
throw new AnalyticsQueryError('from must be earlier than to');
|
|
341
338
|
}
|
|
342
|
-
const agentId =
|
|
339
|
+
const agentId = requiredString(query.agentId, 'agentId');
|
|
343
340
|
const region = optionalString(query.region, 'region');
|
|
344
341
|
const channel = optionalString(query.channel, 'channel');
|
|
345
342
|
const phoneNumber = optionalString(query.phoneNumber, 'phoneNumber');
|
|
346
|
-
|
|
347
|
-
conditions.push(`c.agent_id = ${bind(agentId)}`);
|
|
343
|
+
conditions.push(`c.agent_id = ${bind(agentId)}`);
|
|
348
344
|
if (region)
|
|
349
345
|
conditions.push(`c.region = ${bind(region)}`);
|
|
350
346
|
if (channel)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Pool, PoolConfig, QueryResult } from 'pg';
|
|
2
|
-
import { Analytics,
|
|
2
|
+
import { Analytics, AnalyticsField, AnalyticsFieldDefinitions, ConversationAnalyticsSummary, RegisteredAnalyticsFields } from './analytics';
|
|
3
3
|
import { PostgresAnalyticsQueryService } from './postgres-analytics-query';
|
|
4
4
|
export interface ConversationRecord {
|
|
5
5
|
conversationId: string;
|
|
6
|
-
agentId
|
|
6
|
+
agentId: string;
|
|
7
7
|
channel: string;
|
|
8
8
|
region?: string;
|
|
9
9
|
startedAt: Date;
|
|
@@ -21,9 +21,10 @@ interface Queryable {
|
|
|
21
21
|
export declare class PostgresAnalytics implements Analytics {
|
|
22
22
|
private readonly database;
|
|
23
23
|
constructor(database: Queryable);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
registerAgent<TFields extends AnalyticsFieldDefinitions>(agentId: string, fields: TFields): RegisteredAnalyticsFields<TFields>;
|
|
25
|
+
set<TValue>(conversationId: string, field: AnalyticsField<TValue>, value: TValue): Promise<void>;
|
|
26
|
+
clear(conversationId: string, field: AnalyticsField<unknown>): Promise<void>;
|
|
27
|
+
get<TValue>(conversationId: string, field: AnalyticsField<TValue>): Promise<TValue | undefined>;
|
|
27
28
|
getSummary(conversationId: string): Promise<ConversationAnalyticsSummary | undefined>;
|
|
28
29
|
}
|
|
29
30
|
export declare class PostgresConversationRepository {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-analytics.d.ts","sourceRoot":"","sources":["../src/postgres-analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAE9D,OAAO,EACH,SAAS,EACT,
|
|
1
|
+
{"version":3,"file":"postgres-analytics.d.ts","sourceRoot":"","sources":["../src/postgres-analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAc,UAAU,EAAE,WAAW,EAAE,MAAM,IAAI,CAAA;AAE9D,OAAO,EACH,SAAS,EACT,cAAc,EACd,yBAAyB,EACzB,4BAA4B,EAC5B,yBAAyB,EAG5B,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAA;AAI1E,MAAM,WAAW,kBAAkB;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,IAAI,CAAA;IACf,OAAO,CAAC,EAAE,IAAI,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;CAC3B;AAID,MAAM,WAAW,eAAe;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;CACvB;AAID,UAAU,SAAS;IACf,KAAK,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnE,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACnB,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;CACnC;AAaD,qBAAa,iBAAkB,YAAW,SAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,SAAS;IAEhD,aAAa,CAAC,OAAO,SAAS,yBAAyB,EACnD,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,OAAO,GAChB,yBAAyB,CAAC,OAAO,CAAC;IAI/B,GAAG,CAAC,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,EAC7B,KAAK,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAqBV,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAc5E,GAAG,CAAC,MAAM,EACZ,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,cAAc,CAAC,MAAM,CAAC,GAC9B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAiBxB,UAAU,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,GAAG,SAAS,CAAC;CAsB9F;AAgBD,qBAAa,8BAA8B;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI;IAEjC,gBAAgB,CAClB,YAAY,EAAE,kBAAkB,EAChC,SAAS,CAAC,EAAE,eAAe,GAC5B,OAAO,CAAC,IAAI,CAAC;YAmBF,kBAAkB;YA4ClB,eAAe;CA0BhC;AAsED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDxE;AA6BD,MAAM,WAAW,yBAAyB;IACtC,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,8BAA8B,CAAA;IAC7C,OAAO,EAAE,6BAA6B,CAAA;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAID,wBAAsB,+BAA+B,CACjD,MAAM,EAAE,UAAU,GACnB,OAAO,CAAC,yBAAyB,CAAC,CAgBpC"}
|
|
@@ -11,55 +11,64 @@ class PostgresAnalytics {
|
|
|
11
11
|
constructor(database) {
|
|
12
12
|
this.database = database;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
registerAgent(agentId, fields) {
|
|
15
|
+
return (0, analytics_1.registerAnalyticsFields)(agentId, fields);
|
|
16
|
+
}
|
|
17
|
+
async set(conversationId, field, value) {
|
|
18
|
+
const parsed = field.schema.parse(value);
|
|
16
19
|
(0, analytics_1.assertAnalyticsJsonValue)(parsed);
|
|
17
20
|
const serialized = JSON.stringify(parsed);
|
|
18
21
|
if (serialized === undefined) {
|
|
19
|
-
throw new Error(`Analytics value for '${
|
|
22
|
+
throw new Error(`Analytics value for '${field.key}' is not valid JSON`);
|
|
20
23
|
}
|
|
21
24
|
const result = await this.database.query(`UPDATE conversations
|
|
22
|
-
SET analytics = jsonb_set(analytics, ARRAY[$
|
|
25
|
+
SET analytics = jsonb_set(analytics, ARRAY[$3::text], $4::jsonb, true),
|
|
23
26
|
updated_at = now()
|
|
24
|
-
WHERE conversation_id = $1
|
|
25
|
-
|
|
27
|
+
WHERE conversation_id = $1
|
|
28
|
+
AND agent_id = $2`, [conversationId, field.agentId, field.key, serialized]);
|
|
29
|
+
assertConversationBelongsToAgent(result.rowCount, conversationId, field.agentId);
|
|
26
30
|
}
|
|
27
|
-
async clear(conversationId,
|
|
31
|
+
async clear(conversationId, field) {
|
|
28
32
|
const result = await this.database.query(`UPDATE conversations
|
|
29
|
-
SET analytics = analytics - $
|
|
33
|
+
SET analytics = analytics - $3::text,
|
|
30
34
|
updated_at = now()
|
|
31
|
-
WHERE conversation_id = $1
|
|
32
|
-
|
|
35
|
+
WHERE conversation_id = $1
|
|
36
|
+
AND agent_id = $2`, [conversationId, field.agentId, field.key]);
|
|
37
|
+
assertConversationBelongsToAgent(result.rowCount, conversationId, field.agentId);
|
|
33
38
|
}
|
|
34
|
-
async get(conversationId,
|
|
35
|
-
const result = await this.database.query(`SELECT analytics ? $
|
|
36
|
-
analytics -> $
|
|
39
|
+
async get(conversationId, field) {
|
|
40
|
+
const result = await this.database.query(`SELECT analytics ? $3::text AS present,
|
|
41
|
+
analytics -> $3::text AS value
|
|
37
42
|
FROM conversations
|
|
38
|
-
WHERE conversation_id = $1
|
|
43
|
+
WHERE conversation_id = $1
|
|
44
|
+
AND agent_id = $2`, [conversationId, field.agentId, field.key]);
|
|
39
45
|
if (result.rows.length === 0 || !result.rows[0].present) {
|
|
40
46
|
return undefined;
|
|
41
47
|
}
|
|
42
|
-
return
|
|
48
|
+
return field.schema.parse(result.rows[0].value);
|
|
43
49
|
}
|
|
44
50
|
async getSummary(conversationId) {
|
|
45
|
-
const result = await this.database.query(`SELECT conversation_id, channel, started_at
|
|
51
|
+
const result = await this.database.query(`SELECT conversation_id, agent_id, channel, started_at
|
|
46
52
|
FROM conversations
|
|
47
|
-
WHERE conversation_id = $1
|
|
53
|
+
WHERE conversation_id = $1
|
|
54
|
+
AND agent_id IS NOT NULL
|
|
55
|
+
AND btrim(agent_id) <> ''`, [conversationId]);
|
|
48
56
|
const row = result.rows[0];
|
|
49
57
|
if (!row) {
|
|
50
58
|
return undefined;
|
|
51
59
|
}
|
|
52
60
|
return {
|
|
53
61
|
conversationId: row.conversation_id,
|
|
62
|
+
agentId: row.agent_id,
|
|
54
63
|
channel: row.channel,
|
|
55
64
|
startedAt: row.started_at,
|
|
56
65
|
};
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
exports.PostgresAnalytics = PostgresAnalytics;
|
|
60
|
-
function
|
|
69
|
+
function assertConversationBelongsToAgent(rowCount, conversationId, agentId) {
|
|
61
70
|
if (rowCount !== 1) {
|
|
62
|
-
throw new Error(`Conversation '${conversationId}' does not
|
|
71
|
+
throw new Error(`Conversation '${conversationId}' does not belong to agent '${agentId}'`);
|
|
63
72
|
}
|
|
64
73
|
}
|
|
65
74
|
class PostgresConversationRepository {
|
|
@@ -86,7 +95,7 @@ class PostgresConversationRepository {
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
async upsertConversation(client, conversation) {
|
|
89
|
-
await client.query(`INSERT INTO conversations (
|
|
98
|
+
const result = await client.query(`INSERT INTO conversations (
|
|
90
99
|
conversation_id,
|
|
91
100
|
agent_id,
|
|
92
101
|
channel,
|
|
@@ -103,15 +112,18 @@ class PostgresConversationRepository {
|
|
|
103
112
|
conversations.duration_seconds,
|
|
104
113
|
EXCLUDED.duration_seconds
|
|
105
114
|
),
|
|
106
|
-
updated_at = now()
|
|
115
|
+
updated_at = now()
|
|
116
|
+
WHERE conversations.agent_id IS NULL
|
|
117
|
+
OR conversations.agent_id = EXCLUDED.agent_id`, [
|
|
107
118
|
conversation.conversationId,
|
|
108
|
-
conversation.agentId
|
|
119
|
+
conversation.agentId,
|
|
109
120
|
conversation.channel,
|
|
110
121
|
conversation.region ?? null,
|
|
111
122
|
conversation.startedAt,
|
|
112
123
|
conversation.endedAt ?? null,
|
|
113
124
|
conversation.durationSeconds ?? null,
|
|
114
125
|
]);
|
|
126
|
+
assertConversationBelongsToAgent(result.rowCount, conversation.conversationId, conversation.agentId);
|
|
115
127
|
}
|
|
116
128
|
async upsertPhoneCall(client, conversationId, phoneCall) {
|
|
117
129
|
await client.query(`INSERT INTO phone_calls (
|
|
@@ -184,6 +196,16 @@ const migrations = [
|
|
|
184
196
|
ON phone_calls (phone_number, conversation_id);
|
|
185
197
|
`,
|
|
186
198
|
},
|
|
199
|
+
{
|
|
200
|
+
version: 2,
|
|
201
|
+
sql: `
|
|
202
|
+
-- Analytics rationale: Enforce mandatory agent ownership for new and updated rows
|
|
203
|
+
-- without making deployment fail on historical conversations that have no agent ID.
|
|
204
|
+
ALTER TABLE conversations
|
|
205
|
+
ADD CONSTRAINT conversations_agent_id_required
|
|
206
|
+
CHECK (agent_id IS NOT NULL AND btrim(agent_id) <> '') NOT VALID;
|
|
207
|
+
`,
|
|
208
|
+
},
|
|
187
209
|
];
|
|
188
210
|
async function migrateAnalyticsDatabase(pool) {
|
|
189
211
|
const client = await pool.connect();
|