@talkpilot/core-db 1.0.4 → 1.0.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.
Files changed (77) 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/clientAudioBuffers/index.d.ts +3 -0
  10. package/dist/talkpilot/clientAudioBuffers/index.d.ts.map +1 -0
  11. package/dist/talkpilot/clientAudioBuffers/index.js +19 -0
  12. package/dist/talkpilot/clientAudioBuffers/index.js.map +1 -0
  13. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts +1 -1
  14. package/dist/talkpilot/clientsConfig/clientsConfig.getters.d.ts.map +1 -1
  15. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js +5 -2
  16. package/dist/talkpilot/clientsConfig/clientsConfig.getters.js.map +1 -1
  17. package/dist/talkpilot/flows/flows.types.d.ts +4 -0
  18. package/dist/talkpilot/flows/flows.types.d.ts.map +1 -1
  19. package/dist/talkpilot/groups/index.d.ts +1 -0
  20. package/dist/talkpilot/groups/index.d.ts.map +1 -1
  21. package/dist/talkpilot/groups/index.js +1 -0
  22. package/dist/talkpilot/groups/index.js.map +1 -1
  23. package/dist/talkpilot/index.d.ts +1 -1
  24. package/dist/talkpilot/index.d.ts.map +1 -1
  25. package/dist/talkpilot/index.js +1 -1
  26. package/dist/talkpilot/index.js.map +1 -1
  27. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts +6 -3
  28. package/dist/talkpilot/phone_numbers/phone_numbers.getter.d.ts.map +1 -1
  29. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js +35 -19
  30. package/dist/talkpilot/phone_numbers/phone_numbers.getter.js.map +1 -1
  31. package/dist/talkpilot/plans/plans.getters.d.ts +4 -2
  32. package/dist/talkpilot/plans/plans.getters.d.ts.map +1 -1
  33. package/dist/talkpilot/plans/plans.getters.js +14 -11
  34. package/dist/talkpilot/plans/plans.getters.js.map +1 -1
  35. package/dist/talkpilot/plans/plans.types.d.ts +1 -0
  36. package/dist/talkpilot/plans/plans.types.d.ts.map +1 -1
  37. package/dist/talkpilot/results/results.types.d.ts +2 -0
  38. package/dist/talkpilot/results/results.types.d.ts.map +1 -1
  39. package/dist/talkpilot/sessions/index.d.ts +2 -2
  40. package/dist/talkpilot/sessions/index.d.ts.map +1 -1
  41. package/dist/talkpilot/sessions/index.js +16 -3
  42. package/dist/talkpilot/sessions/index.js.map +1 -1
  43. package/dist/talkpilot/sessions/sessions.getter.d.ts.map +1 -1
  44. package/dist/talkpilot/sessions/sessions.getter.js +57 -6
  45. package/dist/talkpilot/sessions/sessions.getter.js.map +1 -1
  46. package/dist/talkpilot/subscriptions/subscriptions.getters.d.ts +2 -1
  47. package/dist/talkpilot/subscriptions/subscriptions.getters.d.ts.map +1 -1
  48. package/dist/talkpilot/subscriptions/subscriptions.getters.js +9 -11
  49. package/dist/talkpilot/subscriptions/subscriptions.getters.js.map +1 -1
  50. package/dist/talkpilot/subscriptions/subscriptions.types.d.ts +1 -0
  51. package/dist/talkpilot/subscriptions/subscriptions.types.d.ts.map +1 -1
  52. package/dist/talkpilot/utils/query.utils.d.ts +8 -0
  53. package/dist/talkpilot/utils/query.utils.d.ts.map +1 -0
  54. package/dist/talkpilot/utils/query.utils.js +17 -0
  55. package/dist/talkpilot/utils/query.utils.js.map +1 -0
  56. package/package.json +2 -2
  57. package/src/talkpilot/calls/__tests__/calls.spec.ts +58 -0
  58. package/src/talkpilot/calls/calls.getters.ts +29 -2
  59. package/src/talkpilot/calls/calls.types.ts +38 -1
  60. package/src/talkpilot/clientAudioBuffers/index.ts +2 -0
  61. package/src/talkpilot/clientsConfig/clientsConfig.getters.ts +6 -6
  62. package/src/talkpilot/flows/flows.types.ts +6 -0
  63. package/src/talkpilot/groups/index.ts +1 -0
  64. package/src/talkpilot/index.ts +1 -1
  65. package/src/talkpilot/phone_numbers/__tests__/phone_numbers.spec.ts +57 -1
  66. package/src/talkpilot/phone_numbers/phone_numbers.getter.ts +49 -18
  67. package/src/talkpilot/plans/__tests__/plans.spec.ts +66 -0
  68. package/src/talkpilot/plans/plans.getters.ts +26 -22
  69. package/src/talkpilot/plans/plans.types.ts +1 -0
  70. package/src/talkpilot/results/results.types.ts +2 -0
  71. package/src/talkpilot/sessions/__tests__/sessions.spec.ts +73 -10
  72. package/src/talkpilot/sessions/index.ts +2 -2
  73. package/src/talkpilot/sessions/sessions.getter.ts +66 -7
  74. package/src/talkpilot/subscriptions/subscriptions.getters.ts +19 -23
  75. package/src/talkpilot/subscriptions/subscriptions.types.ts +1 -0
  76. package/src/talkpilot/utils/__tests__/query.utils.spec.ts +49 -0
  77. package/src/talkpilot/utils/query.utils.ts +21 -0
