@talkpilot/core-db 1.0.4 → 1.0.7

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.
Files changed (48) hide show
  1. package/dist/talkpilot/calls/calls.getters.d.ts +5 -2
  2. package/dist/talkpilot/calls/calls.getters.d.ts.map +1 -1
  3. package/dist/talkpilot/calls/calls.getters.js +25 -1
  4. package/dist/talkpilot/calls/calls.getters.js.map +1 -1
  5. package/dist/talkpilot/calls/calls.types.d.ts +31 -1
  6. package/dist/talkpilot/calls/calls.types.d.ts.map +1 -1
  7. package/dist/talkpilot/calls/calls.types.js +3 -0
  8. package/dist/talkpilot/calls/calls.types.js.map +1 -1
  9. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +1 -1
  10. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
  11. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +5 -2
  12. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
  13. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +6 -3
  14. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
  15. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +35 -19
  16. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
  17. package/dist/talkpilot/plans/plans.getters.d.ts +4 -2
  18. package/dist/talkpilot/plans/plans.getters.d.ts.map +1 -1
  19. package/dist/talkpilot/plans/plans.getters.js +14 -11
  20. package/dist/talkpilot/plans/plans.getters.js.map +1 -1
  21. package/dist/talkpilot/results/results.types.d.ts +2 -0
  22. package/dist/talkpilot/results/results.types.d.ts.map +1 -1
  23. package/dist/talkpilot/sessions/sessions.getter.d.ts.map +1 -1
  24. package/dist/talkpilot/sessions/sessions.getter.js +57 -6
  25. package/dist/talkpilot/sessions/sessions.getter.js.map +1 -1
  26. package/dist/talkpilot/subscriptions/subscriptions.getters.d.ts +2 -1
  27. package/dist/talkpilot/subscriptions/subscriptions.getters.d.ts.map +1 -1
  28. package/dist/talkpilot/subscriptions/subscriptions.getters.js +9 -11
  29. package/dist/talkpilot/subscriptions/subscriptions.getters.js.map +1 -1
  30. package/dist/talkpilot/utils/query.utils.d.ts +8 -0
  31. package/dist/talkpilot/utils/query.utils.d.ts.map +1 -0
  32. package/dist/talkpilot/utils/query.utils.js +17 -0
  33. package/dist/talkpilot/utils/query.utils.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/talkpilot/calls/__tests__/calls.spec.ts +58 -0
  36. package/src/talkpilot/calls/calls.getters.ts +29 -2
  37. package/src/talkpilot/calls/calls.types.ts +38 -1
  38. package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +6 -6
  39. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +57 -1
  40. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +49 -18
  41. package/src/talkpilot/plans/__tests__/plans.spec.ts +66 -0
  42. package/src/talkpilot/plans/plans.getters.ts +26 -22
  43. package/src/talkpilot/results/results.types.ts +2 -0
  44. package/src/talkpilot/sessions/__tests__/sessions.spec.ts +73 -10
  45. package/src/talkpilot/sessions/sessions.getter.ts +66 -7
  46. package/src/talkpilot/subscriptions/subscriptions.getters.ts +19 -23
  47. package/src/talkpilot/utils/__tests__/query.utils.spec.ts +49 -0
  48. package/src/talkpilot/utils/query.utils.ts +21 -0
@@ -3,20 +3,16 @@ import { getFlowsCollection } from '../../flows/flows.getter';
3
3
  import { getPhoneNumbersCollection } from '../../phone_numbers/phone_numbers.getter';
4
4
  import { createFlow, createPhoneNumber, createSession } from '../../../test-utils/factories';
5
5
  import { faker } from '@faker-js/faker';
6
+ import { ObjectId } from 'mongodb';
6
7
 
