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