@@ -1,7 +1,13 @@
1
1
  import type { Collection } from 'mongodb';
2
- import { getDb, ObjectId } from '../index';
3
- import { findFlowById } from '../flows/flows.getter';
4
- import type { PhoneNumber, PhoneNumberWithFlow } from './phone_numbers.types';
2
+ import {
3
+ Client,
4
+ getClientsCollection,
5
+ getDb,
6
+ getFlowsCollection,
7
+ ObjectId,
8
+ findSubscriptions,
9
+ } from '../index';
10
+ import { PhoneNumber, PhoneNumberWithFlow } from './phone_numbers.types';
5
11
 
6
12
  export const getPhoneNumbersCollection = (): Collection<PhoneNumber> =>
7
13
  getDb().collection<PhoneNumber>('phone_numbers');
@@ -9,30 +15,46 @@ export const getPhoneNumbersCollection = (): Collection<PhoneNumber> =>
9
15
  export const getPhoneDataByPhoneNumber = async (
10
16
  phoneNumber: string
11
17
  ): Promise<PhoneNumberWithFlow | null> => {
12
- const phoneData = await getPhoneNumbersCollection().findOne({ phone_number: phoneNumber });
13
- if (!phoneData) return null;
14
- const flow = await findFlowById(phoneData.flow_id, phoneData.client_id);
15
- if (!flow) return null;
16
- return { ...phoneData, flow };
18
+ const phoneCallData = await getPhoneNumbersCollection().findOne({ phone_number: phoneNumber });
19
+ if (!phoneCallData) {
20
+ throw new Error('PhoneNumber not found');
21
+ }
22
+ const flow = await getFlowsCollection().findOne({ _id: new ObjectId(phoneCallData.flow_id) });
23
+ if (!flow) {
24
+ throw new Error('Flow not found');
25
+ }
26
+ const [subscription] =
27
+ (await findSubscriptions({ clientId: phoneCallData.client_id, isActive: true })) || [];
28
+
29
+ return { ...phoneCallData, flow, subscriptionId: subscription?._id?.toString() ?? undefined };
17
30
  };
18
31
 
