tychat-contracts 1.0.107 → 1.0.109
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/dist/analytics/analytics-query.dto.js +2 -2
- package/dist/conversations/conversation-contact-filters-query.validator.spec.d.ts +2 -0
- package/dist/conversations/conversation-contact-filters-query.validator.spec.d.ts.map +1 -0
- package/dist/conversations/conversation-contact-filters-query.validator.spec.js +189 -0
- package/dist/conversations/list-conversation-contacts-query.dto.d.ts.map +1 -1
- package/dist/conversations/list-conversation-contacts-query.dto.js +20 -0
- package/jest.config.ts +5 -0
- package/package.json +3 -1
- package/scripts/debug-filters-transform.js +29 -0
- package/src/analytics/analytics-query.dto.ts +2 -2
- package/src/conversations/conversation-contact-filters-query.validator.spec.ts +205 -0
- package/src/conversations/list-conversation-contacts-query.dto.ts +20 -0
|
@@ -127,7 +127,7 @@ __decorate([
|
|
|
127
127
|
], AnalyticsDashboardFinancialDto.prototype, "averageTicket", void 0);
|
|
128
128
|
__decorate([
|
|
129
129
|
(0, swagger_1.ApiProperty)({
|
|
130
|
-
description: 'Valor
|
|
130
|
+
description: 'Valor total de procedimentos dos agendamentos ainda pendentes (status pending/processing/sended) no período: soma de `appointment_procedures.value` via JOIN com `appointments`.',
|
|
131
131
|
}),
|
|
132
132
|
__metadata("design:type", Number)
|
|
133
133
|
], AnalyticsDashboardFinancialDto.prototype, "pending", void 0);
|
|
@@ -220,7 +220,7 @@ __decorate([
|
|
|
220
220
|
], AnalyticsDashboardResponseDto.prototype, "appointmentsFinished", void 0);
|
|
221
221
|
__decorate([
|
|
222
222
|
(0, swagger_1.ApiProperty)({
|
|
223
|
-
description: 'Procedimentos agendados:
|
|
223
|
+
description: 'Procedimentos agendados: total de registros em `appointment_procedures` cujo agendamento (`appointments.date`) está no período',
|
|
224
224
|
}),
|
|
225
225
|
__metadata("design:type", Number)
|
|
226
226
|
], AnalyticsDashboardResponseDto.prototype, "proceduresScheduled", void 0);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-contact-filters-query.validator.spec.d.ts","sourceRoot":"","sources":["../../src/conversations/conversation-contact-filters-query.validator.spec.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
require("reflect-metadata");
|
|
4
|
+
const class_transformer_1 = require("class-transformer");
|
|
5
|
+
const class_validator_1 = require("class-validator");
|
|
6
|
+
const list_conversation_contacts_query_dto_1 = require("./list-conversation-contacts-query.dto");
|
|
7
|
+
/**
|
|
8
|
+
* Simulates how NestJS ValidationPipe processes a raw query object
|
|
9
|
+
* when configured with transform: true and enableImplicitConversion: true.
|
|
10
|
+
*/
|
|
11
|
+
function simulateValidationPipe(raw) {
|
|
12
|
+
return (0, class_transformer_1.plainToInstance)(list_conversation_contacts_query_dto_1.ListConversationContactsQueryDto, raw, {
|
|
13
|
+
enableImplicitConversion: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
describe('ConversationContactFiltersQueryConstraint', () => {
|
|
17
|
+
it('should accept a single JSON object string (URL-decoded query param)', async () => {
|
|
18
|
+
const raw = {
|
|
19
|
+
page: '1',
|
|
20
|
+
limit: '20',
|
|
21
|
+
filters: '{"key":"intention","op":"==","value":"clinic_info"}',
|
|
22
|
+
};
|
|
23
|
+
const dto = simulateValidationPipe(raw);
|
|
24
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
25
|
+
whitelist: true,
|
|
26
|
+
forbidNonWhitelisted: true,
|
|
27
|
+
});
|
|
28
|
+
expect(errors).toHaveLength(0);
|
|
29
|
+
expect(dto.filters).toEqual([
|
|
30
|
+
expect.objectContaining({
|
|
31
|
+
key: 'intention',
|
|
32
|
+
op: '==',
|
|
33
|
+
value: 'clinic_info',
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
it('should accept a JSON array string with one filter', async () => {
|
|
38
|
+
const raw = {
|
|
39
|
+
page: '1',
|
|
40
|
+
limit: '20',
|
|
41
|
+
filters: '[{"key":"intention","op":"==","value":"clinic_info"}]',
|
|
42
|
+
};
|
|
43
|
+
const dto = simulateValidationPipe(raw);
|
|
44
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
45
|
+
whitelist: true,
|
|
46
|
+
forbidNonWhitelisted: true,
|
|
47
|
+
});
|
|
48
|
+
expect(errors).toHaveLength(0);
|
|
49
|
+
expect(dto.filters).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
it('should accept a JSON array string with multiple filters', async () => {
|
|
52
|
+
const raw = {
|
|
53
|
+
filters: '[{"key":"intention","op":"==","value":"clinic_info"},{"key":"patientId","op":"!=","value":"abc"}]',
|
|
54
|
+
};
|
|
55
|
+
const dto = simulateValidationPipe(raw);
|
|
56
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
57
|
+
whitelist: true,
|
|
58
|
+
forbidNonWhitelisted: true,
|
|
59
|
+
});
|
|
60
|
+
expect(errors).toHaveLength(0);
|
|
61
|
+
expect(dto.filters).toHaveLength(2);
|
|
62
|
+
});
|
|
63
|
+
it('should accept an array of JSON strings (e.g. filters[0]=...&filters[1]=...)', async () => {
|
|
64
|
+
const raw = {
|
|
65
|
+
filters: [
|
|
66
|
+
'{"key":"intention","op":"==","value":"clinic_info"}',
|
|
67
|
+
'{"key":"patientId","op":"!=","value":"xyz"}',
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
const dto = simulateValidationPipe(raw);
|
|
71
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
72
|
+
whitelist: true,
|
|
73
|
+
forbidNonWhitelisted: true,
|
|
74
|
+
});
|
|
75
|
+
expect(errors).toHaveLength(0);
|
|
76
|
+
expect(dto.filters).toHaveLength(2);
|
|
77
|
+
});
|
|
78
|
+
it('should accept an array of plain objects (Kafka / programmatic usage)', async () => {
|
|
79
|
+
const raw = {
|
|
80
|
+
filters: [
|
|
81
|
+
{ key: 'intention', op: '==', value: 'clinic_info' },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
const dto = simulateValidationPipe(raw);
|
|
85
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
86
|
+
whitelist: true,
|
|
87
|
+
forbidNonWhitelisted: true,
|
|
88
|
+
});
|
|
89
|
+
expect(errors).toHaveLength(0);
|
|
90
|
+
expect(dto.filters).toHaveLength(1);
|
|
91
|
+
});
|
|
92
|
+
it('should accept a single plain object', async () => {
|
|
93
|
+
const raw = {
|
|
94
|
+
filters: { key: 'intention', op: '==', value: 'clinic_info' },
|
|
95
|
+
};
|
|
96
|
+
const dto = simulateValidationPipe(raw);
|
|
97
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
98
|
+
whitelist: true,
|
|
99
|
+
forbidNonWhitelisted: true,
|
|
100
|
+
});
|
|
101
|
+
expect(errors).toHaveLength(0);
|
|
102
|
+
expect(dto.filters).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
it('should accept undefined/null filters', async () => {
|
|
105
|
+
const dto = simulateValidationPipe({ page: '1', limit: '20' });
|
|
106
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
107
|
+
whitelist: true,
|
|
108
|
+
forbidNonWhitelisted: true,
|
|
109
|
+
});
|
|
110
|
+
expect(errors).toHaveLength(0);
|
|
111
|
+
expect(dto.filters).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
it('should accept empty string filters', async () => {
|
|
114
|
+
const dto = simulateValidationPipe({ filters: '' });
|
|
115
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
116
|
+
whitelist: true,
|
|
117
|
+
forbidNonWhitelisted: true,
|
|
118
|
+
});
|
|
119
|
+
expect(errors).toHaveLength(0);
|
|
120
|
+
expect(dto.filters).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
it('should reject invalid JSON string', async () => {
|
|
123
|
+
const dto = simulateValidationPipe({ filters: '{bad json' });
|
|
124
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
125
|
+
whitelist: true,
|
|
126
|
+
forbidNonWhitelisted: true,
|
|
127
|
+
});
|
|
128
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
129
|
+
});
|
|
130
|
+
it('should reject a filter with invalid key', async () => {
|
|
131
|
+
const raw = {
|
|
132
|
+
filters: '{"key":"invalidField","op":"==","value":"x"}',
|
|
133
|
+
};
|
|
134
|
+
const dto = simulateValidationPipe(raw);
|
|
135
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
136
|
+
whitelist: true,
|
|
137
|
+
forbidNonWhitelisted: true,
|
|
138
|
+
});
|
|
139
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
it('should reject a filter with invalid operator', async () => {
|
|
142
|
+
const raw = {
|
|
143
|
+
filters: '{"key":"intention","op":"like","value":"x"}',
|
|
144
|
+
};
|
|
145
|
+
const dto = simulateValidationPipe(raw);
|
|
146
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
147
|
+
whitelist: true,
|
|
148
|
+
forbidNonWhitelisted: true,
|
|
149
|
+
});
|
|
150
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
it('should accept op "in" with array value', async () => {
|
|
153
|
+
const raw = {
|
|
154
|
+
filters: '{"key":"intention","op":"in","value":["clinic_info","other"]}',
|
|
155
|
+
};
|
|
156
|
+
const dto = simulateValidationPipe(raw);
|
|
157
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
158
|
+
whitelist: true,
|
|
159
|
+
forbidNonWhitelisted: true,
|
|
160
|
+
});
|
|
161
|
+
expect(errors).toHaveLength(0);
|
|
162
|
+
});
|
|
163
|
+
it('should reject op "in" with scalar value', async () => {
|
|
164
|
+
const raw = {
|
|
165
|
+
filters: '{"key":"intention","op":"in","value":"clinic_info"}',
|
|
166
|
+
};
|
|
167
|
+
const dto = simulateValidationPipe(raw);
|
|
168
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
169
|
+
whitelist: true,
|
|
170
|
+
forbidNonWhitelisted: true,
|
|
171
|
+
});
|
|
172
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
173
|
+
});
|
|
174
|
+
it('should handle double-encoded JSON string', async () => {
|
|
175
|
+
const inner = JSON.stringify({
|
|
176
|
+
key: 'intention',
|
|
177
|
+
op: '==',
|
|
178
|
+
value: 'clinic_info',
|
|
179
|
+
});
|
|
180
|
+
const raw = { filters: JSON.stringify(inner) };
|
|
181
|
+
const dto = simulateValidationPipe(raw);
|
|
182
|
+
const errors = await (0, class_validator_1.validate)(dto, {
|
|
183
|
+
whitelist: true,
|
|
184
|
+
forbidNonWhitelisted: true,
|
|
185
|
+
});
|
|
186
|
+
expect(errors).toHaveLength(0);
|
|
187
|
+
expect(dto.filters).toHaveLength(1);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list-conversation-contacts-query.dto.d.ts","sourceRoot":"","sources":["../../src/conversations/list-conversation-contacts-query.dto.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gCAAgC,EAAE,MAAM,0CAA0C,CAAC;
|
|
1
|
+
{"version":3,"file":"list-conversation-contacts-query.dto.d.ts","sourceRoot":"","sources":["../../src/conversations/list-conversation-contacts-query.dto.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gCAAgC,EAAE,MAAM,0CAA0C,CAAC;AAqB5F,qBAAa,gCAAgC;IAU3C,IAAI,CAAC,EAAE,MAAM,CAAC;IAad,KAAK,CAAC,EAAE,MAAM,CAAC;IAYf,OAAO,CAAC,EAAE,gCAAgC,EAAE,CAAC;CAC9C"}
|
|
@@ -13,6 +13,24 @@ exports.ListConversationContactsQueryDto = void 0;
|
|
|
13
13
|
const swagger_1 = require("@nestjs/swagger");
|
|
14
14
|
const class_validator_1 = require("class-validator");
|
|
15
15
|
const conversation_contact_filters_query_validator_1 = require("./conversation-contact-filters-query.validator");
|
|
16
|
+
/**
|
|
17
|
+
* Overrides TypeScript's reflected `design:type` metadata to `Object`.
|
|
18
|
+
*
|
|
19
|
+
* When `enableImplicitConversion` is true, class-transformer uses the reflected
|
|
20
|
+
* type to recursively transform array elements. For properties like `filters`
|
|
21
|
+
* whose raw value (string / mixed array) is fully parsed by a custom validator,
|
|
22
|
+
* the implicit conversion mangles the value before the validator sees it.
|
|
23
|
+
*
|
|
24
|
+
* Placing this decorator **first** (topmost) ensures it runs **last** in the
|
|
25
|
+
* decorator chain, overriding the `__metadata("design:type", Array)` emitted
|
|
26
|
+
* by TypeScript. With `design:type = Object`, class-transformer leaves the raw
|
|
27
|
+
* value intact.
|
|
28
|
+
*/
|
|
29
|
+
function RawQueryParam() {
|
|
30
|
+
return (target, propertyKey) => {
|
|
31
|
+
Reflect.defineMetadata('design:type', Object, target, propertyKey);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
16
34
|
class ListConversationContactsQueryDto {
|
|
17
35
|
page;
|
|
18
36
|
limit;
|
|
@@ -52,6 +70,8 @@ __decorate([
|
|
|
52
70
|
items: { type: 'string' },
|
|
53
71
|
example: ['{"key":"intention","op":"==","value":"clinic_info"}'],
|
|
54
72
|
}),
|
|
73
|
+
RawQueryParam() // Must be first — overrides design:type from Array to Object so class-transformer won't mangle the value
|
|
74
|
+
,
|
|
55
75
|
(0, class_validator_1.IsOptional)(),
|
|
56
76
|
(0, class_validator_1.Validate)(conversation_contact_filters_query_validator_1.ConversationContactFiltersQueryConstraint),
|
|
57
77
|
__metadata("design:type", Array)
|
package/jest.config.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tychat-contracts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.109",
|
|
4
4
|
"description": "DTOs compartilhados com class-validator (API e microserviços)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@nestjs/swagger": "^11.2.6",
|
|
21
|
+
"@types/jest": "^30.0.0",
|
|
22
|
+
"ts-jest": "^29.4.9",
|
|
21
23
|
"typescript": "^5.7.3"
|
|
22
24
|
},
|
|
23
25
|
"peerDependencies": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require('reflect-metadata');
|
|
2
|
+
const { plainToInstance } = require('class-transformer');
|
|
3
|
+
const { validateSync } = require('class-validator');
|
|
4
|
+
const {
|
|
5
|
+
ListConversationContactsQueryDto,
|
|
6
|
+
} = require('../dist/conversations/list-conversation-contacts-query.dto.js');
|
|
7
|
+
|
|
8
|
+
const obj = {
|
|
9
|
+
key: 'intention',
|
|
10
|
+
op: '==',
|
|
11
|
+
value: 'clinic_info',
|
|
12
|
+
};
|
|
13
|
+
const r = plainToInstance(
|
|
14
|
+
ListConversationContactsQueryDto,
|
|
15
|
+
{ page: '1', limit: '20', filters: obj },
|
|
16
|
+
{ enableImplicitConversion: true },
|
|
17
|
+
);
|
|
18
|
+
const errors = validateSync(r, {
|
|
19
|
+
whitelist: true,
|
|
20
|
+
forbidNonWhitelisted: true,
|
|
21
|
+
});
|
|
22
|
+
process.stdout.write('errors=' + JSON.stringify(errors) + '\n');
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
'len=' +
|
|
25
|
+
String(r.filters?.length) +
|
|
26
|
+
' isArray=' +
|
|
27
|
+
String(Array.isArray(r.filters)) +
|
|
28
|
+
'\n',
|
|
29
|
+
);
|
|
@@ -104,7 +104,7 @@ export class AnalyticsDashboardFinancialDto {
|
|
|
104
104
|
|
|
105
105
|
@ApiProperty({
|
|
106
106
|
description:
|
|
107
|
-
'Valor
|
|
107
|
+
'Valor total de procedimentos dos agendamentos ainda pendentes (status pending/processing/sended) no período: soma de `appointment_procedures.value` via JOIN com `appointments`.',
|
|
108
108
|
})
|
|
109
109
|
pending: number;
|
|
110
110
|
}
|
|
@@ -159,7 +159,7 @@ export class AnalyticsDashboardResponseDto {
|
|
|
159
159
|
|
|
160
160
|
@ApiProperty({
|
|
161
161
|
description:
|
|
162
|
-
'Procedimentos agendados:
|
|
162
|
+
'Procedimentos agendados: total de registros em `appointment_procedures` cujo agendamento (`appointments.date`) está no período',
|
|
163
163
|
})
|
|
164
164
|
proceduresScheduled: number;
|
|
165
165
|
|
|
@@ -0,0 +1,205 @@
|
|
|
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 empty string filters', async () => {
|
|
124
|
+
const dto = simulateValidationPipe({ filters: '' });
|
|
125
|
+
const errors = await validate(dto, {
|
|
126
|
+
whitelist: true,
|
|
127
|
+
forbidNonWhitelisted: true,
|
|
128
|
+
});
|
|
129
|
+
expect(errors).toHaveLength(0);
|
|
130
|
+
expect(dto.filters).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should reject invalid JSON string', async () => {
|
|
134
|
+
const dto = simulateValidationPipe({ filters: '{bad json' });
|
|
135
|
+
const errors = await validate(dto, {
|
|
136
|
+
whitelist: true,
|
|
137
|
+
forbidNonWhitelisted: true,
|
|
138
|
+
});
|
|
139
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should reject a filter with invalid key', async () => {
|
|
143
|
+
const raw = {
|
|
144
|
+
filters: '{"key":"invalidField","op":"==","value":"x"}',
|
|
145
|
+
};
|
|
146
|
+
const dto = simulateValidationPipe(raw);
|
|
147
|
+
const errors = await validate(dto, {
|
|
148
|
+
whitelist: true,
|
|
149
|
+
forbidNonWhitelisted: true,
|
|
150
|
+
});
|
|
151
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should reject a filter with invalid operator', async () => {
|
|
155
|
+
const raw = {
|
|
156
|
+
filters: '{"key":"intention","op":"like","value":"x"}',
|
|
157
|
+
};
|
|
158
|
+
const dto = simulateValidationPipe(raw);
|
|
159
|
+
const errors = await validate(dto, {
|
|
160
|
+
whitelist: true,
|
|
161
|
+
forbidNonWhitelisted: true,
|
|
162
|
+
});
|
|
163
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should accept op "in" with array value', async () => {
|
|
167
|
+
const raw = {
|
|
168
|
+
filters: '{"key":"intention","op":"in","value":["clinic_info","other"]}',
|
|
169
|
+
};
|
|
170
|
+
const dto = simulateValidationPipe(raw);
|
|
171
|
+
const errors = await validate(dto, {
|
|
172
|
+
whitelist: true,
|
|
173
|
+
forbidNonWhitelisted: true,
|
|
174
|
+
});
|
|
175
|
+
expect(errors).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should reject op "in" with scalar value', async () => {
|
|
179
|
+
const raw = {
|
|
180
|
+
filters: '{"key":"intention","op":"in","value":"clinic_info"}',
|
|
181
|
+
};
|
|
182
|
+
const dto = simulateValidationPipe(raw);
|
|
183
|
+
const errors = await validate(dto, {
|
|
184
|
+
whitelist: true,
|
|
185
|
+
forbidNonWhitelisted: true,
|
|
186
|
+
});
|
|
187
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should handle double-encoded JSON string', async () => {
|
|
191
|
+
const inner = JSON.stringify({
|
|
192
|
+
key: 'intention',
|
|
193
|
+
op: '==',
|
|
194
|
+
value: 'clinic_info',
|
|
195
|
+
});
|
|
196
|
+
const raw = { filters: JSON.stringify(inner) };
|
|
197
|
+
const dto = simulateValidationPipe(raw);
|
|
198
|
+
const errors = await validate(dto, {
|
|
199
|
+
whitelist: true,
|
|
200
|
+
forbidNonWhitelisted: true,
|
|
201
|
+
});
|
|
202
|
+
expect(errors).toHaveLength(0);
|
|
203
|
+
expect(dto.filters).toHaveLength(1);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -3,6 +3,25 @@ import { IsInt, IsOptional, Max, Min, Validate } from 'class-validator';
|
|
|
3
3
|
import { ConversationContactFiltersQueryConstraint } from './conversation-contact-filters-query.validator';
|
|
4
4
|
import { ConversationContactListFilterDto } from './list-conversation-contacts-filters.dto';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Overrides TypeScript's reflected `design:type` metadata to `Object`.
|
|
8
|
+
*
|
|
9
|
+
* When `enableImplicitConversion` is true, class-transformer uses the reflected
|
|
10
|
+
* type to recursively transform array elements. For properties like `filters`
|
|
11
|
+
* whose raw value (string / mixed array) is fully parsed by a custom validator,
|
|
12
|
+
* the implicit conversion mangles the value before the validator sees it.
|
|
13
|
+
*
|
|
14
|
+
* Placing this decorator **first** (topmost) ensures it runs **last** in the
|
|
15
|
+
* decorator chain, overriding the `__metadata("design:type", Array)` emitted
|
|
16
|
+
* by TypeScript. With `design:type = Object`, class-transformer leaves the raw
|
|
17
|
+
* value intact.
|
|
18
|
+
*/
|
|
19
|
+
function RawQueryParam(): PropertyDecorator {
|
|
20
|
+
return (target, propertyKey) => {
|
|
21
|
+
Reflect.defineMetadata('design:type', Object, target, propertyKey);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
export class ListConversationContactsQueryDto {
|
|
7
26
|
@ApiPropertyOptional({
|
|
8
27
|
description: 'Número da página (iniciando em 1)',
|
|
@@ -35,6 +54,7 @@ export class ListConversationContactsQueryDto {
|
|
|
35
54
|
items: { type: 'string' },
|
|
36
55
|
example: ['{"key":"intention","op":"==","value":"clinic_info"}'],
|
|
37
56
|
})
|
|
57
|
+
@RawQueryParam() // Must be first — overrides design:type from Array to Object so class-transformer won't mangle the value
|
|
38
58
|
@IsOptional()
|
|
39
59
|
@Validate(ConversationContactFiltersQueryConstraint)
|
|
40
60
|
filters?: ConversationContactListFilterDto[];
|