tychat-contracts 1.6.21 → 1.6.24
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/README.md +33 -33
- package/dist/storage/delete-object-rpc.dto.d.ts +9 -0
- package/dist/storage/delete-object-rpc.dto.d.ts.map +1 -0
- package/dist/storage/delete-object-rpc.dto.js +48 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/storage-rmq-patterns.d.ts +2 -0
- package/dist/storage/storage-rmq-patterns.d.ts.map +1 -1
- package/dist/storage/storage-rmq-patterns.js +3 -1
- package/jest.config.ts +5 -5
- package/package.json +2 -1
- package/src/ai/ai-usage-response.dto.ts +47 -47
- package/src/ai/create-ai-usage.dto.ts +43 -43
- package/src/analytics/analytics-emitter.helper.ts +54 -54
- package/src/analytics/analytics-query.dto.ts +222 -222
- package/src/analytics/create-analytic-event.dto.ts +85 -85
- package/src/analytics/event-analytic.enum.ts +119 -119
- package/src/appointments/appointment-procedure.dto.ts +76 -76
- package/src/appointments/create-appointment.dto.ts +153 -153
- package/src/appointments/index.ts +10 -10
- package/src/appointments/list-appointments-query.dto.ts +157 -157
- package/src/appointments/update-appointment-procedure.dto.ts +10 -10
- package/src/appointments/update-appointment.dto.ts +4 -4
- package/src/auth/refresh-token.dto.ts +10 -10
- package/src/billing/billing-response.dto.ts +68 -68
- package/src/billing/create-billing.dto.ts +70 -70
- package/src/billing/index.ts +6 -6
- package/src/billing/list-billings-query.dto.ts +61 -61
- package/src/billing/update-billing.dto.ts +4 -4
- package/src/cashback/cashback-balance-response.dto.ts +33 -33
- package/src/cashback/cashback-config-response.dto.ts +43 -43
- package/src/cashback/cashback-enums.ts +33 -33
- package/src/cashback/cashback-kafka-payloads.ts +102 -102
- package/src/cashback/cashback-kafka-topics.ts +23 -23
- package/src/cashback/index.ts +7 -7
- package/src/cashback/referral-code-response.dto.ts +49 -49
- package/src/cashback/use-referral-code-response.dto.ts +15 -15
- package/src/configurations/clinic-configuration.dto.ts +39 -39
- package/src/configurations/opening-hours-slot.dto.ts +41 -41
- package/src/configurations/update-clinic-configuration.dto.ts +39 -39
- package/src/connections/whatsapp-official.dto.ts +113 -113
- package/src/conversations/conversation-contact-filters-query.validator.spec.ts +221 -221
- package/src/conversations/conversation-contact-filters-query.validator.ts +10 -10
- package/src/conversations/conversation-intention.dto.ts +24 -24
- package/src/conversations/conversation-response.dto.ts +69 -69
- package/src/conversations/conversation-session-response.dto.ts +65 -65
- package/src/conversations/conversation-type.dto.ts +15 -15
- package/src/conversations/create-conversation-session.dto.ts +28 -28
- package/src/conversations/create-conversation.dto.ts +69 -69
- package/src/conversations/followup-config-response.dto.ts +290 -290
- package/src/conversations/followup-log-response.dto.ts +58 -58
- package/src/conversations/followup-type.dto.ts +22 -22
- package/src/conversations/index.ts +17 -17
- package/src/conversations/list-conversation-contacts-filters.dto.ts +15 -15
- package/src/conversations/list-conversation-contacts-query.dto.ts +81 -81
- package/src/conversations/satisfaction-response.dto.ts +116 -116
- package/src/filters/index.ts +1 -1
- package/src/filters/parsed-filter.dto.ts +41 -41
- package/src/index.ts +23 -23
- package/src/patients/create-patient.dto.ts +122 -122
- package/src/patients/index.ts +10 -10
- package/src/patients/list-patients-filters.dto.ts +15 -15
- package/src/patients/list-patients-query.dto.ts +61 -61
- package/src/patients/patient-document-response.dto.ts +33 -33
- package/src/patients/patient-history-entry.dto.ts +22 -22
- package/src/patients/patient-status.dto.ts +9 -9
- package/src/patients/update-patient-document.dto.ts +10 -10
- package/src/patients/update-patient.dto.ts +15 -15
- package/src/payment-gateway/create-gateway-payment.dto.ts +123 -123
- package/src/payment-gateway/gateway-payment-response.dto.ts +65 -65
- package/src/payment-gateway/index.ts +12 -12
- package/src/payment-gateway/list-gateway-payments-query.dto.ts +63 -63
- package/src/payment-gateway/payment-gateway-kafka-topics.ts +25 -25
- package/src/payment-gateway/payment-gateway.enums.ts +34 -34
- package/src/payment-gateway/update-gateway-payment.dto.ts +7 -7
- package/src/payment-gateway/webhook-payload.dto.ts +32 -32
- package/src/payments/create-payment.dto.ts +73 -73
- package/src/payments/index.ts +6 -6
- package/src/payments/list-payments-query.dto.ts +44 -44
- package/src/payments/payment-response.dto.ts +67 -67
- package/src/payments/update-payment.dto.ts +4 -4
- package/src/procedures/create-procedure.dto.ts +104 -104
- package/src/procedures/index.ts +6 -6
- package/src/procedures/list-procedures-query.dto.ts +59 -59
- package/src/procedures/update-procedure.dto.ts +4 -4
- package/src/storage/delete-object-rpc.dto.ts +28 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/storage-rmq-patterns.ts +3 -0
- package/src/tenants/index.ts +10 -10
- package/src/tenants/whatsapp-provider-kind.dto.ts +12 -12
- package/dist/analytics/should-skip-analytics-tenant-lookup.d.ts +0 -7
- package/dist/analytics/should-skip-analytics-tenant-lookup.d.ts.map +0 -1
- package/dist/analytics/should-skip-analytics-tenant-lookup.js +0 -13
- package/dist/appointments/opening-hours-slot.dto.d.ts +0 -6
- package/dist/appointments/opening-hours-slot.dto.d.ts.map +0 -1
- package/dist/appointments/opening-hours-slot.dto.js +0 -2
- package/dist/patients/patient-procedure.dto.d.ts +0 -9
- package/dist/patients/patient-procedure.dto.d.ts.map +0 -1
- package/dist/patients/patient-procedure.dto.js +0 -79
- package/dist/patients/update-patient-procedure.dto.d.ts +0 -6
- package/dist/patients/update-patient-procedure.dto.d.ts.map +0 -1
- package/dist/patients/update-patient-procedure.dto.js +0 -8
- package/dist/payments/payment.dto.d.ts +0 -15
- package/dist/payments/payment.dto.d.ts.map +0 -1
- package/dist/payments/payment.dto.js +0 -2
- package/dist/tenants/create-tenant.dto.d.ts +0 -10
- package/dist/tenants/create-tenant.dto.d.ts.map +0 -1
- package/dist/tenants/create-tenant.dto.js +0 -73
- package/dist/tenants/tenant-slug.util.spec.d.ts +0 -2
- package/dist/tenants/tenant-slug.util.spec.d.ts.map +0 -1
- package/dist/tenants/tenant-slug.util.spec.js +0 -102
- package/dist/tenants/update-tenant-payload.dto.d.ts +0 -6
- package/dist/tenants/update-tenant-payload.dto.d.ts.map +0 -1
- package/dist/tenants/update-tenant-payload.dto.js +0 -25
- package/dist/tenants/update-tenant.dto.d.ts +0 -10
- package/dist/tenants/update-tenant.dto.d.ts.map +0 -1
- package/dist/tenants/update-tenant.dto.js +0 -78
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
-
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Payload to register a WhatsApp Official (Meta Cloud API) instance for a tenant.
|
|
6
|
-
* Requires the access token, phone number ID, and WhatsApp Business Account ID
|
|
7
|
-
* obtained from the Meta developer portal.
|
|
8
|
-
*/
|
|
9
|
-
export class CreateWhatsAppOfficialInstanceDto {
|
|
10
|
-
@ApiProperty({
|
|
11
|
-
description: 'Meta Cloud API permanent access token',
|
|
12
|
-
example: 'EAABs...',
|
|
13
|
-
})
|
|
14
|
-
@IsString()
|
|
15
|
-
@IsNotEmpty()
|
|
16
|
-
accessToken: string;
|
|
17
|
-
|
|
18
|
-
@ApiProperty({
|
|
19
|
-
description: 'Phone Number ID from Meta Business Manager',
|
|
20
|
-
example: '106540352242922',
|
|
21
|
-
})
|
|
22
|
-
@IsString()
|
|
23
|
-
@IsNotEmpty()
|
|
24
|
-
phoneNumberId: string;
|
|
25
|
-
|
|
26
|
-
@ApiProperty({
|
|
27
|
-
description: 'WhatsApp Business Account ID (WABA ID)',
|
|
28
|
-
example: '102291529438901',
|
|
29
|
-
})
|
|
30
|
-
@IsString()
|
|
31
|
-
@IsNotEmpty()
|
|
32
|
-
wabaId: string;
|
|
33
|
-
|
|
34
|
-
@ApiPropertyOptional({
|
|
35
|
-
description: 'Webhook verify token used to validate Meta webhook registration',
|
|
36
|
-
example: 'my-verify-token',
|
|
37
|
-
})
|
|
38
|
-
@IsOptional()
|
|
39
|
-
@IsString()
|
|
40
|
-
webhookVerifyToken?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Payload to send a pre-approved template message via WhatsApp Official (Meta Cloud API).
|
|
45
|
-
* Templates must be created and approved in the Meta Business Manager before use.
|
|
46
|
-
*/
|
|
47
|
-
export class SendWhatsAppOfficialTemplateDto {
|
|
48
|
-
@ApiProperty({
|
|
49
|
-
description: 'Recipient phone number with country code (digits only)',
|
|
50
|
-
example: '5511999999999',
|
|
51
|
-
})
|
|
52
|
-
@IsString()
|
|
53
|
-
@IsNotEmpty()
|
|
54
|
-
to: string;
|
|
55
|
-
|
|
56
|
-
@ApiProperty({
|
|
57
|
-
description: 'Approved template name in Meta Business Manager',
|
|
58
|
-
example: 'appointment_confirmation',
|
|
59
|
-
})
|
|
60
|
-
@IsString()
|
|
61
|
-
@IsNotEmpty()
|
|
62
|
-
templateName: string;
|
|
63
|
-
|
|
64
|
-
@ApiProperty({
|
|
65
|
-
description: 'Template language code (BCP 47)',
|
|
66
|
-
example: 'pt_BR',
|
|
67
|
-
})
|
|
68
|
-
@IsString()
|
|
69
|
-
@IsNotEmpty()
|
|
70
|
-
languageCode: string;
|
|
71
|
-
|
|
72
|
-
@ApiPropertyOptional({
|
|
73
|
-
description:
|
|
74
|
-
'Template body variable components (positional parameters). ' +
|
|
75
|
-
'Each element replaces {{1}}, {{2}}, etc. in the template body.',
|
|
76
|
-
example: ['João', '15/04/2026 às 10:00'],
|
|
77
|
-
type: [String],
|
|
78
|
-
})
|
|
79
|
-
@IsOptional()
|
|
80
|
-
@IsString({ each: true })
|
|
81
|
-
bodyParameters?: string[];
|
|
82
|
-
|
|
83
|
-
@ApiPropertyOptional({
|
|
84
|
-
description: 'Template header variable components (positional parameters)',
|
|
85
|
-
example: ['Clínica Saúde'],
|
|
86
|
-
type: [String],
|
|
87
|
-
})
|
|
88
|
-
@IsOptional()
|
|
89
|
-
@IsString({ each: true })
|
|
90
|
-
headerParameters?: string[];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Payload to send a free-form text message via WhatsApp Official (Meta Cloud API).
|
|
95
|
-
* Only works within the 24-hour customer service window.
|
|
96
|
-
*/
|
|
97
|
-
export class SendWhatsAppOfficialTextDto {
|
|
98
|
-
@ApiProperty({
|
|
99
|
-
description: 'Recipient phone number with country code (digits only)',
|
|
100
|
-
example: '5511999999999',
|
|
101
|
-
})
|
|
102
|
-
@IsString()
|
|
103
|
-
@IsNotEmpty()
|
|
104
|
-
to: string;
|
|
105
|
-
|
|
106
|
-
@ApiProperty({
|
|
107
|
-
description: 'Plain text message body',
|
|
108
|
-
example: 'Olá! Tudo bem?',
|
|
109
|
-
})
|
|
110
|
-
@IsString()
|
|
111
|
-
@IsNotEmpty()
|
|
112
|
-
text: string;
|
|
113
|
-
}
|
|
1
|
+
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
|
2
|
+
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Payload to register a WhatsApp Official (Meta Cloud API) instance for a tenant.
|
|
6
|
+
* Requires the access token, phone number ID, and WhatsApp Business Account ID
|
|
7
|
+
* obtained from the Meta developer portal.
|
|
8
|
+
*/
|
|
9
|
+
export class CreateWhatsAppOfficialInstanceDto {
|
|
10
|
+
@ApiProperty({
|
|
11
|
+
description: 'Meta Cloud API permanent access token',
|
|
12
|
+
example: 'EAABs...',
|
|
13
|
+
})
|
|
14
|
+
@IsString()
|
|
15
|
+
@IsNotEmpty()
|
|
16
|
+
accessToken: string;
|
|
17
|
+
|
|
18
|
+
@ApiProperty({
|
|
19
|
+
description: 'Phone Number ID from Meta Business Manager',
|
|
20
|
+
example: '106540352242922',
|
|
21
|
+
})
|
|
22
|
+
@IsString()
|
|
23
|
+
@IsNotEmpty()
|
|
24
|
+
phoneNumberId: string;
|
|
25
|
+
|
|
26
|
+
@ApiProperty({
|
|
27
|
+
description: 'WhatsApp Business Account ID (WABA ID)',
|
|
28
|
+
example: '102291529438901',
|
|
29
|
+
})
|
|
30
|
+
@IsString()
|
|
31
|
+
@IsNotEmpty()
|
|
32
|
+
wabaId: string;
|
|
33
|
+
|
|
34
|
+
@ApiPropertyOptional({
|
|
35
|
+
description: 'Webhook verify token used to validate Meta webhook registration',
|
|
36
|
+
example: 'my-verify-token',
|
|
37
|
+
})
|
|
38
|
+
@IsOptional()
|
|
39
|
+
@IsString()
|
|
40
|
+
webhookVerifyToken?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Payload to send a pre-approved template message via WhatsApp Official (Meta Cloud API).
|
|
45
|
+
* Templates must be created and approved in the Meta Business Manager before use.
|
|
46
|
+
*/
|
|
47
|
+
export class SendWhatsAppOfficialTemplateDto {
|
|
48
|
+
@ApiProperty({
|
|
49
|
+
description: 'Recipient phone number with country code (digits only)',
|
|
50
|
+
example: '5511999999999',
|
|
51
|
+
})
|
|
52
|
+
@IsString()
|
|
53
|
+
@IsNotEmpty()
|
|
54
|
+
to: string;
|
|
55
|
+
|
|
56
|
+
@ApiProperty({
|
|
57
|
+
description: 'Approved template name in Meta Business Manager',
|
|
58
|
+
example: 'appointment_confirmation',
|
|
59
|
+
})
|
|
60
|
+
@IsString()
|
|
61
|
+
@IsNotEmpty()
|
|
62
|
+
templateName: string;
|
|
63
|
+
|
|
64
|
+
@ApiProperty({
|
|
65
|
+
description: 'Template language code (BCP 47)',
|
|
66
|
+
example: 'pt_BR',
|
|
67
|
+
})
|
|
68
|
+
@IsString()
|
|
69
|
+
@IsNotEmpty()
|
|
70
|
+
languageCode: string;
|
|
71
|
+
|
|
72
|
+
@ApiPropertyOptional({
|
|
73
|
+
description:
|
|
74
|
+
'Template body variable components (positional parameters). ' +
|
|
75
|
+
'Each element replaces {{1}}, {{2}}, etc. in the template body.',
|
|
76
|
+
example: ['João', '15/04/2026 às 10:00'],
|
|
77
|
+
type: [String],
|
|
78
|
+
})
|
|
79
|
+
@IsOptional()
|
|
80
|
+
@IsString({ each: true })
|
|
81
|
+
bodyParameters?: string[];
|
|
82
|
+
|
|
83
|
+
@ApiPropertyOptional({
|
|
84
|
+
description: 'Template header variable components (positional parameters)',
|
|
85
|
+
example: ['Clínica Saúde'],
|
|
86
|
+
type: [String],
|
|
87
|
+
})
|
|
88
|
+
@IsOptional()
|
|
89
|
+
@IsString({ each: true })
|
|
90
|
+
headerParameters?: string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Payload to send a free-form text message via WhatsApp Official (Meta Cloud API).
|
|
95
|
+
* Only works within the 24-hour customer service window.
|
|
96
|
+
*/
|
|
97
|
+
export class SendWhatsAppOfficialTextDto {
|
|
98
|
+
@ApiProperty({
|
|
99
|
+
description: 'Recipient phone number with country code (digits only)',
|
|
100
|
+
example: '5511999999999',
|
|
101
|
+
})
|
|
102
|
+
@IsString()
|
|
103
|
+
@IsNotEmpty()
|
|
104
|
+
to: string;
|
|
105
|
+
|
|
106
|
+
@ApiProperty({
|
|
107
|
+
description: 'Plain text message body',
|
|
108
|
+
example: 'Olá! Tudo bem?',
|
|
109
|
+
})
|
|
110
|
+
@IsString()
|
|
111
|
+
@IsNotEmpty()
|
|
112
|
+
text: string;
|
|
113
|
+
}
|
|
@@ -1,221 +1,221 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
|
-
import { plainToInstance } from 'class-transformer';
|
|
3
|
-
import { validate } from 'class-validator';
|
|
4
|
-
import { ListConversationContactsQueryDto } from './list-conversation-contacts-query.dto';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Simulates how NestJS ValidationPipe processes a raw query object
|
|
8
|
-
* when configured with transform: true and enableImplicitConversion: true.
|
|
9
|
-
*/
|
|
10
|
-
function simulateValidationPipe(
|
|
11
|
-
raw: Record<string, unknown>,
|
|
12
|
-
): ListConversationContactsQueryDto {
|
|
13
|
-
return plainToInstance(ListConversationContactsQueryDto, raw, {
|
|
14
|
-
enableImplicitConversion: true,
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('ConversationContactFiltersQueryConstraint', () => {
|
|
19
|
-
it('should accept a single JSON object string (URL-decoded query param)', async () => {
|
|
20
|
-
const raw = {
|
|
21
|
-
page: '1',
|
|
22
|
-
limit: '20',
|
|
23
|
-
filters: '{"key":"intention","op":"==","value":"clinic_info"}',
|
|
24
|
-
};
|
|
25
|
-
const dto = simulateValidationPipe(raw);
|
|
26
|
-
const errors = await validate(dto, {
|
|
27
|
-
whitelist: true,
|
|
28
|
-
forbidNonWhitelisted: true,
|
|
29
|
-
});
|
|
30
|
-
expect(errors).toHaveLength(0);
|
|
31
|
-
expect(dto.filters).toEqual([
|
|
32
|
-
expect.objectContaining({
|
|
33
|
-
key: 'intention',
|
|
34
|
-
op: '==',
|
|
35
|
-
value: 'clinic_info',
|
|
36
|
-
}),
|
|
37
|
-
]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should accept a JSON array string with one filter', async () => {
|
|
41
|
-
const raw = {
|
|
42
|
-
page: '1',
|
|
43
|
-
limit: '20',
|
|
44
|
-
filters: '[{"key":"intention","op":"==","value":"clinic_info"}]',
|
|
45
|
-
};
|
|
46
|
-
const dto = simulateValidationPipe(raw);
|
|
47
|
-
const errors = await validate(dto, {
|
|
48
|
-
whitelist: true,
|
|
49
|
-
forbidNonWhitelisted: true,
|
|
50
|
-
});
|
|
51
|
-
expect(errors).toHaveLength(0);
|
|
52
|
-
expect(dto.filters).toHaveLength(1);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should accept a JSON array string with multiple filters', async () => {
|
|
56
|
-
const raw = {
|
|
57
|
-
filters:
|
|
58
|
-
'[{"key":"intention","op":"==","value":"clinic_info"},{"key":"patientId","op":"!=","value":"abc"}]',
|
|
59
|
-
};
|
|
60
|
-
const dto = simulateValidationPipe(raw);
|
|
61
|
-
const errors = await validate(dto, {
|
|
62
|
-
whitelist: true,
|
|
63
|
-
forbidNonWhitelisted: true,
|
|
64
|
-
});
|
|
65
|
-
expect(errors).toHaveLength(0);
|
|
66
|
-
expect(dto.filters).toHaveLength(2);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should accept an array of JSON strings (e.g. filters[0]=...&filters[1]=...)', async () => {
|
|
70
|
-
const raw = {
|
|
71
|
-
filters: [
|
|
72
|
-
'{"key":"intention","op":"==","value":"clinic_info"}',
|
|
73
|
-
'{"key":"patientId","op":"!=","value":"xyz"}',
|
|
74
|
-
],
|
|
75
|
-
};
|
|
76
|
-
const dto = simulateValidationPipe(raw);
|
|
77
|
-
const errors = await validate(dto, {
|
|
78
|
-
whitelist: true,
|
|
79
|
-
forbidNonWhitelisted: true,
|
|
80
|
-
});
|
|
81
|
-
expect(errors).toHaveLength(0);
|
|
82
|
-
expect(dto.filters).toHaveLength(2);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should accept an array of plain objects (Kafka / programmatic usage)', async () => {
|
|
86
|
-
const raw = {
|
|
87
|
-
filters: [
|
|
88
|
-
{ key: 'intention', op: '==', value: 'clinic_info' },
|
|
89
|
-
],
|
|
90
|
-
};
|
|
91
|
-
const dto = simulateValidationPipe(raw);
|
|
92
|
-
const errors = await validate(dto, {
|
|
93
|
-
whitelist: true,
|
|
94
|
-
forbidNonWhitelisted: true,
|
|
95
|
-
});
|
|
96
|
-
expect(errors).toHaveLength(0);
|
|
97
|
-
expect(dto.filters).toHaveLength(1);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should accept a single plain object', async () => {
|
|
101
|
-
const raw = {
|
|
102
|
-
filters: { key: 'intention', op: '==', value: 'clinic_info' },
|
|
103
|
-
};
|
|
104
|
-
const dto = simulateValidationPipe(raw);
|
|
105
|
-
const errors = await validate(dto, {
|
|
106
|
-
whitelist: true,
|
|
107
|
-
forbidNonWhitelisted: true,
|
|
108
|
-
});
|
|
109
|
-
expect(errors).toHaveLength(0);
|
|
110
|
-
expect(dto.filters).toHaveLength(1);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('should accept undefined/null filters', async () => {
|
|
114
|
-
const dto = simulateValidationPipe({ page: '1', limit: '20' });
|
|
115
|
-
const errors = await validate(dto, {
|
|
116
|
-
whitelist: true,
|
|
117
|
-
forbidNonWhitelisted: true,
|
|
118
|
-
});
|
|
119
|
-
expect(errors).toHaveLength(0);
|
|
120
|
-
expect(dto.filters).toBeUndefined();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('should accept sortBy and sortOrder query params', async () => {
|
|
124
|
-
const dto = simulateValidationPipe({
|
|
125
|
-
page: '1',
|
|
126
|
-
limit: '20',
|
|
127
|
-
sortBy: 'startedAt',
|
|
128
|
-
sortOrder: 'desc',
|
|
129
|
-
});
|
|
130
|
-
const errors = await validate(dto, {
|
|
131
|
-
whitelist: true,
|
|
132
|
-
forbidNonWhitelisted: true,
|
|
133
|
-
});
|
|
134
|
-
expect(errors).toHaveLength(0);
|
|
135
|
-
expect(dto.sortBy).toBe('startedAt');
|
|
136
|
-
expect(dto.sortOrder).toBe('DESC');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should accept empty string filters', async () => {
|
|
140
|
-
const dto = simulateValidationPipe({ filters: '' });
|
|
141
|
-
const errors = await validate(dto, {
|
|
142
|
-
whitelist: true,
|
|
143
|
-
forbidNonWhitelisted: true,
|
|
144
|
-
});
|
|
145
|
-
expect(errors).toHaveLength(0);
|
|
146
|
-
expect(dto.filters).toBeUndefined();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should reject invalid JSON string', async () => {
|
|
150
|
-
const dto = simulateValidationPipe({ filters: '{bad json' });
|
|
151
|
-
const errors = await validate(dto, {
|
|
152
|
-
whitelist: true,
|
|
153
|
-
forbidNonWhitelisted: true,
|
|
154
|
-
});
|
|
155
|
-
expect(errors.length).toBeGreaterThan(0);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should reject a filter with invalid key', async () => {
|
|
159
|
-
const raw = {
|
|
160
|
-
filters: '{"key":"invalidField","op":"==","value":"x"}',
|
|
161
|
-
};
|
|
162
|
-
const dto = simulateValidationPipe(raw);
|
|
163
|
-
const errors = await validate(dto, {
|
|
164
|
-
whitelist: true,
|
|
165
|
-
forbidNonWhitelisted: true,
|
|
166
|
-
});
|
|
167
|
-
expect(errors.length).toBeGreaterThan(0);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should reject a filter with invalid operator', async () => {
|
|
171
|
-
const raw = {
|
|
172
|
-
filters: '{"key":"intention","op":"contains","value":"x"}',
|
|
173
|
-
};
|
|
174
|
-
const dto = simulateValidationPipe(raw);
|
|
175
|
-
const errors = await validate(dto, {
|
|
176
|
-
whitelist: true,
|
|
177
|
-
forbidNonWhitelisted: true,
|
|
178
|
-
});
|
|
179
|
-
expect(errors.length).toBeGreaterThan(0);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should accept op "in" with array value', async () => {
|
|
183
|
-
const raw = {
|
|
184
|
-
filters: '{"key":"intention","op":"in","value":["clinic_info","other"]}',
|
|
185
|
-
};
|
|
186
|
-
const dto = simulateValidationPipe(raw);
|
|
187
|
-
const errors = await validate(dto, {
|
|
188
|
-
whitelist: true,
|
|
189
|
-
forbidNonWhitelisted: true,
|
|
190
|
-
});
|
|
191
|
-
expect(errors).toHaveLength(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should reject op "in" with scalar value', async () => {
|
|
195
|
-
const raw = {
|
|
196
|
-
filters: '{"key":"intention","op":"in","value":"clinic_info"}',
|
|
197
|
-
};
|
|
198
|
-
const dto = simulateValidationPipe(raw);
|
|
199
|
-
const errors = await validate(dto, {
|
|
200
|
-
whitelist: true,
|
|
201
|
-
forbidNonWhitelisted: true,
|
|
202
|
-
});
|
|
203
|
-
expect(errors.length).toBeGreaterThan(0);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should handle double-encoded JSON string', async () => {
|
|
207
|
-
const inner = JSON.stringify({
|
|
208
|
-
key: 'intention',
|
|
209
|
-
op: '==',
|
|
210
|
-
value: 'clinic_info',
|
|
211
|
-
});
|
|
212
|
-
const raw = { filters: JSON.stringify(inner) };
|
|
213
|
-
const dto = simulateValidationPipe(raw);
|
|
214
|
-
const errors = await validate(dto, {
|
|
215
|
-
whitelist: true,
|
|
216
|
-
forbidNonWhitelisted: true,
|
|
217
|
-
});
|
|
218
|
-
expect(errors).toHaveLength(0);
|
|
219
|
-
expect(dto.filters).toHaveLength(1);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { plainToInstance } from 'class-transformer';
|
|
3
|
+
import { validate } from 'class-validator';
|
|
4
|
+
import { ListConversationContactsQueryDto } from './list-conversation-contacts-query.dto';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Simulates how NestJS ValidationPipe processes a raw query object
|
|
8
|
+
* when configured with transform: true and enableImplicitConversion: true.
|
|
9
|
+
*/
|
|
10
|
+
function simulateValidationPipe(
|
|
11
|
+
raw: Record<string, unknown>,
|
|
12
|
+
): ListConversationContactsQueryDto {
|
|
13
|
+
return plainToInstance(ListConversationContactsQueryDto, raw, {
|
|
14
|
+
enableImplicitConversion: true,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('ConversationContactFiltersQueryConstraint', () => {
|
|
19
|
+
it('should accept a single JSON object string (URL-decoded query param)', async () => {
|
|
20
|
+
const raw = {
|
|
21
|
+
page: '1',
|
|
22
|
+
limit: '20',
|
|
23
|
+
filters: '{"key":"intention","op":"==","value":"clinic_info"}',
|
|
24
|
+
};
|
|
25
|
+
const dto = simulateValidationPipe(raw);
|
|
26
|
+
const errors = await validate(dto, {
|
|
27
|
+
whitelist: true,
|
|
28
|
+
forbidNonWhitelisted: true,
|
|
29
|
+
});
|
|
30
|
+
expect(errors).toHaveLength(0);
|
|
31
|
+
expect(dto.filters).toEqual([
|
|
32
|
+
expect.objectContaining({
|
|
33
|
+
key: 'intention',
|
|
34
|
+
op: '==',
|
|
35
|
+
value: 'clinic_info',
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should accept a JSON array string with one filter', async () => {
|
|
41
|
+
const raw = {
|
|
42
|
+
page: '1',
|
|
43
|
+
limit: '20',
|
|
44
|
+
filters: '[{"key":"intention","op":"==","value":"clinic_info"}]',
|
|
45
|
+
};
|
|
46
|
+
const dto = simulateValidationPipe(raw);
|
|
47
|
+
const errors = await validate(dto, {
|
|
48
|
+
whitelist: true,
|
|
49
|
+
forbidNonWhitelisted: true,
|
|
50
|
+
});
|
|
51
|
+
expect(errors).toHaveLength(0);
|
|
52
|
+
expect(dto.filters).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should accept a JSON array string with multiple filters', async () => {
|
|
56
|
+
const raw = {
|
|
57
|
+
filters:
|
|
58
|
+
'[{"key":"intention","op":"==","value":"clinic_info"},{"key":"patientId","op":"!=","value":"abc"}]',
|
|
59
|
+
};
|
|
60
|
+
const dto = simulateValidationPipe(raw);
|
|
61
|
+
const errors = await validate(dto, {
|
|
62
|
+
whitelist: true,
|
|
63
|
+
forbidNonWhitelisted: true,
|
|
64
|
+
});
|
|
65
|
+
expect(errors).toHaveLength(0);
|
|
66
|
+
expect(dto.filters).toHaveLength(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should accept an array of JSON strings (e.g. filters[0]=...&filters[1]=...)', async () => {
|
|
70
|
+
const raw = {
|
|
71
|
+
filters: [
|
|
72
|
+
'{"key":"intention","op":"==","value":"clinic_info"}',
|
|
73
|
+
'{"key":"patientId","op":"!=","value":"xyz"}',
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
const dto = simulateValidationPipe(raw);
|
|
77
|
+
const errors = await validate(dto, {
|
|
78
|
+
whitelist: true,
|
|
79
|
+
forbidNonWhitelisted: true,
|
|
80
|
+
});
|
|
81
|
+
expect(errors).toHaveLength(0);
|
|
82
|
+
expect(dto.filters).toHaveLength(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should accept an array of plain objects (Kafka / programmatic usage)', async () => {
|
|
86
|
+
const raw = {
|
|
87
|
+
filters: [
|
|
88
|
+
{ key: 'intention', op: '==', value: 'clinic_info' },
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
const dto = simulateValidationPipe(raw);
|
|
92
|
+
const errors = await validate(dto, {
|
|
93
|
+
whitelist: true,
|
|
94
|
+
forbidNonWhitelisted: true,
|
|
95
|
+
});
|
|
96
|
+
expect(errors).toHaveLength(0);
|
|
97
|
+
expect(dto.filters).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should accept a single plain object', async () => {
|
|
101
|
+
const raw = {
|
|
102
|
+
filters: { key: 'intention', op: '==', value: 'clinic_info' },
|
|
103
|
+
};
|
|
104
|
+
const dto = simulateValidationPipe(raw);
|
|
105
|
+
const errors = await validate(dto, {
|
|
106
|
+
whitelist: true,
|
|
107
|
+
forbidNonWhitelisted: true,
|
|
108
|
+
});
|
|
109
|
+
expect(errors).toHaveLength(0);
|
|
110
|
+
expect(dto.filters).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should accept undefined/null filters', async () => {
|
|
114
|
+
const dto = simulateValidationPipe({ page: '1', limit: '20' });
|
|
115
|
+
const errors = await validate(dto, {
|
|
116
|
+
whitelist: true,
|
|
117
|
+
forbidNonWhitelisted: true,
|
|
118
|
+
});
|
|
119
|
+
expect(errors).toHaveLength(0);
|
|
120
|
+
expect(dto.filters).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should accept sortBy and sortOrder query params', async () => {
|
|
124
|
+
const dto = simulateValidationPipe({
|
|
125
|
+
page: '1',
|
|
126
|
+
limit: '20',
|
|
127
|
+
sortBy: 'startedAt',
|
|
128
|
+
sortOrder: 'desc',
|
|
129
|
+
});
|
|
130
|
+
const errors = await validate(dto, {
|
|
131
|
+
whitelist: true,
|
|
132
|
+
forbidNonWhitelisted: true,
|
|
133
|
+
});
|
|
134
|
+
expect(errors).toHaveLength(0);
|
|
135
|
+
expect(dto.sortBy).toBe('startedAt');
|
|
136
|
+
expect(dto.sortOrder).toBe('DESC');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should accept empty string filters', async () => {
|
|
140
|
+
const dto = simulateValidationPipe({ filters: '' });
|
|
141
|
+
const errors = await validate(dto, {
|
|
142
|
+
whitelist: true,
|
|
143
|
+
forbidNonWhitelisted: true,
|
|
144
|
+
});
|
|
145
|
+
expect(errors).toHaveLength(0);
|
|
146
|
+
expect(dto.filters).toBeUndefined();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should reject invalid JSON string', async () => {
|
|
150
|
+
const dto = simulateValidationPipe({ filters: '{bad json' });
|
|
151
|
+
const errors = await validate(dto, {
|
|
152
|
+
whitelist: true,
|
|
153
|
+
forbidNonWhitelisted: true,
|
|
154
|
+
});
|
|
155
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should reject a filter with invalid key', async () => {
|
|
159
|
+
const raw = {
|
|
160
|
+
filters: '{"key":"invalidField","op":"==","value":"x"}',
|
|
161
|
+
};
|
|
162
|
+
const dto = simulateValidationPipe(raw);
|
|
163
|
+
const errors = await validate(dto, {
|
|
164
|
+
whitelist: true,
|
|
165
|
+
forbidNonWhitelisted: true,
|
|
166
|
+
});
|
|
167
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should reject a filter with invalid operator', async () => {
|
|
171
|
+
const raw = {
|
|
172
|
+
filters: '{"key":"intention","op":"contains","value":"x"}',
|
|
173
|
+
};
|
|
174
|
+
const dto = simulateValidationPipe(raw);
|
|
175
|
+
const errors = await validate(dto, {
|
|
176
|
+
whitelist: true,
|
|
177
|
+
forbidNonWhitelisted: true,
|
|
178
|
+
});
|
|
179
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should accept op "in" with array value', async () => {
|
|
183
|
+
const raw = {
|
|
184
|
+
filters: '{"key":"intention","op":"in","value":["clinic_info","other"]}',
|
|
185
|
+
};
|
|
186
|
+
const dto = simulateValidationPipe(raw);
|
|
187
|
+
const errors = await validate(dto, {
|
|
188
|
+
whitelist: true,
|
|
189
|
+
forbidNonWhitelisted: true,
|
|
190
|
+
});
|
|
191
|
+
expect(errors).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should reject op "in" with scalar value', async () => {
|
|
195
|
+
const raw = {
|
|
196
|
+
filters: '{"key":"intention","op":"in","value":"clinic_info"}',
|
|
197
|
+
};
|
|
198
|
+
const dto = simulateValidationPipe(raw);
|
|
199
|
+
const errors = await validate(dto, {
|
|
200
|
+
whitelist: true,
|
|
201
|
+
forbidNonWhitelisted: true,
|
|
202
|
+
});
|
|
203
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle double-encoded JSON string', async () => {
|
|
207
|
+
const inner = JSON.stringify({
|
|
208
|
+
key: 'intention',
|
|
209
|
+
op: '==',
|
|
210
|
+
value: 'clinic_info',
|
|
211
|
+
});
|
|
212
|
+
const raw = { filters: JSON.stringify(inner) };
|
|
213
|
+
const dto = simulateValidationPipe(raw);
|
|
214
|
+
const errors = await validate(dto, {
|
|
215
|
+
whitelist: true,
|
|
216
|
+
forbidNonWhitelisted: true,
|
|
217
|
+
});
|
|
218
|
+
expect(errors).toHaveLength(0);
|
|
219
|
+
expect(dto.filters).toHaveLength(1);
|
|
220
|
+
});
|
|
221
|
+
});
|