@flusys/nestjs-form-builder 6.0.2 → 6.1.0
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/cjs/controllers/form-result.controller.spec.js +257 -0
- package/cjs/controllers/form.controller.spec.js +212 -0
- package/cjs/docs/form-builder-swagger.config.spec.js +75 -0
- package/cjs/entities/index.spec.js +18 -0
- package/cjs/modules/form-builder.module.spec.js +133 -0
- package/cjs/services/form-builder-config.service.spec.js +121 -0
- package/cjs/services/form-builder-datasource.provider.spec.js +147 -0
- package/cjs/services/form-result.service.spec.js +702 -0
- package/cjs/services/form.service.spec.js +746 -0
- package/cjs/utils/computed-field.utils.spec.js +1205 -0
- package/cjs/utils/permission.utils.spec.js +118 -0
- package/fesm/controllers/form-result.controller.spec.js +253 -0
- package/fesm/controllers/form.controller.spec.js +208 -0
- package/fesm/docs/form-builder-swagger.config.spec.js +71 -0
- package/fesm/entities/index.spec.js +14 -0
- package/fesm/modules/form-builder.module.spec.js +129 -0
- package/fesm/services/form-builder-config.service.spec.js +117 -0
- package/fesm/services/form-builder-datasource.provider.spec.js +143 -0
- package/fesm/services/form-result.service.spec.js +657 -0
- package/fesm/services/form.service.spec.js +701 -0
- package/fesm/utils/computed-field.utils.spec.js +1201 -0
- package/fesm/utils/permission.utils.spec.js +114 -0
- package/package.json +3 -3
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
7
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
|
+
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
9
|
+
const _entities = require("../entities");
|
|
10
|
+
const _formaccesstypeenum = require("../enums/form-access-type.enum");
|
|
11
|
+
const _permissionutils = /*#__PURE__*/ _interop_require_wildcard(require("../utils/permission.utils"));
|
|
12
|
+
const _formresultservice = require("./form-result.service");
|
|
13
|
+
const _formbuilderconfigservice = require("./form-builder-config.service");
|
|
14
|
+
const _formbuilderdatasourceprovider = require("./form-builder-datasource.provider");
|
|
15
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
16
|
+
if (typeof WeakMap !== "function") return null;
|
|
17
|
+
var cacheBabelInterop = new WeakMap();
|
|
18
|
+
var cacheNodeInterop = new WeakMap();
|
|
19
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
20
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
21
|
+
})(nodeInterop);
|
|
22
|
+
}
|
|
23
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
24
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
25
|
+
return obj;
|
|
26
|
+
}
|
|
27
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
28
|
+
return {
|
|
29
|
+
default: obj
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
33
|
+
if (cache && cache.has(obj)) {
|
|
34
|
+
return cache.get(obj);
|
|
35
|
+
}
|
|
36
|
+
var newObj = {
|
|
37
|
+
__proto__: null
|
|
38
|
+
};
|
|
39
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
40
|
+
for(var key in obj){
|
|
41
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
42
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
43
|
+
if (desc && (desc.get || desc.set)) {
|
|
44
|
+
Object.defineProperty(newObj, key, desc);
|
|
45
|
+
} else {
|
|
46
|
+
newObj[key] = obj[key];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
newObj.default = obj;
|
|
51
|
+
if (cache) {
|
|
52
|
+
cache.set(obj, newObj);
|
|
53
|
+
}
|
|
54
|
+
return newObj;
|
|
55
|
+
}
|
|
56
|
+
jest.mock('../utils/permission.utils', ()=>({
|
|
57
|
+
validateUserPermissions: jest.fn()
|
|
58
|
+
}));
|
|
59
|
+
function buildForm(overrides = {}) {
|
|
60
|
+
return {
|
|
61
|
+
id: 'form-uuid-1',
|
|
62
|
+
name: 'Customer Survey',
|
|
63
|
+
description: null,
|
|
64
|
+
slug: 'customer-survey',
|
|
65
|
+
schema: {
|
|
66
|
+
sections: [],
|
|
67
|
+
settings: {}
|
|
68
|
+
},
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED,
|
|
71
|
+
actionGroups: null,
|
|
72
|
+
tags: null,
|
|
73
|
+
isActive: true,
|
|
74
|
+
createdAt: new Date(),
|
|
75
|
+
updatedAt: new Date(),
|
|
76
|
+
deletedAt: null,
|
|
77
|
+
createdById: null,
|
|
78
|
+
updatedById: null,
|
|
79
|
+
deletedById: null,
|
|
80
|
+
...overrides
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function buildFormResult(overrides = {}) {
|
|
84
|
+
return {
|
|
85
|
+
id: 'result-uuid-1',
|
|
86
|
+
formId: 'form-uuid-1',
|
|
87
|
+
schemaVersionSnapshot: {
|
|
88
|
+
sections: []
|
|
89
|
+
},
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
data: {
|
|
92
|
+
field1: 'value1'
|
|
93
|
+
},
|
|
94
|
+
submittedById: 'user-uuid-1',
|
|
95
|
+
submittedAt: new Date(),
|
|
96
|
+
isDraft: false,
|
|
97
|
+
createdAt: new Date(),
|
|
98
|
+
updatedAt: new Date(),
|
|
99
|
+
deletedAt: null,
|
|
100
|
+
createdById: null,
|
|
101
|
+
updatedById: null,
|
|
102
|
+
deletedById: null,
|
|
103
|
+
...overrides
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
describe('FormResultService', ()=>{
|
|
107
|
+
let service;
|
|
108
|
+
let mockResultRepo;
|
|
109
|
+
let mockFormRepo;
|
|
110
|
+
let mockFormBuilderConfig;
|
|
111
|
+
async function buildService(companyFeatureEnabled = false) {
|
|
112
|
+
mockResultRepo = (0, _repositorymock.createMockRepository)();
|
|
113
|
+
mockFormRepo = (0, _repositorymock.createMockRepository)();
|
|
114
|
+
// `softRemove` isn't part of the shared repository mock's default method list — add it here.
|
|
115
|
+
mockResultRepo.softRemove = jest.fn();
|
|
116
|
+
// Extend the shared query-builder mock with the extra chain methods this service relies on
|
|
117
|
+
// (`innerJoin` for the company scoping join).
|
|
118
|
+
mockResultRepo.createQueryBuilder.mockReturnValue({
|
|
119
|
+
...(0, _repositorymock.createMockQueryBuilder)(),
|
|
120
|
+
innerJoin: jest.fn().mockReturnThis()
|
|
121
|
+
});
|
|
122
|
+
const mockDataSourceProvider = {
|
|
123
|
+
getRepository: jest.fn((entity)=>{
|
|
124
|
+
if (entity === _entities.FormResult) return Promise.resolve(mockResultRepo);
|
|
125
|
+
return Promise.resolve(mockFormRepo);
|
|
126
|
+
}),
|
|
127
|
+
getDataSource: jest.fn()
|
|
128
|
+
};
|
|
129
|
+
mockFormBuilderConfig = {
|
|
130
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(companyFeatureEnabled)
|
|
131
|
+
};
|
|
132
|
+
const module = await _testing.Test.createTestingModule({
|
|
133
|
+
providers: [
|
|
134
|
+
_formresultservice.FormResultService,
|
|
135
|
+
{
|
|
136
|
+
provide: 'CACHE_INSTANCE',
|
|
137
|
+
useValue: {}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
provide: _modules.UtilsService,
|
|
141
|
+
useValue: {}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
provide: _formbuilderconfigservice.FormBuilderConfigService,
|
|
145
|
+
useValue: mockFormBuilderConfig
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
provide: _formbuilderdatasourceprovider.FormBuilderDataSourceProvider,
|
|
149
|
+
useValue: mockDataSourceProvider
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}).compile();
|
|
153
|
+
service = await module.resolve(_formresultservice.FormResultService);
|
|
154
|
+
}
|
|
155
|
+
afterEach(()=>{
|
|
156
|
+
jest.clearAllMocks();
|
|
157
|
+
});
|
|
158
|
+
// ==========================================================================
|
|
159
|
+
// getSelectQuery
|
|
160
|
+
// ==========================================================================
|
|
161
|
+
describe('getSelectQuery', ()=>{
|
|
162
|
+
it('applies the default field list, prefixed with the entity name', async ()=>{
|
|
163
|
+
await buildService(false);
|
|
164
|
+
const query = {
|
|
165
|
+
select: jest.fn().mockReturnThis()
|
|
166
|
+
};
|
|
167
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), undefined);
|
|
168
|
+
expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
|
|
169
|
+
'form_result.id',
|
|
170
|
+
'form_result.formId',
|
|
171
|
+
'form_result.data',
|
|
172
|
+
'form_result.isDraft'
|
|
173
|
+
]));
|
|
174
|
+
});
|
|
175
|
+
it('respects a custom select list', async ()=>{
|
|
176
|
+
await buildService(false);
|
|
177
|
+
const query = {
|
|
178
|
+
select: jest.fn().mockReturnThis()
|
|
179
|
+
};
|
|
180
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
181
|
+
'id',
|
|
182
|
+
'data'
|
|
183
|
+
]);
|
|
184
|
+
expect(query.select).toHaveBeenCalledWith([
|
|
185
|
+
'form_result.id',
|
|
186
|
+
'form_result.data'
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ==========================================================================
|
|
191
|
+
// getExtraManipulateQuery / applyCompanyFilterToQuery
|
|
192
|
+
// ==========================================================================
|
|
193
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
194
|
+
it('joins the form table and filters by company when company feature is enabled', async ()=>{
|
|
195
|
+
await buildService(true);
|
|
196
|
+
const query = {
|
|
197
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
198
|
+
andWhere: jest.fn().mockReturnThis()
|
|
199
|
+
};
|
|
200
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
201
|
+
companyId: 'company-1'
|
|
202
|
+
}));
|
|
203
|
+
expect(query.innerJoin).toHaveBeenCalledWith('form', 'f', 'f.id = form_result.formId');
|
|
204
|
+
expect(query.andWhere).toHaveBeenCalledWith('f.company_id = :companyId', {
|
|
205
|
+
companyId: 'company-1'
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
it('does not join when company feature is disabled', async ()=>{
|
|
209
|
+
await buildService(false);
|
|
210
|
+
const query = {
|
|
211
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
212
|
+
andWhere: jest.fn().mockReturnThis()
|
|
213
|
+
};
|
|
214
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
215
|
+
companyId: 'company-1'
|
|
216
|
+
}));
|
|
217
|
+
expect(query.innerJoin).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
it('does not join when the user has no companyId', async ()=>{
|
|
220
|
+
await buildService(true);
|
|
221
|
+
const query = {
|
|
222
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
223
|
+
andWhere: jest.fn().mockReturnThis()
|
|
224
|
+
};
|
|
225
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
226
|
+
companyId: undefined
|
|
227
|
+
}));
|
|
228
|
+
expect(query.innerJoin).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
it('does not join when there is no user (public queries)', async ()=>{
|
|
231
|
+
await buildService(true);
|
|
232
|
+
const query = {
|
|
233
|
+
innerJoin: jest.fn().mockReturnThis(),
|
|
234
|
+
andWhere: jest.fn().mockReturnThis()
|
|
235
|
+
};
|
|
236
|
+
await service.getExtraManipulateQuery(query, {}, null);
|
|
237
|
+
expect(query.innerJoin).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ==========================================================================
|
|
241
|
+
// convertEntityToResponseDto
|
|
242
|
+
// ==========================================================================
|
|
243
|
+
describe('convertEntityToResponseDto', ()=>{
|
|
244
|
+
it('maps entity fields to the response shape', async ()=>{
|
|
245
|
+
await buildService(false);
|
|
246
|
+
const entity = buildFormResult();
|
|
247
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
248
|
+
expect(dto).toMatchObject({
|
|
249
|
+
id: entity.id,
|
|
250
|
+
formId: entity.formId,
|
|
251
|
+
data: entity.data,
|
|
252
|
+
isDraft: false
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
// ==========================================================================
|
|
257
|
+
// submitForm
|
|
258
|
+
// ==========================================================================
|
|
259
|
+
describe('submitForm', ()=>{
|
|
260
|
+
it('throws NotFoundException when the form does not exist or is inactive', async ()=>{
|
|
261
|
+
await buildService(false);
|
|
262
|
+
mockFormRepo.findOne.mockResolvedValue(null);
|
|
263
|
+
await expect(service.submitForm({
|
|
264
|
+
formId: 'missing',
|
|
265
|
+
data: {}
|
|
266
|
+
}, (0, _loggedusermock.buildMockUser)(), false)).rejects.toMatchObject({
|
|
267
|
+
response: expect.objectContaining({
|
|
268
|
+
messageKey: 'form.not.found'
|
|
269
|
+
})
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
it('rejects a public submission when the form is not marked PUBLIC', async ()=>{
|
|
273
|
+
await buildService(false);
|
|
274
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
275
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED
|
|
276
|
+
}));
|
|
277
|
+
await expect(service.submitForm({
|
|
278
|
+
formId: 'form-uuid-1',
|
|
279
|
+
data: {}
|
|
280
|
+
}, null, true)).rejects.toMatchObject({
|
|
281
|
+
response: expect.objectContaining({
|
|
282
|
+
messageKey: 'form.not.public'
|
|
283
|
+
})
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
it('allows a public submission to a PUBLIC form without a user', async ()=>{
|
|
287
|
+
await buildService(false);
|
|
288
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
289
|
+
accessType: _formaccesstypeenum.FormAccessType.PUBLIC
|
|
290
|
+
}));
|
|
291
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
292
|
+
submittedById: null
|
|
293
|
+
}));
|
|
294
|
+
const result = await service.submitForm({
|
|
295
|
+
formId: 'form-uuid-1',
|
|
296
|
+
data: {
|
|
297
|
+
field1: 'x'
|
|
298
|
+
}
|
|
299
|
+
}, null, true);
|
|
300
|
+
expect(mockResultRepo.findOne).not.toHaveBeenCalled();
|
|
301
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
302
|
+
formId: 'form-uuid-1',
|
|
303
|
+
submittedById: null,
|
|
304
|
+
isDraft: false
|
|
305
|
+
}));
|
|
306
|
+
expect(result.formId).toBe('form-uuid-1');
|
|
307
|
+
});
|
|
308
|
+
it('requires authentication for a non-public form submitted through the authenticated flow', async ()=>{
|
|
309
|
+
await buildService(false);
|
|
310
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
311
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED
|
|
312
|
+
}));
|
|
313
|
+
await expect(service.submitForm({
|
|
314
|
+
formId: 'form-uuid-1',
|
|
315
|
+
data: {}
|
|
316
|
+
}, null, false)).rejects.toMatchObject({
|
|
317
|
+
response: expect.objectContaining({
|
|
318
|
+
messageKey: 'form.auth.required'
|
|
319
|
+
})
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
it('allows submission to an ACTION_GROUP form when the user has the required permission', async ()=>{
|
|
323
|
+
await buildService(false);
|
|
324
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
325
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
326
|
+
actionGroups: [
|
|
327
|
+
'hr.submit'
|
|
328
|
+
]
|
|
329
|
+
}));
|
|
330
|
+
_permissionutils.validateUserPermissions.mockResolvedValue(true);
|
|
331
|
+
mockResultRepo.findOne.mockResolvedValue(null);
|
|
332
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult());
|
|
333
|
+
const result = await service.submitForm({
|
|
334
|
+
formId: 'form-uuid-1',
|
|
335
|
+
data: {}
|
|
336
|
+
}, (0, _loggedusermock.buildMockUser)(), false);
|
|
337
|
+
expect(result.formId).toBe('form-uuid-1');
|
|
338
|
+
});
|
|
339
|
+
it('rejects submission to an ACTION_GROUP form when the user lacks the required permission', async ()=>{
|
|
340
|
+
await buildService(false);
|
|
341
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
342
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
343
|
+
actionGroups: [
|
|
344
|
+
'hr.submit'
|
|
345
|
+
]
|
|
346
|
+
}));
|
|
347
|
+
_permissionutils.validateUserPermissions.mockResolvedValue(false);
|
|
348
|
+
await expect(service.submitForm({
|
|
349
|
+
formId: 'form-uuid-1',
|
|
350
|
+
data: {}
|
|
351
|
+
}, (0, _loggedusermock.buildMockUser)(), false)).rejects.toMatchObject({
|
|
352
|
+
response: expect.objectContaining({
|
|
353
|
+
messageKey: 'form.access.denied'
|
|
354
|
+
})
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
it('rejects submission for an unrecognized (non-AUTHENTICATED) access type', async ()=>{
|
|
358
|
+
await buildService(false);
|
|
359
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
360
|
+
accessType: 'weird'
|
|
361
|
+
}));
|
|
362
|
+
await expect(service.submitForm({
|
|
363
|
+
formId: 'form-uuid-1',
|
|
364
|
+
data: {}
|
|
365
|
+
}, (0, _loggedusermock.buildMockUser)(), false)).rejects.toMatchObject({
|
|
366
|
+
response: expect.objectContaining({
|
|
367
|
+
messageKey: 'form.invalid.access.type'
|
|
368
|
+
})
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
it('saves a new final submission with a schema snapshot and computed fields', async ()=>{
|
|
372
|
+
await buildService(false);
|
|
373
|
+
const form = buildForm({
|
|
374
|
+
schema: {
|
|
375
|
+
settings: {
|
|
376
|
+
computedFields: [
|
|
377
|
+
{
|
|
378
|
+
id: 'cf-1',
|
|
379
|
+
name: 'Total',
|
|
380
|
+
key: 'total',
|
|
381
|
+
valueType: 'number',
|
|
382
|
+
rules: [
|
|
383
|
+
{
|
|
384
|
+
id: 'r1',
|
|
385
|
+
computation: {
|
|
386
|
+
type: 'direct',
|
|
387
|
+
config: {
|
|
388
|
+
type: 'direct',
|
|
389
|
+
value: 99
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
mockFormRepo.findOne.mockResolvedValue(form);
|
|
400
|
+
mockResultRepo.findOne.mockResolvedValue(null);
|
|
401
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult());
|
|
402
|
+
await service.submitForm({
|
|
403
|
+
formId: 'form-uuid-1',
|
|
404
|
+
data: {
|
|
405
|
+
field1: 'x'
|
|
406
|
+
}
|
|
407
|
+
}, (0, _loggedusermock.buildMockUser)(), false);
|
|
408
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
409
|
+
schemaVersionSnapshot: form.schema,
|
|
410
|
+
schemaVersion: form.schemaVersion,
|
|
411
|
+
data: {
|
|
412
|
+
field1: 'x',
|
|
413
|
+
_computed: {
|
|
414
|
+
total: 99
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
isDraft: false
|
|
418
|
+
}));
|
|
419
|
+
});
|
|
420
|
+
it('does not compute fields when saving as a draft', async ()=>{
|
|
421
|
+
await buildService(false);
|
|
422
|
+
const form = buildForm({
|
|
423
|
+
schema: {
|
|
424
|
+
settings: {
|
|
425
|
+
computedFields: [
|
|
426
|
+
{
|
|
427
|
+
id: 'cf-1',
|
|
428
|
+
name: 'Total',
|
|
429
|
+
key: 'total',
|
|
430
|
+
valueType: 'number',
|
|
431
|
+
rules: [
|
|
432
|
+
{
|
|
433
|
+
id: 'r1',
|
|
434
|
+
computation: {
|
|
435
|
+
type: 'direct',
|
|
436
|
+
config: {
|
|
437
|
+
type: 'direct',
|
|
438
|
+
value: 99
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]
|
|
443
|
+
}
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
mockFormRepo.findOne.mockResolvedValue(form);
|
|
449
|
+
mockResultRepo.findOne.mockResolvedValue(null);
|
|
450
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
451
|
+
isDraft: true
|
|
452
|
+
}));
|
|
453
|
+
await service.submitForm({
|
|
454
|
+
formId: 'form-uuid-1',
|
|
455
|
+
data: {
|
|
456
|
+
field1: 'x'
|
|
457
|
+
},
|
|
458
|
+
isDraft: true
|
|
459
|
+
}, (0, _loggedusermock.buildMockUser)(), false);
|
|
460
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
461
|
+
data: {
|
|
462
|
+
field1: 'x'
|
|
463
|
+
},
|
|
464
|
+
isDraft: true
|
|
465
|
+
}));
|
|
466
|
+
});
|
|
467
|
+
it('replaces an existing draft with the final submission (soft-removes the draft, inserts a new record)', async ()=>{
|
|
468
|
+
await buildService(false);
|
|
469
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm());
|
|
470
|
+
const existingDraft = buildFormResult({
|
|
471
|
+
id: 'draft-1',
|
|
472
|
+
isDraft: true
|
|
473
|
+
});
|
|
474
|
+
mockResultRepo.findOne.mockResolvedValue(existingDraft);
|
|
475
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
476
|
+
id: 'final-1',
|
|
477
|
+
isDraft: false
|
|
478
|
+
}));
|
|
479
|
+
const result = await service.submitForm({
|
|
480
|
+
formId: 'form-uuid-1',
|
|
481
|
+
data: {
|
|
482
|
+
field1: 'y'
|
|
483
|
+
}
|
|
484
|
+
}, (0, _loggedusermock.buildMockUser)(), false);
|
|
485
|
+
expect(mockResultRepo.softRemove).toHaveBeenCalledWith(existingDraft);
|
|
486
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
487
|
+
formId: 'form-uuid-1',
|
|
488
|
+
isDraft: false
|
|
489
|
+
}));
|
|
490
|
+
expect(result.id).toBe('final-1');
|
|
491
|
+
});
|
|
492
|
+
it('updates an existing draft in place when re-submitting as a draft', async ()=>{
|
|
493
|
+
await buildService(false);
|
|
494
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm());
|
|
495
|
+
const existingDraft = buildFormResult({
|
|
496
|
+
id: 'draft-1',
|
|
497
|
+
isDraft: true
|
|
498
|
+
});
|
|
499
|
+
mockResultRepo.findOne.mockResolvedValue(existingDraft);
|
|
500
|
+
mockResultRepo.save.mockResolvedValue({
|
|
501
|
+
...existingDraft,
|
|
502
|
+
data: {
|
|
503
|
+
field1: 'z'
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
await service.submitForm({
|
|
507
|
+
formId: 'form-uuid-1',
|
|
508
|
+
data: {
|
|
509
|
+
field1: 'z'
|
|
510
|
+
},
|
|
511
|
+
isDraft: true
|
|
512
|
+
}, (0, _loggedusermock.buildMockUser)(), false);
|
|
513
|
+
expect(mockResultRepo.softRemove).not.toHaveBeenCalled();
|
|
514
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
515
|
+
id: 'draft-1',
|
|
516
|
+
data: {
|
|
517
|
+
field1: 'z'
|
|
518
|
+
}
|
|
519
|
+
}));
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
// ==========================================================================
|
|
523
|
+
// getMyDraft
|
|
524
|
+
// ==========================================================================
|
|
525
|
+
describe('getMyDraft', ()=>{
|
|
526
|
+
it('returns the most recent draft for the user', async ()=>{
|
|
527
|
+
await buildService(false);
|
|
528
|
+
mockResultRepo.findOne.mockResolvedValue(buildFormResult({
|
|
529
|
+
isDraft: true
|
|
530
|
+
}));
|
|
531
|
+
const result = await service.getMyDraft('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
532
|
+
expect(result?.isDraft).toBe(true);
|
|
533
|
+
expect(mockResultRepo.findOne).toHaveBeenCalledWith(expect.objectContaining({
|
|
534
|
+
where: expect.objectContaining({
|
|
535
|
+
formId: 'form-uuid-1',
|
|
536
|
+
isDraft: true
|
|
537
|
+
})
|
|
538
|
+
}));
|
|
539
|
+
});
|
|
540
|
+
it('returns null when the user has no draft', async ()=>{
|
|
541
|
+
await buildService(false);
|
|
542
|
+
mockResultRepo.findOne.mockResolvedValue(null);
|
|
543
|
+
const result = await service.getMyDraft('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
544
|
+
expect(result).toBeNull();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
// ==========================================================================
|
|
548
|
+
// updateDraft
|
|
549
|
+
// ==========================================================================
|
|
550
|
+
describe('updateDraft', ()=>{
|
|
551
|
+
it('throws NotFoundException when the draft does not exist or does not belong to the user', async ()=>{
|
|
552
|
+
await buildService(false);
|
|
553
|
+
mockResultRepo.findOne.mockResolvedValue(null);
|
|
554
|
+
await expect(service.updateDraft('missing-draft', {
|
|
555
|
+
formId: 'form-uuid-1',
|
|
556
|
+
data: {}
|
|
557
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
558
|
+
response: expect.objectContaining({
|
|
559
|
+
messageKey: 'form.result.not.found'
|
|
560
|
+
})
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
it('updates the draft with the latest form schema snapshot and data', async ()=>{
|
|
564
|
+
await buildService(false);
|
|
565
|
+
const existingDraft = buildFormResult({
|
|
566
|
+
id: 'draft-1',
|
|
567
|
+
isDraft: true
|
|
568
|
+
});
|
|
569
|
+
mockResultRepo.findOne.mockResolvedValue(existingDraft);
|
|
570
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
571
|
+
schemaVersion: 2
|
|
572
|
+
}));
|
|
573
|
+
mockResultRepo.save.mockResolvedValue({
|
|
574
|
+
...existingDraft,
|
|
575
|
+
schemaVersion: 2
|
|
576
|
+
});
|
|
577
|
+
const result = await service.updateDraft('draft-1', {
|
|
578
|
+
formId: 'form-uuid-1',
|
|
579
|
+
data: {
|
|
580
|
+
field1: 'updated'
|
|
581
|
+
}
|
|
582
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
583
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
584
|
+
id: 'draft-1',
|
|
585
|
+
schemaVersion: 2
|
|
586
|
+
}));
|
|
587
|
+
expect(result).toBeDefined();
|
|
588
|
+
});
|
|
589
|
+
it('finalizes a draft (isDraft=false) and applies computed fields', async ()=>{
|
|
590
|
+
await buildService(false);
|
|
591
|
+
const existingDraft = buildFormResult({
|
|
592
|
+
id: 'draft-1',
|
|
593
|
+
isDraft: true
|
|
594
|
+
});
|
|
595
|
+
mockResultRepo.findOne.mockResolvedValue(existingDraft);
|
|
596
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
597
|
+
schema: {
|
|
598
|
+
settings: {
|
|
599
|
+
computedFields: [
|
|
600
|
+
{
|
|
601
|
+
id: 'cf-1',
|
|
602
|
+
name: 'Total',
|
|
603
|
+
key: 'total',
|
|
604
|
+
valueType: 'number',
|
|
605
|
+
rules: [
|
|
606
|
+
{
|
|
607
|
+
id: 'r1',
|
|
608
|
+
computation: {
|
|
609
|
+
type: 'direct',
|
|
610
|
+
config: {
|
|
611
|
+
type: 'direct',
|
|
612
|
+
value: 7
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
]
|
|
617
|
+
}
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}));
|
|
622
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
623
|
+
id: 'draft-1',
|
|
624
|
+
isDraft: false
|
|
625
|
+
}));
|
|
626
|
+
await service.updateDraft('draft-1', {
|
|
627
|
+
formId: 'form-uuid-1',
|
|
628
|
+
data: {
|
|
629
|
+
field1: 'final'
|
|
630
|
+
},
|
|
631
|
+
isDraft: false
|
|
632
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
633
|
+
expect(mockResultRepo.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
634
|
+
data: {
|
|
635
|
+
field1: 'final',
|
|
636
|
+
_computed: {
|
|
637
|
+
total: 7
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
isDraft: false
|
|
641
|
+
}));
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
// ==========================================================================
|
|
645
|
+
// getByFormId
|
|
646
|
+
// ==========================================================================
|
|
647
|
+
describe('getByFormId', ()=>{
|
|
648
|
+
it('returns paginated results scoped to the given form', async ()=>{
|
|
649
|
+
await buildService(false);
|
|
650
|
+
const qb = mockResultRepo.createQueryBuilder();
|
|
651
|
+
qb.getManyAndCount.mockResolvedValue([
|
|
652
|
+
[
|
|
653
|
+
buildFormResult()
|
|
654
|
+
],
|
|
655
|
+
1
|
|
656
|
+
]);
|
|
657
|
+
const result = await service.getByFormId('form-uuid-1', (0, _loggedusermock.buildMockUser)(), {
|
|
658
|
+
page: 0,
|
|
659
|
+
pageSize: 10
|
|
660
|
+
});
|
|
661
|
+
expect(qb.where).toHaveBeenCalledWith('form_result.formId = :formId', {
|
|
662
|
+
formId: 'form-uuid-1'
|
|
663
|
+
});
|
|
664
|
+
expect(result.total).toBe(1);
|
|
665
|
+
expect(result.data).toHaveLength(1);
|
|
666
|
+
});
|
|
667
|
+
it('applies default pagination when none is provided', async ()=>{
|
|
668
|
+
await buildService(false);
|
|
669
|
+
const qb = mockResultRepo.createQueryBuilder();
|
|
670
|
+
qb.getManyAndCount.mockResolvedValue([
|
|
671
|
+
[],
|
|
672
|
+
0
|
|
673
|
+
]);
|
|
674
|
+
await service.getByFormId('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
675
|
+
expect(qb.skip).toHaveBeenCalledWith(0);
|
|
676
|
+
expect(qb.take).toHaveBeenCalledWith(10);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
// ==========================================================================
|
|
680
|
+
// hasUserSubmitted
|
|
681
|
+
// ==========================================================================
|
|
682
|
+
describe('hasUserSubmitted', ()=>{
|
|
683
|
+
it('returns true when a non-draft submission exists', async ()=>{
|
|
684
|
+
await buildService(false);
|
|
685
|
+
mockResultRepo.count.mockResolvedValue(1);
|
|
686
|
+
const result = await service.hasUserSubmitted('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
687
|
+
expect(result).toBe(true);
|
|
688
|
+
expect(mockResultRepo.count).toHaveBeenCalledWith(expect.objectContaining({
|
|
689
|
+
where: expect.objectContaining({
|
|
690
|
+
formId: 'form-uuid-1',
|
|
691
|
+
isDraft: false
|
|
692
|
+
})
|
|
693
|
+
}));
|
|
694
|
+
});
|
|
695
|
+
it('returns false when no non-draft submission exists', async ()=>{
|
|
696
|
+
await buildService(false);
|
|
697
|
+
mockResultRepo.count.mockResolvedValue(0);
|
|
698
|
+
const result = await service.hasUserSubmitted('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
699
|
+
expect(result).toBe(false);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
});
|