@duffcloudservices/site-forms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +213 -0
  2. package/dist/DcsForm.vue.d.ts +67 -0
  3. package/dist/composables/useDcsForm.d.ts +36 -0
  4. package/dist/composables/useFormSubmission.d.ts +18 -0
  5. package/dist/composables/useFormValidation.d.ts +19 -0
  6. package/dist/fields/DcsFormCheckbox.vue.d.ts +12 -0
  7. package/dist/fields/DcsFormCheckboxGroup.vue.d.ts +12 -0
  8. package/dist/fields/DcsFormDate.vue.d.ts +14 -0
  9. package/dist/fields/DcsFormFieldWrapper.vue.d.ts +27 -0
  10. package/dist/fields/DcsFormFile.vue.d.ts +12 -0
  11. package/dist/fields/DcsFormHidden.vue.d.ts +7 -0
  12. package/dist/fields/DcsFormHtmlBlock.vue.d.ts +6 -0
  13. package/dist/fields/DcsFormRadio.vue.d.ts +12 -0
  14. package/dist/fields/DcsFormSection.vue.d.ts +21 -0
  15. package/dist/fields/DcsFormSelect.vue.d.ts +35 -0
  16. package/dist/fields/DcsFormText.vue.d.ts +34 -0
  17. package/dist/fields/DcsFormTextarea.vue.d.ts +34 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.js +918 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/loaders/yaml.d.ts +12 -0
  22. package/dist/schema/validate.d.ts +9 -0
  23. package/dist/types.d.ts +106 -0
  24. package/package.json +73 -0
  25. package/src/DcsForm.vue +299 -0
  26. package/src/__tests__/fields.test.ts +82 -0
  27. package/src/__tests__/multi-step.test.ts +46 -0
  28. package/src/__tests__/schema.test.ts +42 -0
  29. package/src/__tests__/submission.test.ts +77 -0
  30. package/src/__tests__/visible-if.test.ts +111 -0
  31. package/src/composables/useDcsForm.ts +201 -0
  32. package/src/composables/useFormSubmission.ts +113 -0
  33. package/src/composables/useFormValidation.ts +127 -0
  34. package/src/fields/DcsFormCheckbox.vue +35 -0
  35. package/src/fields/DcsFormCheckboxGroup.vue +52 -0
  36. package/src/fields/DcsFormDate.vue +34 -0
  37. package/src/fields/DcsFormFieldWrapper.vue +39 -0
  38. package/src/fields/DcsFormFile.vue +38 -0
  39. package/src/fields/DcsFormHidden.vue +17 -0
  40. package/src/fields/DcsFormHtmlBlock.vue +19 -0
  41. package/src/fields/DcsFormRadio.vue +45 -0
  42. package/src/fields/DcsFormSection.vue +19 -0
  43. package/src/fields/DcsFormSelect.vue +62 -0
  44. package/src/fields/DcsFormText.vue +54 -0
  45. package/src/fields/DcsFormTextarea.vue +43 -0
  46. package/src/index.ts +51 -0
  47. package/src/loaders/yaml.ts +51 -0
  48. package/src/schema/form-definition.schema.json +633 -0
  49. package/src/schema/validate.ts +58 -0
  50. package/src/shims.d.ts +10 -0
  51. package/src/types.ts +140 -0
