@flusys/nestjs-form-builder 1.0.0-beta → 1.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.
- package/README.md +722 -0
- package/cjs/controllers/form-result.controller.js +67 -5
- package/cjs/controllers/form.controller.js +48 -15
- package/cjs/docs/form-builder-swagger.config.js +6 -100
- package/cjs/dtos/form-result.dto.js +6 -93
- package/cjs/dtos/form.dto.js +21 -163
- package/cjs/entities/form-with-company.entity.js +12 -2
- package/cjs/entities/form.entity.js +103 -3
- package/cjs/entities/index.js +28 -16
- package/cjs/index.js +1 -0
- package/cjs/interfaces/form-result.interface.js +1 -6
- package/cjs/modules/form-builder.module.js +57 -83
- package/cjs/services/form-builder-config.service.js +6 -16
- package/cjs/services/form-builder-datasource.provider.js +19 -59
- package/cjs/services/form-result.service.js +107 -181
- package/cjs/services/form.service.js +56 -72
- package/cjs/utils/computed-field.utils.js +17 -29
- package/cjs/utils/permission.utils.js +11 -16
- package/controllers/form-result.controller.d.ts +10 -12
- package/dtos/form-result.dto.d.ts +2 -19
- package/dtos/form.dto.d.ts +6 -32
- package/entities/form-with-company.entity.d.ts +2 -2
- package/entities/form.entity.d.ts +12 -2
- package/entities/index.d.ts +7 -2
- package/fesm/controllers/form-result.controller.js +69 -7
- package/fesm/controllers/form.controller.js +50 -17
- package/fesm/docs/form-builder-swagger.config.js +6 -100
- package/fesm/dtos/form-result.dto.js +9 -99
- package/fesm/dtos/form.dto.js +22 -165
- package/fesm/entities/form-with-company.entity.js +12 -2
- package/fesm/entities/form.entity.js +104 -4
- package/fesm/entities/index.js +18 -24
- package/fesm/index.js +2 -0
- package/fesm/modules/form-builder.module.js +57 -83
- package/fesm/services/form-builder-config.service.js +6 -16
- package/fesm/services/form-builder-datasource.provider.js +19 -59
- package/fesm/services/form-result.service.js +107 -181
- package/fesm/services/form.service.js +56 -72
- package/fesm/utils/computed-field.utils.js +17 -29
- package/fesm/utils/permission.utils.js +2 -9
- package/index.d.ts +1 -0
- package/interfaces/form-builder-module.interface.d.ts +4 -7
- package/interfaces/form-result.interface.d.ts +2 -9
- package/interfaces/form.interface.d.ts +2 -10
- package/modules/form-builder.module.d.ts +4 -3
- package/package.json +3 -3
- package/services/form-builder-config.service.d.ts +5 -3
- package/services/form-builder-datasource.provider.d.ts +3 -6
- package/services/form-result.service.d.ts +5 -0
- package/services/form.service.d.ts +13 -10
- package/utils/permission.utils.d.ts +0 -2
- package/cjs/entities/form-base.entity.js +0 -113
- package/entities/form-base.entity.d.ts +0 -13
- package/fesm/entities/form-base.entity.js +0 -106
|
@@ -36,27 +36,68 @@ import { FormBuilderDataSourceProvider } from './form-builder-datasource.provide
|
|
|
36
36
|
import { validateUserPermissions } from '../utils/permission.utils';
|
|
37
37
|
import { calculateComputedFields } from '../utils/computed-field.utils';
|
|
38
38
|
export class FormResultService extends RequestScopedApiService {
|
|
39
|
-
|
|
40
|
-
* Resolve entity class for this service
|
|
41
|
-
* @returns FormResult (same entity regardless of company feature)
|
|
42
|
-
*/ resolveEntity() {
|
|
39
|
+
resolveEntity() {
|
|
43
40
|
return FormResult;
|
|
44
41
|
}
|
|
45
|
-
|
|
46
|
-
* Get DataSource provider for this service
|
|
47
|
-
* @returns FormBuilderDataSourceProvider instance
|
|
48
|
-
*/ getDataSourceProvider() {
|
|
42
|
+
getDataSourceProvider() {
|
|
49
43
|
return this.dataSourceProvider;
|
|
50
44
|
}
|
|
51
|
-
|
|
52
|
-
* Initialize form repository for form lookups
|
|
53
|
-
*/ async ensureFormRepositoryInitialized() {
|
|
45
|
+
async ensureFormRepositoryInitialized() {
|
|
54
46
|
if (!this.formRepository) {
|
|
55
47
|
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
56
48
|
const formEntity = enableCompanyFeature ? FormWithCompany : Form;
|
|
57
49
|
this.formRepository = await this.dataSourceProvider.getRepository(formEntity);
|
|
58
50
|
}
|
|
59
51
|
}
|
|
52
|
+
async getActiveForm(formId) {
|
|
53
|
+
await this.ensureFormRepositoryInitialized();
|
|
54
|
+
const form = await this.formRepository.findOne({
|
|
55
|
+
where: {
|
|
56
|
+
id: formId,
|
|
57
|
+
isActive: true,
|
|
58
|
+
deletedAt: IsNull()
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (!form) {
|
|
62
|
+
throw new NotFoundException('Form not found or inactive');
|
|
63
|
+
}
|
|
64
|
+
return form;
|
|
65
|
+
}
|
|
66
|
+
applyCompanyFilterToQuery(query, user) {
|
|
67
|
+
if (this.formBuilderConfig.isCompanyFeatureEnabled() && user?.companyId) {
|
|
68
|
+
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
69
|
+
companyId: user.companyId
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async validateSubmissionAccess(form, user, isPublic) {
|
|
74
|
+
if (isPublic) {
|
|
75
|
+
if (form.accessType !== FormAccessType.PUBLIC) {
|
|
76
|
+
throw new UnauthorizedException('This form is not available for public submission');
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (form.accessType === FormAccessType.PUBLIC) return;
|
|
81
|
+
if (!user) {
|
|
82
|
+
throw new UnauthorizedException('Authentication required to submit this form');
|
|
83
|
+
}
|
|
84
|
+
if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
85
|
+
const hasPermission = await validateUserPermissions(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled(), this.logger, 'submitting form', form.id);
|
|
86
|
+
if (!hasPermission) {
|
|
87
|
+
throw new ForbiddenException('You do not have permission to submit this form');
|
|
88
|
+
}
|
|
89
|
+
} else if (form.accessType !== FormAccessType.AUTHENTICATED) {
|
|
90
|
+
throw new BadRequestException('Invalid form access type');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
buildUserDraftWhere(formId, userId) {
|
|
94
|
+
return {
|
|
95
|
+
formId,
|
|
96
|
+
submittedById: userId,
|
|
97
|
+
isDraft: true,
|
|
98
|
+
deletedAt: IsNull()
|
|
99
|
+
};
|
|
100
|
+
}
|
|
60
101
|
// Query Customization
|
|
61
102
|
async getSelectQuery(query, _user, select) {
|
|
62
103
|
if (!select || !select.length) {
|
|
@@ -81,22 +122,28 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
81
122
|
isRaw: false
|
|
82
123
|
};
|
|
83
124
|
}
|
|
84
|
-
|
|
85
|
-
* Override: Extra query manipulation - Auto-filter by user's company via Form
|
|
86
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
125
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
87
126
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
88
|
-
|
|
89
|
-
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
90
|
-
if (enableCompanyFeature && user?.companyId) {
|
|
91
|
-
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
92
|
-
companyId: user.companyId
|
|
93
|
-
});
|
|
94
|
-
}
|
|
127
|
+
this.applyCompanyFilterToQuery(query, user);
|
|
95
128
|
return result;
|
|
96
129
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
applyComputedFields(data, form, isDraft) {
|
|
131
|
+
if (isDraft) {
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
const schema = form.schema;
|
|
135
|
+
const settings = schema?.settings;
|
|
136
|
+
const computedFields = settings?.computedFields;
|
|
137
|
+
if (!computedFields || computedFields.length === 0) {
|
|
138
|
+
return data;
|
|
139
|
+
}
|
|
140
|
+
const computedValues = calculateComputedFields(data, computedFields);
|
|
141
|
+
return {
|
|
142
|
+
...data,
|
|
143
|
+
_computed: computedValues
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
convertEntityToResponseDto(entity, _isRaw) {
|
|
100
147
|
return {
|
|
101
148
|
id: entity.id,
|
|
102
149
|
formId: entity.formId,
|
|
@@ -116,137 +163,55 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
116
163
|
};
|
|
117
164
|
}
|
|
118
165
|
// Form Submission
|
|
119
|
-
|
|
120
|
-
* Submit a form (authenticated or public)
|
|
121
|
-
*/ async submitForm(dto, user, isPublic = false) {
|
|
166
|
+
async submitForm(dto, user, isPublic = false) {
|
|
122
167
|
await this.ensureRepositoryInitialized();
|
|
123
|
-
await this.
|
|
124
|
-
|
|
125
|
-
const form = await this.formRepository.findOne({
|
|
126
|
-
where: {
|
|
127
|
-
id: dto.formId,
|
|
128
|
-
isActive: true,
|
|
129
|
-
deletedAt: IsNull()
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
if (!form) {
|
|
133
|
-
throw new NotFoundException('Form not found or inactive');
|
|
134
|
-
}
|
|
135
|
-
// Validate access
|
|
136
|
-
if (isPublic) {
|
|
137
|
-
if (form.accessType !== FormAccessType.PUBLIC) {
|
|
138
|
-
throw new UnauthorizedException('This form is not available for public submission');
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
// For non-public submissions
|
|
142
|
-
switch(form.accessType){
|
|
143
|
-
case FormAccessType.PUBLIC:
|
|
144
|
-
break;
|
|
145
|
-
case FormAccessType.AUTHENTICATED:
|
|
146
|
-
if (!user) {
|
|
147
|
-
throw new UnauthorizedException('Authentication required to submit this form');
|
|
148
|
-
}
|
|
149
|
-
break;
|
|
150
|
-
case FormAccessType.ACTION_GROUP:
|
|
151
|
-
if (!user) {
|
|
152
|
-
throw new UnauthorizedException('Authentication required to submit this form');
|
|
153
|
-
}
|
|
154
|
-
// Validate user has at least one of the required permissions
|
|
155
|
-
if (form.actionGroups?.length) {
|
|
156
|
-
const hasPermission = await validateUserPermissions(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled(), this.logger, 'submitting form', form.id);
|
|
157
|
-
if (!hasPermission) {
|
|
158
|
-
throw new ForbiddenException('You do not have permission to submit this form');
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
break;
|
|
162
|
-
default:
|
|
163
|
-
throw new BadRequestException('Invalid form access type');
|
|
164
|
-
}
|
|
165
|
-
}
|
|
168
|
+
const form = await this.getActiveForm(dto.formId);
|
|
169
|
+
await this.validateSubmissionAccess(form, user, isPublic);
|
|
166
170
|
const isDraft = dto.isDraft ?? false;
|
|
167
|
-
//
|
|
171
|
+
// Handle existing draft for authenticated users
|
|
168
172
|
if (user?.id) {
|
|
169
|
-
// Check for existing draft
|
|
170
173
|
const existingDraft = await this.repository.findOne({
|
|
171
|
-
where:
|
|
172
|
-
formId: dto.formId,
|
|
173
|
-
submittedById: user.id,
|
|
174
|
-
isDraft: true,
|
|
175
|
-
deletedAt: IsNull()
|
|
176
|
-
}
|
|
174
|
+
where: this.buildUserDraftWhere(dto.formId, user.id)
|
|
177
175
|
});
|
|
178
176
|
if (existingDraft) {
|
|
179
177
|
if (isDraft) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
Object.assign(existingDraft, {
|
|
179
|
+
data: dto.data,
|
|
180
|
+
schemaVersionSnapshot: form.schema,
|
|
181
|
+
schemaVersion: form.schemaVersion,
|
|
182
|
+
submittedAt: new Date(),
|
|
183
|
+
metadata: dto.metadata ?? existingDraft.metadata
|
|
184
|
+
});
|
|
186
185
|
const saved = await this.repository.save(existingDraft);
|
|
187
186
|
return this.convertEntityToResponseDto(saved, false);
|
|
188
|
-
} else {
|
|
189
|
-
// Final submission: delete existing draft first
|
|
190
|
-
await this.repository.softRemove(existingDraft);
|
|
191
187
|
}
|
|
188
|
+
await this.repository.softRemove(existingDraft);
|
|
192
189
|
}
|
|
193
190
|
}
|
|
194
|
-
|
|
195
|
-
let finalData = dto.data;
|
|
196
|
-
if (!isDraft) {
|
|
197
|
-
const schema = form.schema;
|
|
198
|
-
const settings = schema?.settings;
|
|
199
|
-
const computedFields = settings?.computedFields;
|
|
200
|
-
if (computedFields && computedFields.length > 0) {
|
|
201
|
-
const computedValues = calculateComputedFields(dto.data, computedFields);
|
|
202
|
-
// Merge computed values with submitted data (computed fields use their key as the property name)
|
|
203
|
-
finalData = {
|
|
204
|
-
...dto.data,
|
|
205
|
-
_computed: computedValues
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
// Create the result with schema snapshot
|
|
210
|
-
const resultData = {
|
|
191
|
+
const saved = await this.repository.save({
|
|
211
192
|
formId: dto.formId,
|
|
212
193
|
schemaVersionSnapshot: form.schema,
|
|
213
194
|
schemaVersion: form.schemaVersion,
|
|
214
|
-
data:
|
|
195
|
+
data: this.applyComputedFields(dto.data, form, isDraft),
|
|
215
196
|
submittedById: user?.id ?? null,
|
|
216
197
|
submittedAt: new Date(),
|
|
217
198
|
isDraft,
|
|
218
199
|
metadata: dto.metadata ?? null
|
|
219
|
-
};
|
|
220
|
-
const saved = await this.repository.save(resultData);
|
|
200
|
+
});
|
|
221
201
|
return this.convertEntityToResponseDto(saved, false);
|
|
222
202
|
}
|
|
223
|
-
|
|
224
|
-
* Get user's draft for a specific form
|
|
225
|
-
* Returns the most recent draft if exists, null otherwise
|
|
226
|
-
*/ async getMyDraft(formId, user) {
|
|
203
|
+
async getMyDraft(formId, user) {
|
|
227
204
|
await this.ensureRepositoryInitialized();
|
|
228
205
|
const draft = await this.repository.findOne({
|
|
229
|
-
where:
|
|
230
|
-
formId,
|
|
231
|
-
submittedById: user.id,
|
|
232
|
-
isDraft: true,
|
|
233
|
-
deletedAt: IsNull()
|
|
234
|
-
},
|
|
206
|
+
where: this.buildUserDraftWhere(formId, user.id),
|
|
235
207
|
order: {
|
|
236
208
|
updatedAt: 'DESC'
|
|
237
209
|
}
|
|
238
210
|
});
|
|
239
|
-
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
return this.convertEntityToResponseDto(draft, false);
|
|
211
|
+
return draft ? this.convertEntityToResponseDto(draft, false) : null;
|
|
243
212
|
}
|
|
244
|
-
|
|
245
|
-
* Update existing draft or convert to submission
|
|
246
|
-
*/ async updateDraft(draftId, dto, user) {
|
|
213
|
+
async updateDraft(draftId, dto, user) {
|
|
247
214
|
await this.ensureRepositoryInitialized();
|
|
248
|
-
await this.ensureFormRepositoryInitialized();
|
|
249
|
-
// Find the draft
|
|
250
215
|
const draft = await this.repository.findOne({
|
|
251
216
|
where: {
|
|
252
217
|
id: draftId,
|
|
@@ -257,74 +222,35 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
257
222
|
if (!draft) {
|
|
258
223
|
throw new NotFoundException('Draft not found');
|
|
259
224
|
}
|
|
260
|
-
|
|
261
|
-
const form = await this.formRepository.findOne({
|
|
262
|
-
where: {
|
|
263
|
-
id: dto.formId,
|
|
264
|
-
isActive: true,
|
|
265
|
-
deletedAt: IsNull()
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
if (!form) {
|
|
269
|
-
throw new NotFoundException('Form not found or inactive');
|
|
270
|
-
}
|
|
225
|
+
const form = await this.getActiveForm(dto.formId);
|
|
271
226
|
const isDraft = dto.isDraft ?? false;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
finalData = {
|
|
281
|
-
...dto.data,
|
|
282
|
-
_computed: computedValues
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
// Update the draft
|
|
287
|
-
draft.data = finalData;
|
|
288
|
-
draft.schemaVersionSnapshot = form.schema;
|
|
289
|
-
draft.schemaVersion = form.schemaVersion;
|
|
290
|
-
draft.submittedAt = new Date();
|
|
291
|
-
draft.isDraft = isDraft;
|
|
292
|
-
draft.metadata = dto.metadata ?? draft.metadata;
|
|
227
|
+
Object.assign(draft, {
|
|
228
|
+
data: this.applyComputedFields(dto.data, form, isDraft),
|
|
229
|
+
schemaVersionSnapshot: form.schema,
|
|
230
|
+
schemaVersion: form.schemaVersion,
|
|
231
|
+
submittedAt: new Date(),
|
|
232
|
+
isDraft,
|
|
233
|
+
metadata: dto.metadata ?? draft.metadata
|
|
234
|
+
});
|
|
293
235
|
const saved = await this.repository.save(draft);
|
|
294
236
|
return this.convertEntityToResponseDto(saved, false);
|
|
295
237
|
}
|
|
296
|
-
|
|
297
|
-
* Get results for a specific form
|
|
298
|
-
*/ async getByFormId(formId, user, pagination) {
|
|
238
|
+
async getByFormId(formId, user, pagination) {
|
|
299
239
|
await this.ensureRepositoryInitialized();
|
|
300
|
-
const query = this.repository.createQueryBuilder(this.entityName)
|
|
301
|
-
query.where('form_result.formId = :formId', {
|
|
240
|
+
const query = this.repository.createQueryBuilder(this.entityName).where('form_result.formId = :formId', {
|
|
302
241
|
formId
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Apply company filter via Form if enabled
|
|
306
|
-
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
307
|
-
if (enableCompanyFeature && user?.companyId) {
|
|
308
|
-
query.innerJoin('form', 'f', 'f.id = form_result.formId').andWhere('f.company_id = :companyId', {
|
|
309
|
-
companyId: user.companyId
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
// Pagination
|
|
242
|
+
}).andWhere('form_result.deletedAt IS NULL');
|
|
243
|
+
this.applyCompanyFilterToQuery(query, user);
|
|
313
244
|
const page = pagination?.page ?? 0;
|
|
314
245
|
const pageSize = pagination?.pageSize ?? 10;
|
|
315
|
-
query.skip(page * pageSize).take(pageSize);
|
|
316
|
-
query.orderBy('form_result.submittedAt', 'DESC');
|
|
246
|
+
query.skip(page * pageSize).take(pageSize).orderBy('form_result.submittedAt', 'DESC');
|
|
317
247
|
const [entities, total] = await query.getManyAndCount();
|
|
318
|
-
const data = entities.map((e)=>this.convertEntityToResponseDto(e, false));
|
|
319
248
|
return {
|
|
320
|
-
data,
|
|
249
|
+
data: entities.map((e)=>this.convertEntityToResponseDto(e, false)),
|
|
321
250
|
total
|
|
322
251
|
};
|
|
323
252
|
}
|
|
324
|
-
|
|
325
|
-
* Check if user has already submitted this form (non-draft)
|
|
326
|
-
* Used for single response mode
|
|
327
|
-
*/ async hasUserSubmitted(formId, user) {
|
|
253
|
+
async hasUserSubmitted(formId, user) {
|
|
328
254
|
await this.ensureRepositoryInitialized();
|
|
329
255
|
const count = await this.repository.count({
|
|
330
256
|
where: {
|
|
@@ -27,6 +27,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException, Scope, UnauthorizedException } from '@nestjs/common';
|
|
31
32
|
import { IsNull } from 'typeorm';
|
|
32
33
|
import { FormBuilderConfigService } from './form-builder-config.service';
|
|
@@ -35,17 +36,11 @@ import { FormAccessType } from '../enums/form-access-type.enum';
|
|
|
35
36
|
import { FormBuilderDataSourceProvider } from './form-builder-datasource.provider';
|
|
36
37
|
import { validateUserPermissions } from '../utils/permission.utils';
|
|
37
38
|
export class FormService extends RequestScopedApiService {
|
|
38
|
-
|
|
39
|
-
* Resolve entity class for this service
|
|
40
|
-
* @returns Form or FormWithCompany based on configuration
|
|
41
|
-
*/ resolveEntity() {
|
|
39
|
+
resolveEntity() {
|
|
42
40
|
const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
|
|
43
41
|
return enableCompanyFeature ? FormWithCompany : Form;
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
* Get DataSource provider for this service
|
|
47
|
-
* @returns FormBuilderDataSourceProvider instance
|
|
48
|
-
*/ getDataSourceProvider() {
|
|
43
|
+
getDataSourceProvider() {
|
|
49
44
|
return this.dataSourceProvider;
|
|
50
45
|
}
|
|
51
46
|
// Entity Conversion
|
|
@@ -84,7 +79,7 @@ export class FormService extends RequestScopedApiService {
|
|
|
84
79
|
form.companyId = dto.companyId;
|
|
85
80
|
}
|
|
86
81
|
// If companyId is not in form at all, set from user
|
|
87
|
-
if (
|
|
82
|
+
if (form.companyId === undefined) {
|
|
88
83
|
form.companyId = user?.companyId ?? null;
|
|
89
84
|
}
|
|
90
85
|
} else {
|
|
@@ -122,22 +117,18 @@ export class FormService extends RequestScopedApiService {
|
|
|
122
117
|
isRaw: false
|
|
123
118
|
};
|
|
124
119
|
}
|
|
125
|
-
|
|
126
|
-
* Override: Extra query manipulation - Auto-filter by user's company
|
|
127
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
120
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
128
121
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
}
|
|
122
|
+
// Apply company filter using shared utility
|
|
123
|
+
applyCompanyFilter(query, {
|
|
124
|
+
isCompanyFeatureEnabled: this.formBuilderConfig.isCompanyFeatureEnabled(),
|
|
125
|
+
entityAlias: 'form'
|
|
126
|
+
}, user);
|
|
136
127
|
return result;
|
|
137
128
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
129
|
+
convertEntityToResponseDto(entity, _isRaw) {
|
|
130
|
+
// Type guard for company-enabled entity
|
|
131
|
+
const entityWithCompany = entity;
|
|
141
132
|
return {
|
|
142
133
|
id: entity.id,
|
|
143
134
|
name: entity.name,
|
|
@@ -148,7 +139,7 @@ export class FormService extends RequestScopedApiService {
|
|
|
148
139
|
accessType: entity.accessType,
|
|
149
140
|
actionGroups: entity.actionGroups,
|
|
150
141
|
isActive: entity.isActive,
|
|
151
|
-
companyId:
|
|
142
|
+
companyId: entityWithCompany.companyId ?? null,
|
|
152
143
|
metadata: entity.metadata,
|
|
153
144
|
createdAt: entity.createdAt,
|
|
154
145
|
updatedAt: entity.updatedAt,
|
|
@@ -158,33 +149,37 @@ export class FormService extends RequestScopedApiService {
|
|
|
158
149
|
deletedById: entity.deletedById
|
|
159
150
|
};
|
|
160
151
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
152
|
+
toPublicForm(form) {
|
|
153
|
+
return {
|
|
154
|
+
id: form.id,
|
|
155
|
+
name: form.name,
|
|
156
|
+
description: form.description,
|
|
157
|
+
schema: form.schema,
|
|
158
|
+
schemaVersion: form.schemaVersion
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async findPublicActiveForm(where) {
|
|
165
162
|
await this.ensureRepositoryInitialized();
|
|
166
|
-
|
|
163
|
+
return this.repository.findOne({
|
|
167
164
|
where: {
|
|
168
|
-
|
|
165
|
+
...where,
|
|
169
166
|
accessType: FormAccessType.PUBLIC,
|
|
170
167
|
isActive: true,
|
|
171
168
|
deletedAt: IsNull()
|
|
172
169
|
}
|
|
173
170
|
});
|
|
171
|
+
}
|
|
172
|
+
// Public Form Access
|
|
173
|
+
async getPublicForm(formId) {
|
|
174
|
+
const form = await this.findPublicActiveForm({
|
|
175
|
+
id: formId
|
|
176
|
+
});
|
|
174
177
|
if (!form) {
|
|
175
178
|
throw new NotFoundException('Form not found or not available for public access');
|
|
176
179
|
}
|
|
177
|
-
return
|
|
178
|
-
id: form.id,
|
|
179
|
-
name: form.name,
|
|
180
|
-
description: form.description,
|
|
181
|
-
schema: form.schema,
|
|
182
|
-
schemaVersion: form.schemaVersion
|
|
183
|
-
};
|
|
180
|
+
return this.toPublicForm(form);
|
|
184
181
|
}
|
|
185
|
-
|
|
186
|
-
* Get form for submission with access validation
|
|
187
|
-
*/ async getFormForSubmission(formId, user) {
|
|
182
|
+
async getFormForSubmission(formId, user) {
|
|
188
183
|
await this.ensureRepositoryInitialized();
|
|
189
184
|
const form = await this.repository.findOne({
|
|
190
185
|
where: {
|
|
@@ -197,27 +192,19 @@ export class FormService extends RequestScopedApiService {
|
|
|
197
192
|
throw new NotFoundException('Form not found or inactive');
|
|
198
193
|
}
|
|
199
194
|
// Access validation based on accessType
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!user) {
|
|
210
|
-
throw new UnauthorizedException('Authentication required to submit this form');
|
|
211
|
-
}
|
|
212
|
-
// Permission check is handled by the controller/guard
|
|
213
|
-
return form;
|
|
214
|
-
default:
|
|
215
|
-
throw new BadRequestException('Invalid access type');
|
|
195
|
+
if (form.accessType === FormAccessType.PUBLIC) {
|
|
196
|
+
return form; // Anyone can access
|
|
197
|
+
}
|
|
198
|
+
// All non-public forms require authentication
|
|
199
|
+
if (!user) {
|
|
200
|
+
throw new UnauthorizedException('Authentication required to submit this form');
|
|
201
|
+
}
|
|
202
|
+
if (form.accessType === FormAccessType.AUTHENTICATED || form.accessType === FormAccessType.ACTION_GROUP) {
|
|
203
|
+
return form; // Permission check for ACTION_GROUP is handled by the controller/guard
|
|
216
204
|
}
|
|
205
|
+
throw new BadRequestException('Invalid access type');
|
|
217
206
|
}
|
|
218
|
-
|
|
219
|
-
* Get form by slug
|
|
220
|
-
*/ async getBySlug(slug) {
|
|
207
|
+
async getBySlug(slug) {
|
|
221
208
|
await this.ensureRepositoryInitialized();
|
|
222
209
|
const form = await this.repository.findOne({
|
|
223
210
|
where: {
|
|
@@ -228,9 +215,15 @@ export class FormService extends RequestScopedApiService {
|
|
|
228
215
|
return form ? this.convertEntityToResponseDto(form, false) : null;
|
|
229
216
|
}
|
|
230
217
|
/**
|
|
231
|
-
* Get form
|
|
232
|
-
* Returns
|
|
233
|
-
*/ async
|
|
218
|
+
* Get public form by slug (no authentication required)
|
|
219
|
+
* Returns null if form doesn't exist, is not public, or is inactive
|
|
220
|
+
*/ async getPublicFormBySlug(slug) {
|
|
221
|
+
const form = await this.findPublicActiveForm({
|
|
222
|
+
slug
|
|
223
|
+
});
|
|
224
|
+
return form ? this.toPublicForm(form) : null;
|
|
225
|
+
}
|
|
226
|
+
async getFormAccessInfo(formId) {
|
|
234
227
|
await this.ensureRepositoryInitialized();
|
|
235
228
|
const form = await this.repository.findOne({
|
|
236
229
|
where: {
|
|
@@ -258,10 +251,7 @@ export class FormService extends RequestScopedApiService {
|
|
|
258
251
|
isActive: form.isActive
|
|
259
252
|
};
|
|
260
253
|
}
|
|
261
|
-
|
|
262
|
-
* Get form for authenticated submission
|
|
263
|
-
* Returns full form for users who are logged in
|
|
264
|
-
*/ async getAuthenticatedForm(formId, user) {
|
|
254
|
+
async getAuthenticatedForm(formId, user) {
|
|
265
255
|
const form = await this.getFormForSubmission(formId, user);
|
|
266
256
|
// For action_group access, check permissions from cache
|
|
267
257
|
if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
@@ -270,13 +260,7 @@ export class FormService extends RequestScopedApiService {
|
|
|
270
260
|
throw new ForbiddenException('You do not have permission to access this form');
|
|
271
261
|
}
|
|
272
262
|
}
|
|
273
|
-
return
|
|
274
|
-
id: form.id,
|
|
275
|
-
name: form.name,
|
|
276
|
-
description: form.description,
|
|
277
|
-
schema: form.schema,
|
|
278
|
-
schemaVersion: form.schemaVersion
|
|
279
|
-
};
|
|
263
|
+
return this.toPublicForm(form);
|
|
280
264
|
}
|
|
281
265
|
constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider){
|
|
282
266
|
super('form', null, cacheManager, utilsService, FormService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider;
|