@fogpipe/forma-core 0.15.0 → 0.17.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/dist/index.js CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  isDisplayField,
20
20
  isEnabled,
21
21
  isFieldVisible,
22
+ isMatrixField,
22
23
  isReadonly,
23
24
  isRequired,
24
25
  isSelectionField,
@@ -26,7 +27,7 @@ import {
26
27
  parseDecimalFormat,
27
28
  validate,
28
29
  validateSingleField
29
- } from "./chunk-COAUJ3IL.js";
30
+ } from "./chunk-K7STGGGJ.js";
30
31
  import {
31
32
  evaluate,
32
33
  evaluateBoolean,
@@ -62,6 +63,7 @@ export {
62
63
  isDisplayField,
63
64
  isEnabled,
64
65
  isFieldVisible,
66
+ isMatrixField,
65
67
  isReadonly,
66
68
  isRequired,
67
69
  isSelectionField,
package/dist/types.d.ts CHANGED
@@ -51,7 +51,7 @@ export interface JSONSchemaString extends JSONSchemaBase {
51
51
  minLength?: number;
52
52
  maxLength?: number;
53
53
  pattern?: string;
54
- format?: "date" | "date-time" | "email" | "uri" | "uuid";
54
+ format?: "date" | "date-time" | "email" | "uri" | "uuid" | "phone";
55
55
  enum?: string[];
56
56
  default?: string;
57
57
  }
@@ -69,6 +69,7 @@ export interface JSONSchemaInteger extends JSONSchemaBase {
69
69
  minimum?: number;
70
70
  maximum?: number;
71
71
  multipleOf?: number;
72
+ enum?: number[];
72
73
  default?: number;
73
74
  }
74
75
  export interface JSONSchemaBoolean extends JSONSchemaBase {
@@ -94,7 +95,7 @@ export interface JSONSchemaEnum extends JSONSchemaBase {
94
95
  /**
95
96
  * Field type enumeration - maps to UI component types
96
97
  */
97
- export type FieldType = "text" | "password" | "number" | "integer" | "boolean" | "select" | "multiselect" | "date" | "datetime" | "email" | "url" | "textarea" | "array" | "object" | "computed" | "display";
98
+ export type FieldType = "text" | "phone" | "password" | "number" | "integer" | "boolean" | "select" | "multiselect" | "date" | "datetime" | "email" | "url" | "textarea" | "array" | "object" | "computed" | "display" | "matrix";
98
99
  /**
99
100
  * Validation rule with FEEL expression
100
101
  */
@@ -167,7 +168,7 @@ export interface FieldDefinitionBase {
167
168
  * rather than inside variantConfig.
168
169
  */
169
170
  export interface AdornableFieldDefinition extends FieldDefinitionBase {
170
- type: "text" | "email" | "url" | "password" | "textarea" | "number" | "integer";
171
+ type: "text" | "phone" | "email" | "url" | "password" | "textarea" | "number" | "integer";
171
172
  /** Text/symbol displayed before input (e.g., "$", "https://") */
172
173
  prefix?: string;
173
174
  /** Text/symbol displayed after input (e.g., "USD", "kg", "%") */
@@ -226,6 +227,45 @@ export interface DisplayFieldDefinition extends FieldDefinitionBase {
226
227
  /** Display format (e.g., "currency", "percent", "decimal(2)", or template "{value} Nm") */
227
228
  format?: string;
228
229
  }
230
+ /**
231
+ * Row definition for matrix fields.
232
+ */
233
+ export interface MatrixRow {
234
+ /** Unique row identifier (snake_case, used in data keys and FEEL expressions) */
235
+ id: string;
236
+ /** Display label for this row */
237
+ label: string;
238
+ /** When to show this row */
239
+ visibleWhen?: FEELExpression;
240
+ }
241
+ /**
242
+ * Column definition for matrix fields.
243
+ */
244
+ export interface MatrixColumn {
245
+ /** Column value — use numbers for numeric scales, strings for categorical */
246
+ value: string | number;
247
+ /** Display label for this column */
248
+ label: string;
249
+ }
250
+ /**
251
+ * Matrix/grid fields — a 2D grid where rows are items/statements and
252
+ * columns are shared response options. Each row produces a single value
253
+ * (or multiple values in multi-select mode).
254
+ *
255
+ * Data shape (single-select): `{ speed: 4, quality: 5, support: 3 }`
256
+ * Data shape (multi-select): `{ comm: ["beginner", "intermediate"], lead: ["advanced"] }`
257
+ *
258
+ * FEEL access: `fieldId.rowId` (e.g., `service_rating.speed >= 4`)
259
+ */
260
+ export interface MatrixFieldDefinition extends FieldDefinitionBase {
261
+ type: "matrix";
262
+ /** Row definitions (items/statements to rate) */
263
+ rows: MatrixRow[];
264
+ /** Column definitions (shared response options) */
265
+ columns: MatrixColumn[];
266
+ /** Allow multiple selections per row (default: false) */
267
+ multiSelect?: boolean;
268
+ }
229
269
  /**
230
270
  * Computed fields (read-only calculated values).
231
271
  * Note: Computed field definitions live in Forma.computed, not Forma.fields.
@@ -250,7 +290,7 @@ export interface ComputedFieldDefinition extends FieldDefinitionBase {
250
290
  * field.options; // SelectOption[] | undefined
251
291
  * }
252
292
  */
253
- export type FieldDefinition = AdornableFieldDefinition | SelectionFieldDefinition | SimpleFieldDefinition | ArrayFieldDefinition | ObjectFieldDefinition | DisplayFieldDefinition | ComputedFieldDefinition;
293
+ export type FieldDefinition = AdornableFieldDefinition | SelectionFieldDefinition | SimpleFieldDefinition | ArrayFieldDefinition | ObjectFieldDefinition | DisplayFieldDefinition | ComputedFieldDefinition | MatrixFieldDefinition;
254
294
  /** Check if field supports prefix/suffix adorners */
255
295
  export declare function isAdornableField(field: FieldDefinition): field is AdornableFieldDefinition;
256
296
  /** Check if field is a display-only field (no data) */
@@ -259,6 +299,8 @@ export declare function isDisplayField(field: FieldDefinition): field is Display
259
299
  export declare function isSelectionField(field: FieldDefinition): field is SelectionFieldDefinition;
260
300
  /** Check if field is an array field */
261
301
  export declare function isArrayField(field: FieldDefinition): field is ArrayFieldDefinition;
302
+ /** Check if field is a matrix field */
303
+ export declare function isMatrixField(field: FieldDefinition): field is MatrixFieldDefinition;
262
304
  /** Check if field collects data (not display) */
263
305
  export declare function isDataField(field: FieldDefinition): boolean;
264
306
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fogpipe/forma-core",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Forma core runtime: Types and evaluation engines for dynamic forms with FEEL expressions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Tests for matrix field type - validation and visibility
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { validate } from "../engine/validate.js";
7
+ import { getVisibility } from "../engine/visibility.js";
8
+ import type { Forma, MatrixFieldDefinition } from "../types.js";
9
+ import { isMatrixField, isDataField } from "../types.js";
10
+
11
+ /**
12
+ * Helper to create a minimal Forma spec with a matrix field
13
+ */
14
+ function createMatrixSpec(options: {
15
+ fieldDef?: Partial<MatrixFieldDefinition>;
16
+ schemaProperties?: Record<string, unknown>;
17
+ required?: string[];
18
+ data?: Record<string, unknown>;
19
+ }): Forma {
20
+ const {
21
+ fieldDef = {},
22
+ schemaProperties,
23
+ required = [],
24
+ } = options;
25
+
26
+ const matrixField: MatrixFieldDefinition = {
27
+ type: "matrix",
28
+ label: "Service Rating",
29
+ rows: [
30
+ { id: "speed", label: "Speed" },
31
+ { id: "quality", label: "Quality" },
32
+ { id: "support", label: "Support" },
33
+ ],
34
+ columns: [
35
+ { value: 1, label: "Poor" },
36
+ { value: 2, label: "Fair" },
37
+ { value: 3, label: "Good" },
38
+ { value: 4, label: "Very Good" },
39
+ { value: 5, label: "Excellent" },
40
+ ],
41
+ ...fieldDef,
42
+ };
43
+
44
+ const schema = schemaProperties ?? {
45
+ service_rating: {
46
+ type: "object",
47
+ properties: {
48
+ speed: { type: "integer", enum: [1, 2, 3, 4, 5] },
49
+ quality: { type: "integer", enum: [1, 2, 3, 4, 5] },
50
+ support: { type: "integer", enum: [1, 2, 3, 4, 5] },
51
+ },
52
+ required: ["speed", "quality", "support"],
53
+ },
54
+ };
55
+
56
+ return {
57
+ version: "1.0",
58
+ meta: { id: "test", title: "Test" },
59
+ schema: {
60
+ type: "object",
61
+ properties: schema as Forma["schema"]["properties"],
62
+ required: required.length > 0 ? required : undefined,
63
+ },
64
+ fields: {
65
+ service_rating: matrixField,
66
+ } as Forma["fields"],
67
+ fieldOrder: ["service_rating"],
68
+ };
69
+ }
70
+
71
+ // ============================================================================
72
+ // Type Guards
73
+ // ============================================================================
74
+
75
+ describe("type guards", () => {
76
+ it("isMatrixField should return true for matrix fields", () => {
77
+ const field: MatrixFieldDefinition = {
78
+ type: "matrix",
79
+ rows: [{ id: "a", label: "A" }],
80
+ columns: [{ value: 1, label: "1" }],
81
+ };
82
+ expect(isMatrixField(field)).toBe(true);
83
+ });
84
+
85
+ it("isMatrixField should return false for non-matrix fields", () => {
86
+ expect(isMatrixField({ type: "text" })).toBe(false);
87
+ expect(isMatrixField({ type: "select" })).toBe(false);
88
+ expect(isMatrixField({ type: "array" })).toBe(false);
89
+ });
90
+
91
+ it("isDataField should return true for matrix fields", () => {
92
+ const field: MatrixFieldDefinition = {
93
+ type: "matrix",
94
+ rows: [{ id: "a", label: "A" }],
95
+ columns: [{ value: 1, label: "1" }],
96
+ };
97
+ expect(isDataField(field)).toBe(true);
98
+ });
99
+ });
100
+
101
+ // ============================================================================
102
+ // Validation
103
+ // ============================================================================
104
+
105
+ describe("matrix validation", () => {
106
+ describe("valid data", () => {
107
+ it("should pass with valid numeric column values", () => {
108
+ const spec = createMatrixSpec({});
109
+ const data = {
110
+ service_rating: { speed: 4, quality: 5, support: 3 },
111
+ };
112
+ const result = validate(data, spec);
113
+ expect(result.valid).toBe(true);
114
+ expect(result.errors).toHaveLength(0);
115
+ });
116
+
117
+ it("should pass with valid string column values", () => {
118
+ const spec = createMatrixSpec({
119
+ fieldDef: {
120
+ columns: [
121
+ { value: "agree", label: "Agree" },
122
+ { value: "disagree", label: "Disagree" },
123
+ ],
124
+ },
125
+ schemaProperties: {
126
+ service_rating: {
127
+ type: "object",
128
+ properties: {
129
+ speed: { type: "string", enum: ["agree", "disagree"] },
130
+ quality: { type: "string", enum: ["agree", "disagree"] },
131
+ support: { type: "string", enum: ["agree", "disagree"] },
132
+ },
133
+ },
134
+ },
135
+ });
136
+ const data = {
137
+ service_rating: { speed: "agree", quality: "disagree", support: "agree" },
138
+ };
139
+ const result = validate(data, spec);
140
+ expect(result.valid).toBe(true);
141
+ });
142
+
143
+ it("should pass with partial answers when not required", () => {
144
+ const spec = createMatrixSpec({});
145
+ const data = {
146
+ service_rating: { speed: 4 },
147
+ };
148
+ const result = validate(data, spec);
149
+ expect(result.valid).toBe(true);
150
+ });
151
+
152
+ it("should pass with null value when not required", () => {
153
+ const spec = createMatrixSpec({});
154
+ const result = validate({ service_rating: null }, spec);
155
+ expect(result.valid).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe("invalid data", () => {
160
+ it("should fail with invalid column values", () => {
161
+ const spec = createMatrixSpec({});
162
+ const data = {
163
+ service_rating: { speed: 99, quality: 5, support: 3 },
164
+ };
165
+ const result = validate(data, spec);
166
+ expect(result.valid).toBe(false);
167
+ expect(result.errors).toHaveLength(1);
168
+ expect(result.errors[0].field).toBe("service_rating.speed");
169
+ });
170
+
171
+ it("should fail when required and empty", () => {
172
+ const spec = createMatrixSpec({ required: ["service_rating"] });
173
+ const result = validate({}, spec);
174
+ expect(result.valid).toBe(false);
175
+ expect(result.errors.some((e) => e.field === "service_rating")).toBe(true);
176
+ });
177
+
178
+ it("should fail when required and not all visible rows answered", () => {
179
+ const spec = createMatrixSpec({ required: ["service_rating"] });
180
+ const data = {
181
+ service_rating: { speed: 4 }, // quality and support missing
182
+ };
183
+ const result = validate(data, spec);
184
+ expect(result.valid).toBe(false);
185
+ expect(result.errors).toHaveLength(2);
186
+ expect(result.errors[0].field).toBe("service_rating.quality");
187
+ expect(result.errors[1].field).toBe("service_rating.support");
188
+ });
189
+
190
+ it("should pass when required and all visible rows answered", () => {
191
+ const spec = createMatrixSpec({ required: ["service_rating"] });
192
+ const data = {
193
+ service_rating: { speed: 4, quality: 5, support: 3 },
194
+ };
195
+ const result = validate(data, spec);
196
+ expect(result.valid).toBe(true);
197
+ });
198
+ });
199
+
200
+ describe("multi-select mode", () => {
201
+ it("should pass with array values in multi-select mode", () => {
202
+ const spec = createMatrixSpec({
203
+ fieldDef: {
204
+ multiSelect: true,
205
+ columns: [
206
+ { value: "beginner", label: "Beginner" },
207
+ { value: "intermediate", label: "Intermediate" },
208
+ { value: "advanced", label: "Advanced" },
209
+ ],
210
+ },
211
+ schemaProperties: {
212
+ service_rating: {
213
+ type: "object",
214
+ properties: {
215
+ speed: {
216
+ type: "array",
217
+ items: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
218
+ },
219
+ quality: {
220
+ type: "array",
221
+ items: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
222
+ },
223
+ support: {
224
+ type: "array",
225
+ items: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
226
+ },
227
+ },
228
+ },
229
+ },
230
+ });
231
+ const data = {
232
+ service_rating: {
233
+ speed: ["beginner", "intermediate"],
234
+ quality: ["advanced"],
235
+ support: [],
236
+ },
237
+ };
238
+ const result = validate(data, spec);
239
+ expect(result.valid).toBe(true);
240
+ });
241
+
242
+ it("should fail with invalid values in multi-select array", () => {
243
+ const spec = createMatrixSpec({
244
+ fieldDef: {
245
+ multiSelect: true,
246
+ columns: [
247
+ { value: "beginner", label: "Beginner" },
248
+ { value: "advanced", label: "Advanced" },
249
+ ],
250
+ },
251
+ schemaProperties: {
252
+ service_rating: {
253
+ type: "object",
254
+ properties: {
255
+ speed: {
256
+ type: "array",
257
+ items: { type: "string", enum: ["beginner", "advanced"] },
258
+ },
259
+ quality: {
260
+ type: "array",
261
+ items: { type: "string", enum: ["beginner", "advanced"] },
262
+ },
263
+ support: {
264
+ type: "array",
265
+ items: { type: "string", enum: ["beginner", "advanced"] },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ });
271
+ const data = {
272
+ service_rating: {
273
+ speed: ["beginner", "INVALID"],
274
+ quality: ["advanced"],
275
+ support: [],
276
+ },
277
+ };
278
+ const result = validate(data, spec);
279
+ expect(result.valid).toBe(false);
280
+ expect(result.errors[0].field).toBe("service_rating.speed");
281
+ });
282
+
283
+ it("should fail when multi-select row has non-array value", () => {
284
+ const spec = createMatrixSpec({
285
+ fieldDef: {
286
+ multiSelect: true,
287
+ columns: [
288
+ { value: "a", label: "A" },
289
+ { value: "b", label: "B" },
290
+ ],
291
+ },
292
+ schemaProperties: {
293
+ service_rating: {
294
+ type: "object",
295
+ properties: {
296
+ speed: { type: "array", items: { type: "string" } },
297
+ quality: { type: "array", items: { type: "string" } },
298
+ support: { type: "array", items: { type: "string" } },
299
+ },
300
+ },
301
+ },
302
+ });
303
+ const data = {
304
+ service_rating: {
305
+ speed: "a", // should be array
306
+ quality: ["a"],
307
+ support: ["b"],
308
+ },
309
+ };
310
+ const result = validate(data, spec);
311
+ expect(result.valid).toBe(false);
312
+ expect(result.errors[0].field).toBe("service_rating.speed");
313
+ expect(result.errors[0].message).toContain("list");
314
+ });
315
+ });
316
+ });
317
+
318
+ // ============================================================================
319
+ // Visibility
320
+ // ============================================================================
321
+
322
+ describe("matrix visibility", () => {
323
+ it("should compute visibility for all matrix rows", () => {
324
+ const spec = createMatrixSpec({});
325
+ const visibility = getVisibility({}, spec);
326
+ expect(visibility["service_rating"]).toBe(true);
327
+ expect(visibility["service_rating.speed"]).toBe(true);
328
+ expect(visibility["service_rating.quality"]).toBe(true);
329
+ expect(visibility["service_rating.support"]).toBe(true);
330
+ });
331
+
332
+ it("should hide matrix field when visibleWhen evaluates to false", () => {
333
+ const spec = createMatrixSpec({
334
+ fieldDef: { visibleWhen: "show_rating = true" },
335
+ });
336
+ const visibility = getVisibility({ show_rating: false }, spec);
337
+ expect(visibility["service_rating"]).toBe(false);
338
+ // Row visibility should not be computed for hidden fields
339
+ expect(visibility["service_rating.speed"]).toBeUndefined();
340
+ });
341
+
342
+ it("should hide individual rows with visibleWhen", () => {
343
+ const spec = createMatrixSpec({
344
+ fieldDef: {
345
+ rows: [
346
+ { id: "speed", label: "Speed" },
347
+ { id: "quality", label: "Quality", visibleWhen: "show_quality = true" },
348
+ { id: "support", label: "Support" },
349
+ ],
350
+ },
351
+ });
352
+
353
+ const visibilityHidden = getVisibility({ show_quality: false }, spec);
354
+ expect(visibilityHidden["service_rating.speed"]).toBe(true);
355
+ expect(visibilityHidden["service_rating.quality"]).toBe(false);
356
+ expect(visibilityHidden["service_rating.support"]).toBe(true);
357
+
358
+ const visibilityShown = getVisibility({ show_quality: true }, spec);
359
+ expect(visibilityShown["service_rating.quality"]).toBe(true);
360
+ });
361
+
362
+ it("should skip hidden rows during validation", () => {
363
+ const spec = createMatrixSpec({
364
+ fieldDef: {
365
+ rows: [
366
+ { id: "speed", label: "Speed" },
367
+ { id: "quality", label: "Quality", visibleWhen: "show_quality = true" },
368
+ { id: "support", label: "Support" },
369
+ ],
370
+ },
371
+ });
372
+ // quality row is hidden but has an invalid value — should NOT produce error
373
+ const data = {
374
+ service_rating: { speed: 4, quality: 99, support: 3 },
375
+ };
376
+ const result = validate(data, spec, { onlyVisible: true });
377
+ expect(result.valid).toBe(true);
378
+ });
379
+ });
@@ -13,6 +13,7 @@ import type {
13
13
  Forma,
14
14
  FieldDefinition,
15
15
  ArrayFieldDefinition,
16
+ MatrixFieldDefinition,
16
17
  ValidationRule,
17
18
  EvaluationContext,
18
19
  ValidationResult,
@@ -199,6 +200,24 @@ function validateField(
199
200
  errors.push(...arrayErrors);
200
201
  }
201
202
 
203
+ // 5. Matrix validation
204
+ if (
205
+ fieldDef.type === "matrix" &&
206
+ value !== null &&
207
+ value !== undefined &&
208
+ typeof value === "object" &&
209
+ !Array.isArray(value)
210
+ ) {
211
+ const matrixErrors = validateMatrix(
212
+ path,
213
+ value as Record<string, unknown>,
214
+ fieldDef,
215
+ visibility,
216
+ required,
217
+ );
218
+ errors.push(...matrixErrors);
219
+ }
220
+
202
221
  return errors;
203
222
  }
204
223
 
@@ -687,6 +706,84 @@ function validateArrayItem(
687
706
  return errors;
688
707
  }
689
708
 
709
+ // ============================================================================
710
+ // Matrix Validation
711
+ // ============================================================================
712
+
713
+ /**
714
+ * Validate matrix field values against row/column definitions.
715
+ * Each row value must be a valid column value (or array of column values if multiSelect).
716
+ */
717
+ function validateMatrix(
718
+ path: string,
719
+ value: Record<string, unknown>,
720
+ fieldDef: MatrixFieldDefinition,
721
+ visibility: Record<string, boolean>,
722
+ required: boolean,
723
+ ): FieldError[] {
724
+ const errors: FieldError[] = [];
725
+ const label = fieldDef.label ?? path;
726
+ const validColumnValues = new Set(fieldDef.columns.map((c) => c.value));
727
+
728
+ for (const row of fieldDef.rows) {
729
+ // Skip hidden rows
730
+ const rowPath = `${path}.${row.id}`;
731
+ if (visibility[rowPath] === false) {
732
+ continue;
733
+ }
734
+
735
+ const rowValue = value[row.id];
736
+
737
+ // Required: all visible rows must be answered
738
+ if (required && (rowValue === null || rowValue === undefined)) {
739
+ errors.push({
740
+ field: rowPath,
741
+ message: `${row.label} is required`,
742
+ severity: "error",
743
+ });
744
+ continue;
745
+ }
746
+
747
+ // Skip empty row values for non-required fields
748
+ if (rowValue === null || rowValue === undefined) {
749
+ continue;
750
+ }
751
+
752
+ if (fieldDef.multiSelect) {
753
+ // Multi-select: value should be an array of column values
754
+ if (!Array.isArray(rowValue)) {
755
+ errors.push({
756
+ field: rowPath,
757
+ message: `${row.label} must be a list of selections`,
758
+ severity: "error",
759
+ });
760
+ continue;
761
+ }
762
+ for (const item of rowValue) {
763
+ if (!validColumnValues.has(item as string | number)) {
764
+ errors.push({
765
+ field: rowPath,
766
+ message: `${row.label} contains an invalid selection`,
767
+ severity: "error",
768
+ });
769
+ break;
770
+ }
771
+ }
772
+ } else {
773
+ // Single-select: value should be a single column value
774
+ if (!validColumnValues.has(rowValue as string | number)) {
775
+ errors.push({
776
+ field: rowPath,
777
+ message: `${row.label} has an invalid value for ${label}`,
778
+ severity: "error",
779
+ });
780
+ }
781
+ }
782
+ }
783
+
784
+ return errors;
785
+ }
786
+
690
787
  // ============================================================================
691
788
  // Single Field Validation
692
789
  // ============================================================================