@@ -0,0 +1,633 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://duffcloudservices.com/schemas/form-definition.schema.json",
4
+ "title": "PortalFormDefinition",
5
+ "type": "object",
6
+ "description": "Portable form definition. This schema is the source of truth for\nboth the portal CRUD APIs and the `.dcs/forms/<formId>.yaml`\nfiles consumed by customer-site builds via `<DcsForm/>`.\n",
7
+ "required": [
8
+ "formId",
9
+ "title",
10
+ "submission",
11
+ "fields"
12
+ ],
13
+ "properties": {
14
+ "formId": {
15
+ "type": "string",
16
+ "description": "Kebab-case identifier, unique per site.",
17
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
18
+ "minLength": 1,
19
+ "maxLength": 80,
20
+ "example": "contact"
21
+ },
22
+ "title": {
23
+ "type": "string",
24
+ "description": "Human-readable form title shown in the portal and rendered as the form heading.",
25
+ "minLength": 1,
26
+ "maxLength": 200,
27
+ "example": "Contact Us"
28
+ },
29
+ "description": {
30
+ "type": "string",
31
+ "description": "Optional descriptive text rendered above the fields.",
32
+ "maxLength": 2000
33
+ },
34
+ "submitLabel": {
35
+ "type": "string",
36
+ "description": "Label for the submit button. Defaults to \"Send\" client-side.",
37
+ "maxLength": 80,
38
+ "example": "Send message"
39
+ },
40
+ "successMessage": {
41
+ "type": "string",
42
+ "description": "Confirmation message shown after a successful submission.",
43
+ "maxLength": 2000,
44
+ "example": "Thanks — we'll be in touch shortly."
45
+ },
46
+ "submission": {
47
+ "$ref": "#/definitions/PortalFormSubmissionConfig"
48
+ },
49
+ "steps": {
50
+ "type": "array",
51
+ "description": "Optional multi-step grouping. When omitted, fields render as a single page.",
52
+ "items": {
53
+ "$ref": "#/definitions/PortalFormStep"
54
+ }
55
+ },
56
+ "fields": {
57
+ "type": "array",
58
+ "description": "Flat list of all fields in the form. When `steps` is present,\nevery field referenced from a step's `fieldIds` must exist here.\n",
59
+ "items": {
60
+ "$ref": "#/definitions/PortalFormField"
61
+ }
62
+ },
63
+ "version": {
64
+ "type": "integer",
65
+ "minimum": 1,
66
+ "description": "Monotonically increasing version, bumped on each save."
67
+ },
68
+ "createdAt": {
69
+ "type": "string",
70
+ "format": "date-time",
71
+ "description": "Form creation timestamp."
72
+ },
73
+ "updatedAt": {
74
+ "type": "string",
75
+ "format": "date-time",
76
+ "description": "Last modification timestamp."
77
+ }
78
+ },
79
+ "definitions": {
80
+ "PortalFormFieldType": {
81
+ "type": "string",
82
+ "description": "Field type controlling input rendering and validation. Includes\nlayout-only types (`section-heading`, `html-block`) that render\ndecorative content rather than capturing values, and `hidden`\nfor prefilled values not displayed to the user.\n",
83
+ "enum": [
84
+ "text",
85
+ "email",
86
+ "tel",
87
+ "textarea",
88
+ "select",
89
+ "multiselect",
90
+ "radio",
91
+ "checkbox",
92
+ "checkbox-group",
93
+ "date",
94
+ "file",
95
+ "hidden",
96
+ "section-heading",
97
+ "html-block"
98
+ ]
99
+ },
100
+ "PortalFormFieldWidth": {
101
+ "type": "string",
102
+ "description": "Layout hint for the field within its row.",
103
+ "enum": [
104
+ "full",
105
+ "half",
106
+ "third"
107
+ ]
108
+ },
109
+ "PortalFormFieldOption": {
110
+ "type": "object",
111
+ "description": "A single option for select / radio / checkbox-group fields.",
112
+ "required": [
113
+ "value",
114
+ "label"
115
+ ],
116
+ "properties": {
117
+ "value": {
118
+ "type": "string",
119
+ "description": "Submitted value for this option."
120
+ },
121
+ "label": {
122
+ "type": "string",
123
+ "description": "Human-readable label shown to the user."
124
+ }
125
+ }
126
+ },
127
+ "PortalFormFieldValidation": {
128
+ "type": "object",
129
+ "description": "Optional validation rules applied to a field's value.",
130
+ "properties": {
131
+ "regex": {
132
+ "type": "string",
133
+ "description": "ECMAScript-compatible pattern the value must match."
134
+ },
135
+ "minLength": {
136
+ "type": "integer",
137
+ "minimum": 0,
138
+ "description": "Minimum string length (text-like fields only)."
139
+ },
140
+ "maxLength": {
141
+ "type": "integer",
142
+ "minimum": 0,
143
+ "description": "Maximum string length (text-like fields only)."
144
+ },
145
+ "min": {
146
+ "type": "number",
147
+ "description": "Minimum numeric / date value."
148
+ },
149
+ "max": {
150
+ "type": "number",
151
+ "description": "Maximum numeric / date value."
152
+ },
153
+ "accept": {
154
+ "type": "array",
155
+ "description": "Accepted MIME types or file extensions for `file` fields.",
156
+ "items": {
157
+ "type": "string"
158
+ }
159
+ }
160
+ }
161
+ },
162
+ "PortalFormFieldVisibleIf": {
163
+ "type": "object",
164
+ "description": "Single-predicate visibility rule. The field is shown only when\nthe referenced sibling field's value equals `equals`. Future\nrevisions may extend this with AND / OR composition; clients\nshould treat unknown extra properties as opaque.\n",
165
+ "required": [
166
+ "fieldId",
167
+ "equals"
168
+ ],
169
+ "properties": {
170
+ "fieldId": {
171
+ "type": "string",
172
+ "description": "ID of the sibling field whose value gates visibility."
173
+ },
174
+ "equals": {
175
+ "description": "Value (string, number, or boolean) the sibling field must equal.",
176
+ "oneOf": [
177
+ {
178
+ "type": "string"
179
+ },
180
+ {
181
+ "type": "number"
182
+ },
183
+ {
184
+ "type": "boolean"
185
+ }
186
+ ]
187
+ }
188
+ }
189
+ },
190
+ "PortalFormField": {
191
+ "type": "object",
192
+ "description": "A single field within a form definition.",
193
+ "required": [
194
+ "id",
195
+ "type",
196
+ "label"
197
+ ],
198
+ "properties": {
199
+ "id": {
200
+ "type": "string",
201
+ "description": "Kebab-case identifier, unique within the form.",
202
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
203
+ "minLength": 1,
204
+ "maxLength": 80
205
+ },
206
+ "type": {
207
+ "$ref": "#/definitions/PortalFormFieldType"
208
+ },
209
+ "label": {
210
+ "type": "string",
211
+ "description": "Human-readable label rendered above the input.",
212
+ "minLength": 1,
213
+ "maxLength": 200
214
+ },
215
+ "helpText": {
216
+ "type": "string",
217
+ "description": "Optional helper / description text shown beneath the label.",
218
+ "maxLength": 500
219
+ },
220
+ "placeholder": {
221
+ "type": "string",
222
+ "description": "Optional placeholder shown inside empty inputs.",
223
+ "maxLength": 200
224
+ },
225
+ "defaultValue": {
226
+ "description": "Optional default value (string, number, boolean, or array of strings).",
227
+ "oneOf": [
228
+ {
229
+ "type": "string"
230
+ },
231
+ {
232
+ "type": "number"
233
+ },
234
+ {
235
+ "type": "boolean"
236
+ },
237
+ {
238
+ "type": "array",
239
+ "items": {
240
+ "type": "string"
241
+ }
242
+ }
243
+ ]
244
+ },
245
+ "required": {
246
+ "type": "boolean",
247
+ "default": false,
248
+ "description": "Whether the field must be supplied to submit the form."
249
+ },
250
+ "width": {
251
+ "allOf": [
252
+ {
253
+ "$ref": "#/definitions/PortalFormFieldWidth"
254
+ }
255
+ ],
256
+ "default": "full"
257
+ },
258
+ "options": {
259
+ "type": "array",
260
+ "description": "Options for `select`, `multiselect`, `radio`, `checkbox-group` fields.",
261
+ "items": {
262
+ "$ref": "#/definitions/PortalFormFieldOption"
263
+ }
264
+ },
265
+ "validation": {
266
+ "$ref": "#/definitions/PortalFormFieldValidation"
267
+ },
268
+ "visibleIf": {
269
+ "$ref": "#/definitions/PortalFormFieldVisibleIf"
270
+ },
271
+ "phi": {
272
+ "type": "boolean",
273
+ "default": false,
274
+ "description": "Marks the field as collecting Protected Health Information.\nWhen true the value must never appear in notification emails\nor logs and the owning site must be in the Medical category\n(see `compliance.instructions.md`).\n"
275
+ },
276
+ "html": {
277
+ "type": "string",
278
+ "description": "Sanitized HTML body for `html-block` fields. Ignored for\nother field types.\n",
279
+ "maxLength": 10000
280
+ }
281
+ }
282
+ },
283
+ "PortalFormStep": {
284
+ "type": "object",
285
+ "description": "Optional grouping for multi-step (wizard-style) forms such as\nthe KT Braun estate-planning questionnaires. When `steps` is\nomitted on a form, all fields render as a single page.\n",
286
+ "required": [
287
+ "id",
288
+ "title",
289
+ "fieldIds"
290
+ ],
291
+ "properties": {
292
+ "id": {
293
+ "type": "string",
294
+ "description": "Kebab-case step identifier, unique within the form.",
295
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
296
+ "minLength": 1,
297
+ "maxLength": 80
298
+ },
299
+ "title": {
300
+ "type": "string",
301
+ "description": "Step title shown in the wizard header.",
302
+ "minLength": 1,
303
+ "maxLength": 200
304
+ },
305
+ "description": {
306
+ "type": "string",
307
+ "description": "Optional descriptive text shown below the step title.",
308
+ "maxLength": 1000
309
+ },
310
+ "fieldIds": {
311
+ "type": "array",
312
+ "description": "Ordered list of field IDs that belong to this step.",
313
+ "items": {
314
+ "type": "string",
315
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
316
+ }
317
+ }
318
+ }
319
+ },
320
+ "PortalFormSubmissionLeadConfig": {
321
+ "type": "object",
322
+ "required": [
323
+ "kind"
324
+ ],
325
+ "description": "Routes submissions into the existing PortalLeads pipeline.",
326
+ "properties": {
327
+ "kind": {
328
+ "type": "string",
329
+ "enum": [
330
+ "lead"
331
+ ]
332
+ },
333
+ "category": {
334
+ "type": "string",
335
+ "description": "Service category id used to route the lead."
336
+ }
337
+ }
338
+ },
339
+ "PortalFormSubmissionEmailConfig": {
340
+ "type": "object",
341
+ "required": [
342
+ "kind",
343
+ "to"
344
+ ],
345
+ "description": "Routes submissions as a notification email to the listed recipients.",
346
+ "properties": {
347
+ "kind": {
348
+ "type": "string",
349
+ "enum": [
350
+ "email"
351
+ ]
352
+ },
353
+ "to": {
354
+ "type": "array",
355
+ "minItems": 1,
356
+ "items": {
357
+ "type": "string",
358
+ "format": "email"
359
+ },
360
+ "description": "Recipient email addresses."
361
+ },
362
+ "subjectTemplate": {
363
+ "type": "string",
364
+ "description": "Optional subject-line template. Use `{title}` to interpolate\nthe form's title; values are intentionally not interpolated\ninto the subject line for compliance reasons.\n",
365
+ "maxLength": 200
366
+ }
367
+ }
368
+ },
369
+ "PortalFormSubmissionWebhookConfig": {
370
+ "type": "object",
371
+ "required": [
372
+ "kind",
373
+ "url"
374
+ ],
375
+ "description": "Posts the submission JSON to a third-party webhook.",
376
+ "properties": {
377
+ "kind": {
378
+ "type": "string",
379
+ "enum": [
380
+ "webhook"
381
+ ]
382
+ },
383
+ "url": {
384
+ "type": "string",
385
+ "format": "uri",
386
+ "description": "HTTPS endpoint receiving the signed POST."
387
+ },
388
+ "signingSecretRef": {
389
+ "type": "string",
390
+ "description": "Name of the Key Vault secret used to HMAC-sign the request.\nThe secret value is never returned in API responses.\n"
391
+ }
392
+ }
393
+ },
394
+ "PortalFormSubmissionConfig": {
395
+ "description": "Discriminated union of submission destinations. Exactly one of\n`lead`, `email`, or `webhook` shapes is used based on `kind`.\n",
396
+ "oneOf": [
397
+ {
398
+ "$ref": "#/definitions/PortalFormSubmissionLeadConfig"
399
+ },
400
+ {
401
+ "$ref": "#/definitions/PortalFormSubmissionEmailConfig"
402
+ },
403
+ {
404
+ "$ref": "#/definitions/PortalFormSubmissionWebhookConfig"
405
+ }
406
+ ],
407
+ "discriminator": {
408
+ "propertyName": "kind",
409
+ "mapping": {
410
+ "lead": "#/definitions/PortalFormSubmissionLeadConfig",
411
+ "email": "#/definitions/PortalFormSubmissionEmailConfig",
412
+ "webhook": "#/definitions/PortalFormSubmissionWebhookConfig"
413
+ }
414
+ }
415
+ },
416
+ "PortalFormSummary": {
417
+ "type": "object",
418
+ "description": "List-row projection of a form definition.",
419
+ "required": [
420
+ "formId",
421
+ "title",
422
+ "fieldCount"
423
+ ],
424
+ "properties": {
425
+ "formId": {
426
+ "type": "string",
427
+ "description": "Kebab-case identifier.",
428
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
429
+ "example": "contact"
430
+ },
431
+ "title": {
432
+ "type": "string",
433
+ "description": "Human-readable title.",
434
+ "example": "Contact Us"
435
+ },
436
+ "description": {
437
+ "type": "string",
438
+ "description": "Optional descriptive text."
439
+ },
440
+ "fieldCount": {
441
+ "type": "integer",
442
+ "minimum": 0,
443
+ "description": "Number of input-bearing fields in the form.",
444
+ "example": 4
445
+ },
446
+ "attachedPageSlugs": {
447
+ "type": "array",
448
+ "description": "Slugs of pages that currently reference this form via `formId`.",
449
+ "items": {
450
+ "type": "string"
451
+ }
452
+ },
453
+ "submissionKind": {
454
+ "type": "string",
455
+ "enum": [
456
+ "lead",
457
+ "email",
458
+ "webhook"
459
+ ],
460
+ "description": "Submission destination kind."
461
+ },
462
+ "lastUpdated": {
463
+ "type": "string",
464
+ "format": "date-time",
465
+ "description": "Last modification timestamp."
466
+ }
467
+ }
468
+ },
469
+ "PortalCreateFormRequest": {
470
+ "type": "object",
471
+ "description": "Payload for creating a new form definition.",
472
+ "required": [
473
+ "formId",
474
+ "title",
475
+ "submission",
476
+ "fields"
477
+ ],
478
+ "properties": {
479
+ "formId": {
480
+ "type": "string",
481
+ "description": "Kebab-case identifier, unique per site.",
482
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
483
+ "minLength": 1,
484
+ "maxLength": 80
485
+ },
486
+ "title": {
487
+ "type": "string",
488
+ "minLength": 1,
489
+ "maxLength": 200
490
+ },
491
+ "description": {
492
+ "type": "string",
493
+ "maxLength": 2000
494
+ },
495
+ "submitLabel": {
496
+ "type": "string",
497
+ "maxLength": 80
498
+ },
499
+ "successMessage": {
500
+ "type": "string",
501
+ "maxLength": 2000
502
+ },
503
+ "submission": {
504
+ "$ref": "#/definitions/PortalFormSubmissionConfig"
505
+ },
506
+ "steps": {
507
+ "type": "array",
508
+ "items": {
509
+ "$ref": "#/definitions/PortalFormStep"
510
+ }
511
+ },
512
+ "fields": {
513
+ "type": "array",
514
+ "items": {
515
+ "$ref": "#/definitions/PortalFormField"
516
+ }
517
+ }
518
+ }
519
+ },
520
+ "PortalUpdateFormRequest": {
521
+ "type": "object",
522
+ "description": "Payload for updating an existing form definition. The `formId`\nis taken from the URL; supplying it in the body is optional and,\nif present, must match the path.\n",
523
+ "properties": {
524
+ "formId": {
525
+ "type": "string",
526
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
527
+ "minLength": 1,
528
+ "maxLength": 80,
529
+ "description": "Optional echo of the path formId; must match if provided."
530
+ },
531
+ "title": {
532
+ "type": "string",
533
+ "minLength": 1,
534
+ "maxLength": 200
535
+ },
536
+ "description": {
537
+ "type": "string",
538
+ "maxLength": 2000
539
+ },
540
+ "submitLabel": {
541
+ "type": "string",
542
+ "maxLength": 80
543
+ },
544
+ "successMessage": {
545
+ "type": "string",
546
+ "maxLength": 2000
547
+ },
548
+ "submission": {
549
+ "$ref": "#/definitions/PortalFormSubmissionConfig"
550
+ },
551
+ "steps": {
552
+ "type": "array",
553
+ "items": {
554
+ "$ref": "#/definitions/PortalFormStep"
555
+ }
556
+ },
557
+ "fields": {
558
+ "type": "array",
559
+ "items": {
560
+ "$ref": "#/definitions/PortalFormField"
561
+ }
562
+ },
563
+ "expectedVersion": {
564
+ "type": "integer",
565
+ "minimum": 1,
566
+ "description": "Optional optimistic-concurrency token. When supplied, the\nupdate is rejected with 409 if the stored form's `version`\nno longer matches.\n"
567
+ }
568
+ }
569
+ },
570
+ "PortalDuplicateFormRequest": {
571
+ "type": "object",
572
+ "description": "Payload for cloning an existing form under a new formId.",
573
+ "required": [
574
+ "formId"
575
+ ],
576
+ "properties": {
577
+ "formId": {
578
+ "type": "string",
579
+ "description": "Target kebab-case formId for the cloned form.",
580
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
581
+ "minLength": 1,
582
+ "maxLength": 80
583
+ },
584
+ "title": {
585
+ "type": "string",
586
+ "description": "Optional title override; defaults to `<source title> (copy)`.",
587
+ "minLength": 1,
588
+ "maxLength": 200
589
+ }
590
+ }
591
+ },
592
+ "PortalFormResponse": {
593
+ "type": "object",
594
+ "description": "Full form definition response.",
595
+ "required": [
596
+ "form"
597
+ ],
598
+ "properties": {
599
+ "form": {
600
+ "$ref": "#/definitions/PortalFormDefinition"
601
+ },
602
+ "attachedPageSlugs": {
603
+ "type": "array",
604
+ "description": "Slugs of pages that currently reference this form via `formId`.",
605
+ "items": {
606
+ "type": "string"
607
+ }
608
+ }
609
+ }
610
+ },
611
+ "PortalFormListResponse": {
612
+ "type": "object",
613
+ "required": [
614
+ "forms",
615
+ "totalCount"
616
+ ],
617
+ "properties": {
618
+ "forms": {
619
+ "type": "array",
620
+ "items": {
621
+ "$ref": "#/definitions/PortalFormSummary"
622
+ }
623
+ },
624
+ "totalCount": {
625
+ "type": "integer",
626
+ "minimum": 0,
627
+ "description": "Total number of forms matching the query.",
628
+ "example": 3
629
+ }
630
+ }
631
+ }
632
+ }
633
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Validates a loaded form definition against the form-definition JSON
3
+ * Schema emitted by `@dcs/contracts`. Dev-only — failures are logged
4
+ * via `console.warn` rather than thrown so a malformed YAML never
5
+ * takes down a customer site in production.
6
+ *
7
+ * Schema is bundled as a snapshot of
8
+ * `contracts/dist/form-definition.schema.json` to avoid a runtime
9
+ * dependency on the contracts build output.
10
+ */
11
+ import Ajv, { type ErrorObject, type ValidateFunction } from 'ajv'
12
+ import addFormats from 'ajv-formats'
13
+ import schema from './form-definition.schema.json'
14
+ import type { PortalFormDefinition } from '../types'
15
+
16
+ let validator: ValidateFunction | null = null
17
+
18
+ function getValidator(): ValidateFunction {
19
+ if (validator) return validator
20
+ const ajv = new Ajv({
21
+ allErrors: true,
22
+ strict: false,
23
+ discriminator: false,
24
+ })
25
+ addFormats(ajv)
26
+ validator = ajv.compile(schema as object)
27
+ return validator
28
+ }
29
+
30
+ export interface SchemaValidationResult {
31
+ valid: boolean
32
+ errors: ErrorObject[]
33
+ }
34
+
35
+ export function validateFormDefinition(
36
+ def: PortalFormDefinition | unknown,
37
+ ): SchemaValidationResult {
38
+ const v = getValidator()
39
+ const valid = v(def) as boolean
40
+ return { valid, errors: valid ? [] : (v.errors ?? []) }
41
+ }
42
+
43
+ /** Logs a `console.warn` when invalid, but only when `import.meta.env.DEV`. */
44
+ export function warnIfInvalid(
45
+ formId: string,
46
+ def: PortalFormDefinition | unknown,
47
+ isDev: boolean,
48
+ ): SchemaValidationResult {
49
+ const result = validateFormDefinition(def)
50
+ if (!result.valid && isDev) {
51
+ // eslint-disable-next-line no-console
52
+ console.warn(
53
+ `[@duffcloudservices/site-forms] Form "${formId}" failed schema validation:`,
54
+ result.errors,
55
+ )
56
+ }
57
+ return result
58
+ }
package/src/shims.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue'
3
+ const component: DefineComponent<{}, {}, unknown>
4
+ export default component
5
+ }
6
+
7
+ declare module '*.json' {
8
+ const value: unknown
9
+ export default value
10
+ }