@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.
Files changed (54) hide show
  1. package/README.md +722 -0
  2. package/cjs/controllers/form-result.controller.js +67 -5
  3. package/cjs/controllers/form.controller.js +48 -15
  4. package/cjs/docs/form-builder-swagger.config.js +6 -100
  5. package/cjs/dtos/form-result.dto.js +6 -93
  6. package/cjs/dtos/form.dto.js +21 -163
  7. package/cjs/entities/form-with-company.entity.js +12 -2
  8. package/cjs/entities/form.entity.js +103 -3
  9. package/cjs/entities/index.js +28 -16
  10. package/cjs/index.js +1 -0
  11. package/cjs/interfaces/form-result.interface.js +1 -6
  12. package/cjs/modules/form-builder.module.js +57 -83
  13. package/cjs/services/form-builder-config.service.js +6 -16
  14. package/cjs/services/form-builder-datasource.provider.js +19 -59
  15. package/cjs/services/form-result.service.js +107 -181
  16. package/cjs/services/form.service.js +56 -72
  17. package/cjs/utils/computed-field.utils.js +17 -29
  18. package/cjs/utils/permission.utils.js +11 -16
  19. package/controllers/form-result.controller.d.ts +10 -12
  20. package/dtos/form-result.dto.d.ts +2 -19
  21. package/dtos/form.dto.d.ts +6 -32
  22. package/entities/form-with-company.entity.d.ts +2 -2
  23. package/entities/form.entity.d.ts +12 -2
  24. package/entities/index.d.ts +7 -2
  25. package/fesm/controllers/form-result.controller.js +69 -7
  26. package/fesm/controllers/form.controller.js +50 -17
  27. package/fesm/docs/form-builder-swagger.config.js +6 -100
  28. package/fesm/dtos/form-result.dto.js +9 -99
  29. package/fesm/dtos/form.dto.js +22 -165
  30. package/fesm/entities/form-with-company.entity.js +12 -2
  31. package/fesm/entities/form.entity.js +104 -4
  32. package/fesm/entities/index.js +18 -24
  33. package/fesm/index.js +2 -0
  34. package/fesm/modules/form-builder.module.js +57 -83
  35. package/fesm/services/form-builder-config.service.js +6 -16
  36. package/fesm/services/form-builder-datasource.provider.js +19 -59
  37. package/fesm/services/form-result.service.js +107 -181
  38. package/fesm/services/form.service.js +56 -72
  39. package/fesm/utils/computed-field.utils.js +17 -29
  40. package/fesm/utils/permission.utils.js +2 -9
  41. package/index.d.ts +1 -0
  42. package/interfaces/form-builder-module.interface.d.ts +4 -7
  43. package/interfaces/form-result.interface.d.ts +2 -9
  44. package/interfaces/form.interface.d.ts +2 -10
  45. package/modules/form-builder.module.d.ts +4 -3
  46. package/package.json +3 -3
  47. package/services/form-builder-config.service.d.ts +5 -3
  48. package/services/form-builder-datasource.provider.d.ts +3 -6
  49. package/services/form-result.service.d.ts +5 -0
  50. package/services/form.service.d.ts +13 -10
  51. package/utils/permission.utils.d.ts +0 -2
  52. package/cjs/entities/form-base.entity.js +0 -113
  53. package/entities/form-base.entity.d.ts +0 -13
  54. 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
- // If company feature enabled and user has companyId, filter via Form's company
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
- * Override: Convert entity to response DTO
99
- */ convertEntityToResponseDto(entity, _isRaw) {
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.ensureFormRepositoryInitialized();
124
- // Get the form
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
- // For authenticated users with drafts
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
- // Update existing draft instead of creating new one
181
- existingDraft.data = dto.data;
182
- existingDraft.schemaVersionSnapshot = form.schema;
183
- existingDraft.schemaVersion = form.schemaVersion;
184
- existingDraft.submittedAt = new Date();
185
- existingDraft.metadata = dto.metadata ?? existingDraft.metadata;
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
- // Calculate computed fields if this is a final submission (not draft)
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: finalData,
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
- if (!draft) {
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
- // Get form for fresh schema snapshot (in case form was updated)
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
- // Calculate computed fields if this is a final submission (not draft)
273
- let finalData = dto.data;
274
- if (!isDraft) {
275
- const schema = form.schema;
276
- const settings = schema?.settings;
277
- const computedFields = settings?.computedFields;
278
- if (computedFields && computedFields.length > 0) {
279
- const computedValues = calculateComputedFields(dto.data, computedFields);
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
- query.andWhere('form_result.deletedAt IS NULL');
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 (!('companyId' in form) || form.companyId === undefined) {
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
- // If company feature enabled and user has companyId, filter by user's company
130
- const enableCompanyFeature = this.formBuilderConfig.isCompanyFeatureEnabled();
131
- if (enableCompanyFeature && user?.companyId) {
132
- query.andWhere('form.companyId = :companyId', {
133
- companyId: user.companyId
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
- * Override: Convert entity to response DTO
140
- */ convertEntityToResponseDto(entity, _isRaw) {
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: ('companyId' in entity ? entity.companyId : null) ?? null,
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
- // Public Form Access
162
- /**
163
- * Get form for public submission (no auth)
164
- */ async getPublicForm(formId) {
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
- const form = await this.repository.findOne({
163
+ return this.repository.findOne({
167
164
  where: {
168
- id: formId,
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
- switch(form.accessType){
201
- case FormAccessType.PUBLIC:
202
- return form; // Anyone can access
203
- case FormAccessType.AUTHENTICATED:
204
- if (!user) {
205
- throw new UnauthorizedException('Authentication required to submit this form');
206
- }
207
- return form;
208
- case FormAccessType.ACTION_GROUP:
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 access info (public endpoint)
232
- * Returns basic form info including access requirements
233
- */ async getFormAccessInfo(formId) {
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;