19
- export const getClientPhoneNumber = async (clientId: string): Promise<string | null> => {
20
- return (await getPhoneNumbersCollection().findOne({ client_id: clientId }))?.phone_number ?? null;
32
+ export const getClientPrimaryPhoneNumber = async (clientId: string): Promise<string | null> => {
33
+ return (
34
+ (await getPhoneNumbersCollection().findOne({ client_id: clientId, is_primary: true }))
35
+ ?.phone_number ?? null
36
+ );
21
37
  };
22
38
 
23
- export const getClientPhoneData = async (clientId: string): Promise<PhoneNumber | null> => {
24
- const phoneNumberData = await getPhoneNumbersCollection().findOne({
25
- client_id: clientId,
26
- });
27
- return phoneNumberData;
39
+ export const getClientPhoneNumber = getClientPrimaryPhoneNumber;
40
+
41
+ export const getClientPhoneData = async (
42
+ clientId: string,
43
+ isPrimary?: boolean
44
+ ): Promise<PhoneNumber | null> => {
45
+ const filter = { client_id: clientId, is_primary: isPrimary !== false };
46
+ const options = isPrimary === false ? { sort: { createdAt: -1 } as const } : {};
47
+ return getPhoneNumbersCollection().findOne(filter, options);
28
48
  };
29
49
 
30
50
  export const createPhoneNumberEntity = async (
31
51
  phoneNumber: string,
32
52
  flowId: string,
33
- clientId: string,
34
- isPrimary = true,
53
+ clientId: string
35
54
  ): Promise<PhoneNumber> => {
55
+ const existing = await getClientPhoneData(clientId);
56
+ const isPrimary = !existing;
57
+
36
58
  await getPhoneNumbersCollection().insertOne({
37
59
  phone_number: phoneNumber,
38
60
  flow_id: new ObjectId(flowId),
@@ -41,7 +63,16 @@ export const createPhoneNumberEntity = async (
41
63
  createdAt: new Date(),
42
64
  updatedAt: new Date(),
43
65
  });
44
- const phoneNumberData = await getClientPhoneData(clientId);
66
+ const phoneNumberData = await getClientPhoneData(clientId, isPrimary);
45
67
  if (!phoneNumberData) throw new Error('Failed to create phoneNumber');
46
68
  return phoneNumberData;
47
69
  };
70
+
71
+ export const findClientByPhoneNumber = async (phoneNumber: string): Promise<Client> => {
72
+ const phoneData = await getPhoneNumbersCollection().findOne({ phone_number: phoneNumber });
73
+ if (!phoneData) throw new Error('Failed to get phone data');
74
+ const clientId = phoneData.client_id;
75
+ const client = await getClientsCollection().findOne({ clientId });
76
+ if (!client) throw new Error('Failed to get client');
77
+ return client;
78
+ };
@@ -0,0 +1,66 @@
1
+ import {
2
+ createPlanDoc,
3
+ findPlans,
4
+ findPlansByQuery,
5
+ countPlans,
6
+ updatePlanDoc,
7
+ getPlansCollection,
8
+ } from '../plans.getters';
9
+ import { ObjectId } from 'mongodb';
10
+ import { Plan } from '../plans.types';
11
+
12
+ describe('db.plans', () => {
13
+ const createTestPlan = (overrides?: Partial<Plan>): Omit<Plan, 'createdAt' | 'updatedAt'> => ({
14
+ productKey: 'basic-500',
15
+ name: 'Basic 500',
16
+ monthlyCallQuota: 500,
17
+ currency: 'ILS',
18
+ priceMonthlyMinor: 10000,
19
+ isActive: true,
20
+ ...overrides,
21
+ });
22
+
23
+ it('should create and find a plan', async () => {
24
+ const planData = createTestPlan({ name: 'CreateTest' });
25
+ const created = await createPlanDoc(planData);
26
+
27
+ expect(created._id).toBeDefined();
28
+ expect(created.name).toBe('CreateTest');
29
+
30
+ const found = await findPlans({ _id: created._id });
31
+ expect(found.length).toBe(1);
32
+ expect(found[0].name).toBe('CreateTest');
33
+ });
34
+
35
+ it('should find plans by query with limit', async () => {
36
+ await getPlansCollection().deleteMany({});
37
+ await createPlanDoc(createTestPlan({ productKey: 'p1', isActive: true }));
38
+ await createPlanDoc(createTestPlan({ productKey: 'p2', isActive: true }));
39
+ await createPlanDoc(createTestPlan({ productKey: 'p3', isActive: false }));
40
+
41
+ const activePlans = await findPlansByQuery({ isActive: true }, { limit: 1 });
42
+ expect(activePlans.length).toBe(1);
43
+ expect(activePlans[0].isActive).toBe(true);
44
+ });
45
+
46
+ it('should count plans matching query', async () => {
47
+ await getPlansCollection().deleteMany({});
48
+ await createPlanDoc(createTestPlan({ currency: 'USD' }));
49
+ await createPlanDoc(createTestPlan({ currency: 'USD' }));
50
+ await createPlanDoc(createTestPlan({ currency: 'ILS' }));
51
+
52
+ const count = await countPlans({ currency: 'USD' });
53
+ expect(count).toBe(2);
54
+ });
55
+
56
+ it('should update a plan', async () => {
57
+ const plan = await createPlanDoc(createTestPlan({ isActive: true }));
58
+ const updated = await updatePlanDoc(plan._id, { isActive: false });
59
+
60
+ expect(updated).not.toBeNull();
61
+ expect(updated?.isActive).toBe(false);
62
+
63
+ const found = await findPlans({ _id: plan._id });
64
+ expect(found[0].isActive).toBe(false);
65
+ });
66
+ });
@@ -1,5 +1,6 @@
1
- import { Plan, getDb, PlanDoc, PlanFilter, SimplePlanFilter, PlanQueryOptions } from '../index';
2
- import { ObjectId, Filter } from 'mongodb';
1
+ import {getDb, Plan, PlanDoc, PlanFilter, PlanQueryOptions, SimplePlanFilter} from '../index';
2
+ import {Filter, ObjectId} from 'mongodb';
3
+ import {applyQueryOptions} from '../utils/query.utils';
3
4
 
4
5
  export const getPlansCollection = () => {
5
6
  return getDb().collection<Plan>('plans');
@@ -74,16 +75,20 @@ export const findPlans = async (
74
75
  }
75
76
  }
76
77
 
77
- let queryBuilder = getPlansCollection().find(query);
78
+ const cursor = getPlansCollection().find(query);
79
+ return await applyQueryOptions(cursor, options).toArray();
80
+ };
78
81
 
79
- if (options?.sort) {
80
- queryBuilder = queryBuilder.sort(options.sort);
81
- }
82
- if (options?.limit) {
83
- queryBuilder = queryBuilder.limit(options.limit);
84
- }
82
+ export const findPlansByQuery = async (
83
+ query: Filter<Plan>,
84
+ options?: PlanQueryOptions
85
+ ): Promise<PlanDoc[]> => {
86
+ const cursor = getPlansCollection().find(query);
87
+ return await applyQueryOptions(cursor, options).toArray();
88
+ };
85
89
 
86
- return await queryBuilder.toArray();
90
+ export const countPlans = async (query: Filter<Plan>): Promise<number> => {
91
+ return getPlansCollection().countDocuments(query);
87
92
  };
88
93
 
89
94
  export const createPlanDoc = async (
@@ -103,19 +108,18 @@ export const createPlanDoc = async (
103
108
 
104
109
  export const updatePlanDoc = async (
105
110
  planId: ObjectId,
106
- updates: Partial<Pick<Plan, 'isActive'>>
111
+ updates: Partial<Omit<Plan, 'createdAt' | 'updatedAt'>>
107
112
  ): Promise<PlanDoc | null> => {
108
- const result = await getPlansCollection().findOneAndUpdate(
109
- { _id: planId },
110
- {
111
- $set: {
112
- ...updates,
113
- updatedAt: new Date(),
113
+ return await getPlansCollection().findOneAndUpdate(
114
+ {_id: planId},
115
+ {
116
+ $set: {
117
+ ...updates,
118
+ updatedAt: new Date(),
119
+ },
114
120
  },
115
- },
116
- {
117
- returnDocument: 'after',
118
- }
121
+ {
122
+ returnDocument: 'after',
123
+ }
119
124
  );
120
- return result;
121
125
  };
@@ -76,6 +76,7 @@ export type PlanFilter = SimplePlanFilter & {
76
76
 
77
77
  export type PlanQueryOptions = {
78
78
  sort?: Sort;
79
+ skip?: number;
79
80
  limit?: number;
80
81
  };
81
82
 
@@ -25,6 +25,8 @@ export interface CallResult {
25
25
  transcription: TranscriptionSegment[];
26
26
  flowId: ObjectId | string;
27
27
  recordingUrl: string | null;
28
+ agentHungUp?: boolean;
29
+ endReason?: string;
28
30
  }
29
31
 
30
32
  export type TranscriptionSegment =
@@ -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 () => {
@@ -1,2 +1,2 @@
1
- export { getSessionsCollection } from './sessions.getter';
2
- export type { Session } from './sessions.types';
1
+ export * from './sessions.getter';
2
+ export * from './sessions.types';
@@ -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
  };
@@ -61,5 +61,6 @@ export type SubscriptionFilter = SimpleSubscriptionFilter & {
61
61
  */
62
62
  export type SubscriptionQueryOptions = {
63
63
  sort?: Sort;
64
+ skip?: number;
64
65
  limit?: number;
65
66
  };
@@ -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
+ };