7
8
  describe('sessions', () => {
8
9
  describe('findSessionOfIncomingCall()', () => {
9
10
  it('returns session by incoming phone number', async () => {
10
- const to = faker.phone.number();
11
- const from = faker.phone.number();
11
+ const to = '+972500000000';
12
+ const from = '+972540000000';
12
13
  const flow = createFlow();
13
14
  const phoneNumberData = createPhoneNumber({ phone_number: to, flow_id: flow._id });
14
- const json = {
15
- [faker.lorem.word()]: faker.lorem.sentence(),
16
- [faker.lorem.word()]: faker.number.int(),
17
- [faker.lorem.word()]: faker.datatype.boolean(),
18
- [faker.lorem.word()]: faker.internet.email(),
19
- };
15
+ const json = { [faker.lorem.word()]: faker.lorem.sentence() };
20
16
 
21
17
  const session = createSession({
22
18
  flow_id: flow._id,
@@ -29,10 +25,77 @@ describe('sessions', () => {
29
25
  await getSessionsCollection().insertOne(session);
30
26
 
31
27
  const result = await findSessionOfIncomingCall(from, to);
32
- expect(result).toBeDefined();
28
+ expect(result).not.toBeNull();
33
29
  expect(result?.phone_numbers[0].phoneNumber).toEqual(from);
34
30
  expect(result?.flow_id).toEqual(flow._id);
35
- expect(result?.json).toEqual(json);
31
+ });
32
+
33
+ it('works with different phone number formats (with/without plus)', async () => {
34
+ const toInDB = '972500000000';
35
+ const fromInDB = '+972540000000';
36
+ const flow = createFlow();
37
+ const phoneNumberData = createPhoneNumber({ phone_number: toInDB, flow_id: flow._id });
38
+
39
+ const session = createSession({
40
+ flow_id: flow._id,
41
+ phone_numbers: [{ phoneNumber: fromInDB, gender: 'female', name: 'sara' }],
42
+ });
43
+
44
+ await getFlowsCollection().insertOne(flow);
45
+ await getPhoneNumbersCollection().insertOne(phoneNumberData);
46
+ await getSessionsCollection().insertOne(session);
47
+
48
+ // Search with from WITH plus (matches + in DB), to WITH plus (matches without + in DB)
49
+ const result = await findSessionOfIncomingCall('+972540000000', '+972500000000');
50
+ expect(result).not.toBeNull();
51
+ expect(result?.phone_numbers[0].phoneNumber).toEqual(fromInDB);
52
+ });
53
+
54
+ it('works when flow_id is string in phone_numbers and ObjectId in session', async () => {
55
+ const to = '+972501111111';
56
+ const from = '+972541111111';
57
+ const flowId = new ObjectId();
58
+
59
+ // Store flow_id as string in phone_numbers
60
+ const phoneNumberData = createPhoneNumber({
61
+ phone_number: to,
62
+ flow_id: flowId.toHexString() as any
63
+ });
64
+
65
+ const session = createSession({
66
+ flow_id: flowId, // stored as ObjectId
67
+ phone_numbers: [{ phoneNumber: from, gender: 'male', name: 'isaac' }],
68
+ });
69
+
70
+ await getPhoneNumbersCollection().insertOne(phoneNumberData);
71
+ await getSessionsCollection().insertOne(session);
72
+
73
+ const result = await findSessionOfIncomingCall(from, to);
74
+ expect(result).not.toBeNull();
75
+ expect(result?.flow_id?.toString()).toEqual(flowId.toHexString());
76
+ });
77
+
78
+ it('works when flow_id is string in session', async () => {
79
+ const to = '+972502222222';
80
+ const from = '+972542222222';
81
+ const flowId = new ObjectId();
82
+
83
+ const phoneNumberData = createPhoneNumber({
84
+ phone_number: to,
85
+ flow_id: flowId
86
+ });
87
+
88
+ const session = createSession({
89
+ flow_id: flowId.toHexString(), // stored as string
90
+ phone_numbers: [{ phoneNumber: from, gender: 'female', name: 'rivka' }],
91
+ });
92
+
93
+ await getPhoneNumbersCollection().insertOne(phoneNumberData);
94
+ await getSessionsCollection().insertOne(session);
95
+
96
+ const result = await findSessionOfIncomingCall(from, to);
97
+ expect(result).not.toBeNull();
98
+ expect(result?.flow_id).toEqual(flowId.toHexString());
36
99
  });
37
100
 
38
101
  it('returns null if receiver phone number is not found', async () => {
@@ -6,6 +6,34 @@ import type { Session } from './sessions.types';
6
6
  export const getSessionsCollection = (): Collection<Session> =>
7
7
  getDb().collection<Session>('sessions');
8
8
 
9
+ const buildPhoneCandidates = (phone: string): string[] => {
10
+ const raw = (phone ?? '').trim();
11
+ if (!raw) return [];
12
+
13
+ const noPlus = raw.startsWith('+') ? raw.slice(1) : raw;
14
+ const digitsOnly = raw.replace(/\D/g, '');
15
+ const digitsOnlyNoPlus = noPlus.replace(/\D/g, '');
16
+
17
+ return Array.from(new Set([raw, noPlus, digitsOnly, digitsOnlyNoPlus].filter(Boolean)));
18
+ };
19
+
20
+ const buildFlowIdCandidates = (flowId: unknown): Array<string | ObjectId> => {
21
+ if (!flowId) return [];
22
+
23
+ // Some sessions store flow_id as string, others as ObjectId.
24
+ if (flowId instanceof ObjectId) {
25
+ return [flowId, flowId.toString()];
26
+ }
27
+
28
+ const asString = String(flowId);
29
+ if (!asString) return [];
30
+
31
+ if (ObjectId.isValid(asString)) {
32
+ return [new ObjectId(asString), asString];
33
+ }
34
+ return [asString];
35
+ };
36
+
9
37
  export const findSessionById = (
10
38
  sessionId: ObjectId,
11
39
  clientId: string
@@ -14,15 +42,46 @@ export const findSessionById = (
14
42
  };
15
43
 
16
44
  export const findSessionOfIncomingCall = async (from: string, to: string) => {
17
- const receiverPhoneData = await getPhoneNumbersCollection().findOne({ phone_number: to });
45
+ const toCandidates = buildPhoneCandidates(to);
46
+ const fromCandidates = buildPhoneCandidates(from);
47
+
48
+ const receiverPhoneData = await getPhoneNumbersCollection().findOne({
49
+ phone_number: { $in: toCandidates },
50
+ });
18
51
  if (!receiverPhoneData) {
19
52
  return null;
20
53
  }
21
- const session = await getSessionsCollection().findOne({
22
- flow_id: new ObjectId(receiverPhoneData.flow_id),
23
- phone_numbers: {
24
- $elemMatch: { phoneNumber: from },
25
- },
26
- });
54
+
55
+ const flowIdCandidates = buildFlowIdCandidates(receiverPhoneData.flow_id);
56
+
57
+ const [session] = await getSessionsCollection()
58
+ .aggregate<WithId<Session>>([
59
+ {
60
+ $match: {
61
+ flow_id: { $in: flowIdCandidates },
62
+ phone_numbers: {
63
+ $elemMatch: { phoneNumber: { $in: fromCandidates } },
64
+ },
65
+ },
66
+ },
67
+ {
68
+ $addFields: {
69
+ phone_numbers: {
70
+ $filter: {
71
+ input: '$phone_numbers',
72
+ as: 'pn',
73
+ cond: { $in: ['$$pn.phoneNumber', fromCandidates] },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ { $limit: 1 },
79
+ ])
80
+ .toArray();
81
+
82
+ if (!session) {
83
+ console.info('No session found for incoming call');
84
+ return null;
85
+ }
27
86
  return session;
28
87
  };
@@ -1,13 +1,14 @@
1
- import { getDb } from '../index';
1
+ import {getDb} from '../index';
2
2
  import {
3
+ SimpleSubscriptionFilter,
3
4
  Subscription,
4
5
  SubscriptionDoc,
5
- SimpleSubscriptionFilter,
6
6
  SubscriptionFilter,
7
7
  SubscriptionQueryOptions,
8
8
  } from './subscriptions.types';
9
- import { ObjectId, Filter } from 'mongodb';
10
- import { buildSimpleQuery } from './subscriptions.getters.utils';
9
+ import {Filter, ObjectId} from 'mongodb';
10
+ import {buildSimpleQuery} from './subscriptions.getters.utils';
11
+ import {applyQueryOptions} from '../utils/query.utils';
11
12
 
12
13
  export const getSubscriptionsCollection = () => {
13
14
  return getDb().collection<Subscription>('subscriptions');
@@ -46,15 +47,12 @@ export const findSubscriptions = async (
46
47
  }
47
48
  }
48
49
 
49
- let queryBuilder = getSubscriptionsCollection().find(query);
50
- if (options?.sort) {
51
- queryBuilder = queryBuilder.sort(options.sort);
52
- }
53
- if (options?.limit) {
54
- queryBuilder = queryBuilder.limit(options.limit);
55
- }
50
+ const cursor = getSubscriptionsCollection().find(query);
51
+ return await applyQueryOptions(cursor, options).toArray();
52
+ };
56
53
 
57
- return await queryBuilder.toArray();
54
+ export const countSubscriptions = async (query: Filter<Subscription>): Promise<number> => {
55
+ return getSubscriptionsCollection().countDocuments(query);
58
56
  };
59
57
 
60
58
  export const createSubscriptionDoc = async (
@@ -131,17 +129,15 @@ export const pushNewSubscriptionCycle = async (
131
129
  subscriptionId: ObjectId,
132
130
  newCycle: Subscription['cycles'][number]
133
131
  ): Promise<SubscriptionDoc | null> => {
134
- const result = await getSubscriptionsCollection().findOneAndUpdate(
135
- { _id: subscriptionId },
136
- {
137
- $push: {
138
- cycles: newCycle,
132
+ return await getSubscriptionsCollection().findOneAndUpdate(
133
+ {_id: subscriptionId},
134
+ {
135
+ $push: {
136
+ cycles: newCycle,
137
+ },
139
138
  },
140
- },
141
- {
142
- returnDocument: 'after',
143
- }
139
+ {
140
+ returnDocument: 'after',
141
+ }
144
142
  );
145
-
146
- return result;
147
143
  };
@@ -0,0 +1,49 @@
1
+ import { applyQueryOptions } from '../query.utils';
2
+
3
+ describe('query.utils', () => {
4
+ describe('applyQueryOptions()', () => {
5
+ let mockCursor: any;
6
+
7
+ beforeEach(() => {
8
+ mockCursor = {
9
+ sort: jest.fn().mockReturnThis(),
10
+ skip: jest.fn().mockReturnThis(),
11
+ limit: jest.fn().mockReturnThis(),
12
+ };
13
+ });
14
+
15
+ it('should return cursor if no options provided', () => {
16
+ const result = applyQueryOptions(mockCursor);
17
+ expect(result).toBe(mockCursor);
18
+ expect(mockCursor.sort).not.toHaveBeenCalled();
19
+ });
20
+
21
+ it('should apply sort, skip, and limit', () => {
22
+ const options = {
23
+ sort: { name: 1 },
24
+ skip: 10,
25
+ limit: 5,
26
+ };
27
+
28
+ const result = applyQueryOptions(mockCursor, options);
29
+
30
+ expect(result).toBe(mockCursor);
31
+ expect(mockCursor.sort).toHaveBeenCalledWith({ name: 1 });
32
+ expect(mockCursor.skip).toHaveBeenCalledWith(10);
33
+ expect(mockCursor.limit).toHaveBeenCalledWith(5);
34
+ });
35
+
36
+ it('should apply only sort if skip and limit are missing', () => {
37
+ const options = {
38
+ sort: { name: 1 },
39
+ };
40
+
41
+ const result = applyQueryOptions(mockCursor, options);
42
+
43
+ expect(result).toBe(mockCursor);
44
+ expect(mockCursor.sort).toHaveBeenCalledWith({ name: 1 });
45
+ expect(mockCursor.skip).not.toHaveBeenCalled();
46
+ expect(mockCursor.limit).not.toHaveBeenCalled();
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,21 @@
1
+ import { FindCursor } from 'mongodb';
2
+
3
+ export type QueryOptions = {
4
+ sort?: any;
5
+ skip?: number;
6
+ limit?: number;
7
+ };
8
+
9
+ export const applyQueryOptions = <T>(
10
+ cursor: FindCursor<T>,
11
+ options?: QueryOptions
12
+ ): FindCursor<T> => {
13
+ if (!options) return cursor;
14
+
15
+ let result = cursor;
16
+ if (options.sort) result = result.sort(options.sort);
17
+ if (options.skip) result = result.skip(options.skip);
18
+ if (options.limit) result = result.limit(options.limit);
19
+
20
+ return result;
21
+ };