@flusys/nestjs-form-builder 6.4.0 → 7.0.0-beta.1
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/config/event-actions.js +29 -0
- package/cjs/config/index.js +1 -0
- package/cjs/modules/form-builder.module.js +1 -0
- package/cjs/services/form-result.service.js +35 -0
- package/cjs/services/form-result.service.spec.js +122 -1
- package/config/event-actions.d.ts +8 -0
- package/config/index.d.ts +1 -0
- package/fesm/config/event-actions.js +11 -0
- package/fesm/config/index.js +1 -0
- package/fesm/modules/form-builder.module.js +2 -1
- package/fesm/services/form-result.service.js +36 -1
- package/fesm/services/form-result.service.spec.js +122 -1
- package/interfaces/form-builder-module.interface.d.ts +2 -0
- package/package.json +3 -3
- package/services/form-result.service.d.ts +2 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the form builder module, alongside the
|
|
3
|
+
* CRUD lifecycle actions every ApiService emits.
|
|
4
|
+
*/ "use strict";
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
function _export(target, all) {
|
|
9
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
_export(exports, {
|
|
15
|
+
get FORM_BUILDER_EVENT_ACTIONS () {
|
|
16
|
+
return FORM_BUILDER_EVENT_ACTIONS;
|
|
17
|
+
},
|
|
18
|
+
get FORM_BUILDER_EVENT_ENTITIES () {
|
|
19
|
+
return FORM_BUILDER_EVENT_ENTITIES;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const FORM_BUILDER_EVENT_ACTIONS = {
|
|
23
|
+
SUBMITTED: 'submitted',
|
|
24
|
+
DRAFT_SAVED: 'draft-saved'
|
|
25
|
+
};
|
|
26
|
+
const FORM_BUILDER_EVENT_ENTITIES = {
|
|
27
|
+
FORM: 'form',
|
|
28
|
+
FORM_RESULT: 'form_result'
|
|
29
|
+
};
|
package/cjs/config/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", {
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
|
+
_export_star(require("./event-actions"), exports);
|
|
5
6
|
_export_star(require("./form-builder.constants"), exports);
|
|
6
7
|
_export_star(require("./message-keys"), exports);
|
|
7
8
|
function _export_star(from, to) {
|
|
@@ -80,6 +80,7 @@ let FormBuilderModule = class FormBuilderModule {
|
|
|
80
80
|
useValue: options
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
|
+
providers.push((0, _modules.createModuleEventsProvider)('form-builder', _formbuilderconstants.FORM_BUILDER_MODULE_OPTIONS));
|
|
83
84
|
return providers;
|
|
84
85
|
}
|
|
85
86
|
static buildAsyncProviders(options) {
|
|
@@ -189,6 +189,7 @@ let FormResultService = class FormResultService extends _classes.ApiService {
|
|
|
189
189
|
const form = await this.getActiveForm(dto.formId);
|
|
190
190
|
await this.validateSubmissionAccess(form, user, isPublic);
|
|
191
191
|
const isDraft = dto.isDraft ?? false;
|
|
192
|
+
const staleResults = [];
|
|
192
193
|
// Handle existing draft for authenticated users
|
|
193
194
|
if (user?.id) {
|
|
194
195
|
const existingDraft = await this.repository.findOne({
|
|
@@ -203,9 +204,14 @@ let FormResultService = class FormResultService extends _classes.ApiService {
|
|
|
203
204
|
submittedAt: new Date()
|
|
204
205
|
});
|
|
205
206
|
const saved = await this.repository.save(existingDraft);
|
|
207
|
+
await this.invalidateResultCache([
|
|
208
|
+
saved
|
|
209
|
+
]);
|
|
210
|
+
await this.publishSubmissionEvent(saved, dto.formId, true, user);
|
|
206
211
|
return this.convertEntityToResponseDto(saved, false);
|
|
207
212
|
}
|
|
208
213
|
await this.repository.softRemove(existingDraft);
|
|
214
|
+
staleResults.push(existingDraft);
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
const saved = await this.repository.save({
|
|
@@ -217,6 +223,11 @@ let FormResultService = class FormResultService extends _classes.ApiService {
|
|
|
217
223
|
submittedAt: new Date(),
|
|
218
224
|
isDraft
|
|
219
225
|
});
|
|
226
|
+
await this.invalidateResultCache([
|
|
227
|
+
...staleResults,
|
|
228
|
+
saved
|
|
229
|
+
]);
|
|
230
|
+
await this.publishSubmissionEvent(saved, dto.formId, isDraft, user);
|
|
220
231
|
return this.convertEntityToResponseDto(saved, false);
|
|
221
232
|
}
|
|
222
233
|
async getMyDraft(formId, user) {
|
|
@@ -254,8 +265,32 @@ let FormResultService = class FormResultService extends _classes.ApiService {
|
|
|
254
265
|
isDraft
|
|
255
266
|
});
|
|
256
267
|
const saved = await this.repository.save(draft);
|
|
268
|
+
await this.invalidateResultCache([
|
|
269
|
+
saved
|
|
270
|
+
]);
|
|
271
|
+
await this.publishSubmissionEvent(saved, dto.formId, isDraft, user);
|
|
257
272
|
return this.convertEntityToResponseDto(saved, false);
|
|
258
273
|
}
|
|
274
|
+
async publishSubmissionEvent(result, formId, isDraft, user) {
|
|
275
|
+
await this.publishDomainAction(isDraft ? _config.FORM_BUILDER_EVENT_ACTIONS.DRAFT_SAVED : _config.FORM_BUILDER_EVENT_ACTIONS.SUBMITTED, {
|
|
276
|
+
ids: [
|
|
277
|
+
result.id
|
|
278
|
+
],
|
|
279
|
+
data: result,
|
|
280
|
+
metadata: {
|
|
281
|
+
formId,
|
|
282
|
+
isDraft,
|
|
283
|
+
schemaVersion: result.schemaVersion
|
|
284
|
+
},
|
|
285
|
+
user
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async invalidateResultCache(results) {
|
|
289
|
+
await Promise.all([
|
|
290
|
+
this.clearCacheForAll(),
|
|
291
|
+
this.clearCacheForId(results)
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
259
294
|
async getByFormId(formId, user, pagination) {
|
|
260
295
|
await this.ensureDataSourceRepository();
|
|
261
296
|
const query = this.repository.createQueryBuilder(this.entityName).where('form_result.formId = :formId', {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
6
7
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
7
8
|
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
8
9
|
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
@@ -108,6 +109,15 @@ describe('FormResultService', ()=>{
|
|
|
108
109
|
let mockResultRepo;
|
|
109
110
|
let mockFormRepo;
|
|
110
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
|
+
});
|
|
111
121
|
async function buildService(companyFeatureEnabled = false) {
|
|
112
122
|
mockResultRepo = (0, _repositorymock.createMockRepository)();
|
|
113
123
|
mockFormRepo = (0, _repositorymock.createMockRepository)();
|
|
@@ -138,7 +148,7 @@ describe('FormResultService', ()=>{
|
|
|
138
148
|
},
|
|
139
149
|
{
|
|
140
150
|
provide: _modules.UtilsService,
|
|
141
|
-
useValue:
|
|
151
|
+
useValue: mockUtilsService
|
|
142
152
|
},
|
|
143
153
|
{
|
|
144
154
|
provide: _formbuilderconfigservice.FormBuilderConfigService,
|
|
@@ -417,6 +427,43 @@ describe('FormResultService', ()=>{
|
|
|
417
427
|
isDraft: false
|
|
418
428
|
}));
|
|
419
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
|
+
});
|
|
420
467
|
it('does not compute fields when saving as a draft', async ()=>{
|
|
421
468
|
await buildService(false);
|
|
422
469
|
const form = buildForm({
|
|
@@ -699,4 +746,78 @@ describe('FormResultService', ()=>{
|
|
|
699
746
|
expect(result).toBe(false);
|
|
700
747
|
});
|
|
701
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
|
+
});
|
|
702
823
|
});
|
package/config/index.d.ts
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the form builder module, alongside the
|
|
3
|
+
* CRUD lifecycle actions every ApiService emits.
|
|
4
|
+
*/ export const FORM_BUILDER_EVENT_ACTIONS = {
|
|
5
|
+
SUBMITTED: 'submitted',
|
|
6
|
+
DRAFT_SAVED: 'draft-saved'
|
|
7
|
+
};
|
|
8
|
+
/** Every entity the form builder module publishes under */ export const FORM_BUILDER_EVENT_ENTITIES = {
|
|
9
|
+
FORM: 'form',
|
|
10
|
+
FORM_RESULT: 'form_result'
|
|
11
|
+
};
|
package/fesm/config/index.js
CHANGED
|
@@ -17,7 +17,7 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
17
17
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
18
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
19
|
}
|
|
20
|
-
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
20
|
+
import { CacheModule, UtilsModule, createModuleEventsProvider } from '@flusys/nestjs-shared/modules';
|
|
21
21
|
import { Module } from '@nestjs/common';
|
|
22
22
|
import { FORM_BUILDER_MODULE_OPTIONS } from '../config/form-builder.constants';
|
|
23
23
|
import { FormController, FormResultController } from '../controllers';
|
|
@@ -70,6 +70,7 @@ export class FormBuilderModule {
|
|
|
70
70
|
useValue: options
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
providers.push(createModuleEventsProvider('form-builder', FORM_BUILDER_MODULE_OPTIONS));
|
|
73
74
|
return providers;
|
|
74
75
|
}
|
|
75
76
|
static buildAsyncProviders(options) {
|
|
@@ -31,7 +31,7 @@ import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
|
31
31
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
32
32
|
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException, Scope, UnauthorizedException } from '@nestjs/common';
|
|
33
33
|
import { IsNull } from 'typeorm';
|
|
34
|
-
import { FORM_MESSAGES, FORM_RESULT_MESSAGES } from '../config';
|
|
34
|
+
import { FORM_BUILDER_EVENT_ACTIONS, FORM_MESSAGES, FORM_RESULT_MESSAGES } from '../config';
|
|
35
35
|
import { SubmitFormDto } from '../dtos/form-result.dto';
|
|
36
36
|
import { Form, FormResult, FormWithCompany } from '../entities';
|
|
37
37
|
import { FormAccessType } from '../enums/form-access-type.enum';
|
|
@@ -179,6 +179,7 @@ export class FormResultService extends ApiService {
|
|
|
179
179
|
const form = await this.getActiveForm(dto.formId);
|
|
180
180
|
await this.validateSubmissionAccess(form, user, isPublic);
|
|
181
181
|
const isDraft = dto.isDraft ?? false;
|
|
182
|
+
const staleResults = [];
|
|
182
183
|
// Handle existing draft for authenticated users
|
|
183
184
|
if (user?.id) {
|
|
184
185
|
const existingDraft = await this.repository.findOne({
|
|
@@ -193,9 +194,14 @@ export class FormResultService extends ApiService {
|
|
|
193
194
|
submittedAt: new Date()
|
|
194
195
|
});
|
|
195
196
|
const saved = await this.repository.save(existingDraft);
|
|
197
|
+
await this.invalidateResultCache([
|
|
198
|
+
saved
|
|
199
|
+
]);
|
|
200
|
+
await this.publishSubmissionEvent(saved, dto.formId, true, user);
|
|
196
201
|
return this.convertEntityToResponseDto(saved, false);
|
|
197
202
|
}
|
|
198
203
|
await this.repository.softRemove(existingDraft);
|
|
204
|
+
staleResults.push(existingDraft);
|
|
199
205
|
}
|
|
200
206
|
}
|
|
201
207
|
const saved = await this.repository.save({
|
|
@@ -207,6 +213,11 @@ export class FormResultService extends ApiService {
|
|
|
207
213
|
submittedAt: new Date(),
|
|
208
214
|
isDraft
|
|
209
215
|
});
|
|
216
|
+
await this.invalidateResultCache([
|
|
217
|
+
...staleResults,
|
|
218
|
+
saved
|
|
219
|
+
]);
|
|
220
|
+
await this.publishSubmissionEvent(saved, dto.formId, isDraft, user);
|
|
210
221
|
return this.convertEntityToResponseDto(saved, false);
|
|
211
222
|
}
|
|
212
223
|
async getMyDraft(formId, user) {
|
|
@@ -244,8 +255,32 @@ export class FormResultService extends ApiService {
|
|
|
244
255
|
isDraft
|
|
245
256
|
});
|
|
246
257
|
const saved = await this.repository.save(draft);
|
|
258
|
+
await this.invalidateResultCache([
|
|
259
|
+
saved
|
|
260
|
+
]);
|
|
261
|
+
await this.publishSubmissionEvent(saved, dto.formId, isDraft, user);
|
|
247
262
|
return this.convertEntityToResponseDto(saved, false);
|
|
248
263
|
}
|
|
264
|
+
async publishSubmissionEvent(result, formId, isDraft, user) {
|
|
265
|
+
await this.publishDomainAction(isDraft ? FORM_BUILDER_EVENT_ACTIONS.DRAFT_SAVED : FORM_BUILDER_EVENT_ACTIONS.SUBMITTED, {
|
|
266
|
+
ids: [
|
|
267
|
+
result.id
|
|
268
|
+
],
|
|
269
|
+
data: result,
|
|
270
|
+
metadata: {
|
|
271
|
+
formId,
|
|
272
|
+
isDraft,
|
|
273
|
+
schemaVersion: result.schemaVersion
|
|
274
|
+
},
|
|
275
|
+
user
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async invalidateResultCache(results) {
|
|
279
|
+
await Promise.all([
|
|
280
|
+
this.clearCacheForAll(),
|
|
281
|
+
this.clearCacheForId(results)
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
249
284
|
async getByFormId(formId, user, pagination) {
|
|
250
285
|
await this.ensureDataSourceRepository();
|
|
251
286
|
const query = this.repository.createQueryBuilder(this.entityName).where('form_result.formId = :formId', {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Test } from '@nestjs/testing';
|
|
2
|
+
import { EventBusRegistry } from '@flusys/nestjs-shared/classes';
|
|
2
3
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
4
|
import { createMockQueryBuilder, createMockRepository } from '@test-utils/mocks/repository.mock';
|
|
4
5
|
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
@@ -63,6 +64,15 @@ describe('FormResultService', ()=>{
|
|
|
63
64
|
let mockResultRepo;
|
|
64
65
|
let mockFormRepo;
|
|
65
66
|
let mockFormBuilderConfig;
|
|
67
|
+
const mockUtilsService = {
|
|
68
|
+
clearCache: jest.fn(),
|
|
69
|
+
getCacheKey: jest.fn().mockReturnValue('cache-key'),
|
|
70
|
+
trackCacheKey: jest.fn()
|
|
71
|
+
};
|
|
72
|
+
// Call history is shared across tests in this file; reset it so each cache assertion is exact.
|
|
73
|
+
beforeEach(()=>{
|
|
74
|
+
mockUtilsService.clearCache.mockClear();
|
|
75
|
+
});
|
|
66
76
|
async function buildService(companyFeatureEnabled = false) {
|
|
67
77
|
mockResultRepo = createMockRepository();
|
|
68
78
|
mockFormRepo = createMockRepository();
|
|
@@ -93,7 +103,7 @@ describe('FormResultService', ()=>{
|
|
|
93
103
|
},
|
|
94
104
|
{
|
|
95
105
|
provide: UtilsService,
|
|
96
|
-
useValue:
|
|
106
|
+
useValue: mockUtilsService
|
|
97
107
|
},
|
|
98
108
|
{
|
|
99
109
|
provide: FormBuilderConfigService,
|
|
@@ -372,6 +382,43 @@ describe('FormResultService', ()=>{
|
|
|
372
382
|
isDraft: false
|
|
373
383
|
}));
|
|
374
384
|
});
|
|
385
|
+
it('invalidates the result list cache and the new entry after a submission', async ()=>{
|
|
386
|
+
await buildService(false);
|
|
387
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm());
|
|
388
|
+
mockResultRepo.findOne.mockResolvedValue(null);
|
|
389
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
390
|
+
id: 'result-1'
|
|
391
|
+
}));
|
|
392
|
+
await service.submitForm({
|
|
393
|
+
formId: 'form-uuid-1',
|
|
394
|
+
data: {
|
|
395
|
+
field1: 'x'
|
|
396
|
+
}
|
|
397
|
+
}, buildMockUser(), false);
|
|
398
|
+
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('form_result', expect.anything());
|
|
399
|
+
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('form_result', expect.anything(), 'result-1');
|
|
400
|
+
});
|
|
401
|
+
it('invalidates the soft-removed draft as well when a draft is finalized into a new record', async ()=>{
|
|
402
|
+
await buildService(false);
|
|
403
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm());
|
|
404
|
+
mockResultRepo.findOne.mockResolvedValue(buildFormResult({
|
|
405
|
+
id: 'draft-1',
|
|
406
|
+
isDraft: true
|
|
407
|
+
}));
|
|
408
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
409
|
+
id: 'result-2'
|
|
410
|
+
}));
|
|
411
|
+
await service.submitForm({
|
|
412
|
+
formId: 'form-uuid-1',
|
|
413
|
+
data: {
|
|
414
|
+
field1: 'x'
|
|
415
|
+
},
|
|
416
|
+
isDraft: false
|
|
417
|
+
}, buildMockUser(), false);
|
|
418
|
+
expect(mockResultRepo.softRemove).toHaveBeenCalled();
|
|
419
|
+
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('form_result', expect.anything(), 'draft-1');
|
|
420
|
+
expect(mockUtilsService.clearCache).toHaveBeenCalledWith('form_result', expect.anything(), 'result-2');
|
|
421
|
+
});
|
|
375
422
|
it('does not compute fields when saving as a draft', async ()=>{
|
|
376
423
|
await buildService(false);
|
|
377
424
|
const form = buildForm({
|
|
@@ -654,4 +701,78 @@ describe('FormResultService', ()=>{
|
|
|
654
701
|
expect(result).toBe(false);
|
|
655
702
|
});
|
|
656
703
|
});
|
|
704
|
+
describe('domain events', ()=>{
|
|
705
|
+
let publish;
|
|
706
|
+
beforeEach(()=>{
|
|
707
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
708
|
+
EventBusRegistry.reset();
|
|
709
|
+
EventBusRegistry.setBus({
|
|
710
|
+
publish,
|
|
711
|
+
subscribe: jest.fn(),
|
|
712
|
+
close: jest.fn()
|
|
713
|
+
});
|
|
714
|
+
EventBusRegistry.configureModule('form-builder', {
|
|
715
|
+
enabled: true
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
afterEach(()=>{
|
|
719
|
+
EventBusRegistry.reset();
|
|
720
|
+
});
|
|
721
|
+
it('publishes form-builder.form_result.submitted for a final submission', async ()=>{
|
|
722
|
+
await buildService(false);
|
|
723
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
724
|
+
accessType: FormAccessType.PUBLIC
|
|
725
|
+
}));
|
|
726
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
727
|
+
isDraft: false
|
|
728
|
+
}));
|
|
729
|
+
await service.submitForm({
|
|
730
|
+
formId: 'form-uuid-1',
|
|
731
|
+
data: {}
|
|
732
|
+
}, null, true);
|
|
733
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
734
|
+
name: 'form-builder.form_result.submitted',
|
|
735
|
+
action: 'submitted',
|
|
736
|
+
payload: expect.objectContaining({
|
|
737
|
+
ids: [
|
|
738
|
+
'result-uuid-1'
|
|
739
|
+
],
|
|
740
|
+
metadata: expect.objectContaining({
|
|
741
|
+
formId: 'form-uuid-1',
|
|
742
|
+
isDraft: false
|
|
743
|
+
})
|
|
744
|
+
})
|
|
745
|
+
}));
|
|
746
|
+
});
|
|
747
|
+
it('publishes form-builder.form_result.draft-saved when the submission is a draft', async ()=>{
|
|
748
|
+
await buildService(false);
|
|
749
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
750
|
+
accessType: FormAccessType.PUBLIC
|
|
751
|
+
}));
|
|
752
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult({
|
|
753
|
+
isDraft: true
|
|
754
|
+
}));
|
|
755
|
+
await service.submitForm({
|
|
756
|
+
formId: 'form-uuid-1',
|
|
757
|
+
data: {},
|
|
758
|
+
isDraft: true
|
|
759
|
+
}, null, true);
|
|
760
|
+
expect(publish.mock.calls[0][0].name).toBe('form-builder.form_result.draft-saved');
|
|
761
|
+
});
|
|
762
|
+
it('does not publish when the module has events disabled', async ()=>{
|
|
763
|
+
EventBusRegistry.configureModule('form-builder', {
|
|
764
|
+
enabled: false
|
|
765
|
+
});
|
|
766
|
+
await buildService(false);
|
|
767
|
+
mockFormRepo.findOne.mockResolvedValue(buildForm({
|
|
768
|
+
accessType: FormAccessType.PUBLIC
|
|
769
|
+
}));
|
|
770
|
+
mockResultRepo.save.mockResolvedValue(buildFormResult());
|
|
771
|
+
await service.submitForm({
|
|
772
|
+
formId: 'form-uuid-1',
|
|
773
|
+
data: {}
|
|
774
|
+
}, null, true);
|
|
775
|
+
expect(publish).not.toHaveBeenCalled();
|
|
776
|
+
});
|
|
777
|
+
});
|
|
657
778
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
2
2
|
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
3
|
+
import { IModuleEventsConfig } from '@flusys/nestjs-shared/interfaces';
|
|
3
4
|
export interface IFormBuilderConfig extends IDataSourceServiceOptions {
|
|
5
|
+
events?: IModuleEventsConfig;
|
|
4
6
|
}
|
|
5
7
|
export interface FormBuilderModuleOptions extends IDynamicModuleConfig {
|
|
6
8
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-form-builder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-beta.1",
|
|
4
4
|
"description": "Dynamic form builder module with schema versioning and access control",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"express": "^5.0.0"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@flusys/nestjs-core": "
|
|
88
|
-
"@flusys/nestjs-shared": "
|
|
87
|
+
"@flusys/nestjs-core": "7.0.0-beta.1",
|
|
88
|
+
"@flusys/nestjs-shared": "7.0.0-beta.1"
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -33,6 +33,8 @@ export declare class FormResultService extends ApiService<CreateFormResultDto, U
|
|
|
33
33
|
submitForm(dto: SubmitFormDto, user: ILoggedUserInfo | null, isPublic?: boolean): Promise<IFormResult>;
|
|
34
34
|
getMyDraft(formId: string, user: ILoggedUserInfo): Promise<IFormResult | null>;
|
|
35
35
|
updateDraft(draftId: string, dto: SubmitFormDto, user: ILoggedUserInfo): Promise<IFormResult>;
|
|
36
|
+
private publishSubmissionEvent;
|
|
37
|
+
private invalidateResultCache;
|
|
36
38
|
getByFormId(formId: string, user: ILoggedUserInfo | null, pagination?: {
|
|
37
39
|
page?: number;
|
|
38
40
|
pageSize?: number;
|