@frt-platform/report-core 1.2.1 β 1.4.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 +301 -496
- package/dist/index.d.mts +148 -94
- package/dist/index.d.ts +148 -94
- package/dist/index.js +551 -398
- package/dist/index.mjs +552 -398
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,695 +1,500 @@
|
|
|
1
1
|
# `@frt-platform/report-core`
|
|
2
2
|
|
|
3
|
-
Core engine for
|
|
3
|
+
Core engine for defining, validating, migrating, diffing, and serializing **dynamic report templates**.
|
|
4
4
|
|
|
5
5
|
This package is:
|
|
6
6
|
|
|
7
|
-
* **Framework-agnostic**
|
|
8
|
-
* **
|
|
9
|
-
* **
|
|
7
|
+
* π§ **Framework-agnostic**
|
|
8
|
+
* π§© **UI-agnostic**
|
|
9
|
+
* π **Safe by design (Zod-based)**
|
|
10
|
+
* π§± **The foundation of the FRT incident & reporting platform**
|
|
10
11
|
|
|
11
|
-
It
|
|
12
|
-
|
|
13
|
-
* A **typed schema** for report templates (templates β sections β fields)
|
|
14
|
-
* **Zod-based validation** and parsing from unknown / JSON input
|
|
15
|
-
* **Legacy schema migration** (flat `fields` β `sections`, old type names, etc.)
|
|
16
|
-
* **Normalization** helpers to ensure safe IDs & consistent structure
|
|
17
|
-
* **Field defaults** and **ID utilities** you can use to build your own UI
|
|
18
|
-
* **Response validation** based on templates (`validateReportResponse`)
|
|
19
|
-
* **Conditional logic** for fields (`visibleIf` / `requiredIf`)
|
|
20
|
-
* **Template diffing** (`diffTemplates`) to compare template versions (including ordering)
|
|
21
|
-
* **Type-level response inference** (`InferResponse`) for fully typed responses
|
|
22
|
-
* **Field registry** (`FieldRegistry`) for custom / extensible field types
|
|
23
|
-
* **JSON Schema export** (`exportJSONSchema`) for integration with OpenAPI / external validators
|
|
24
|
-
|
|
25
|
-
Itβs the core that powers a flexible incident/report builder, but itβs generic enough to be reused in any app.
|
|
12
|
+
It contains **no React**, **no database code**, and **no styling**.
|
|
13
|
+
You can use it in **Node**, **Next.js**, **backend services**, or custom form engines.
|
|
26
14
|
|
|
27
15
|
---
|
|
28
16
|
|
|
29
|
-
|
|
17
|
+
# β¨ Features
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
# npm
|
|
33
|
-
npm install @frt-platform/report-core zod
|
|
19
|
+
### π Template Schema
|
|
34
20
|
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
* Sections β fields
|
|
22
|
+
* Field IDs, labels, descriptions, placeholders
|
|
23
|
+
* Built-in field types:
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
* `shortText`, `longText`
|
|
26
|
+
* `number`
|
|
27
|
+
* `date`
|
|
28
|
+
* `checkbox`
|
|
29
|
+
* `singleSelect`, `multiSelect`
|
|
30
|
+
* `repeatGroup` (nested fieldsets)
|
|
41
31
|
|
|
42
|
-
|
|
32
|
+
### π Field Constraints
|
|
43
33
|
|
|
44
|
-
|
|
34
|
+
* min/max length (`shortText`, `longText`)
|
|
35
|
+
* min/max value (`number`)
|
|
36
|
+
* min/max selections (`multiSelect`)
|
|
37
|
+
* allowed options
|
|
38
|
+
* checkbox required semantics
|
|
39
|
+
* default values
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
### π Conditional Logic
|
|
47
42
|
|
|
48
|
-
|
|
43
|
+
* `visibleIf`
|
|
44
|
+
* `requiredIf`
|
|
45
|
+
* Supports:
|
|
46
|
+
`equals`, `any`, `all`, `not`
|
|
47
|
+
* Fully integrated into validation.
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
* A **section** contains multiple **fields**
|
|
52
|
-
* A **field** has a `type` (short text, long text, number, select, etc.) and optional constraints
|
|
53
|
-
* Fields can have **conditional logic** (`visibleIf`, `requiredIf`)
|
|
49
|
+
### π₯ Validation Engine
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
* Build response schema dynamically with conditions
|
|
52
|
+
* DX helper: `buildResponseSchema(template)` (no response needed)
|
|
53
|
+
* Throwing API: `validateReportResponse()`
|
|
54
|
+
* Non-throwing API: `validateReportResponseDetailed()`
|
|
55
|
+
* Rich `ResponseFieldError` objects with:
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// "shortText",
|
|
63
|
-
// "longText",
|
|
64
|
-
// "number",
|
|
65
|
-
// "date",
|
|
66
|
-
// "checkbox",
|
|
67
|
-
// "singleSelect",
|
|
68
|
-
// "multiSelect",
|
|
69
|
-
// "repeatGroup", // reserved for repeating groups / fieldsets
|
|
70
|
-
// ]
|
|
71
|
-
```
|
|
57
|
+
* section title
|
|
58
|
+
* field label
|
|
59
|
+
* error code
|
|
60
|
+
* full message
|
|
61
|
+
* nested repeatGroup row context
|
|
72
62
|
|
|
73
|
-
|
|
63
|
+
### π Template Migration & Normalization
|
|
74
64
|
|
|
75
|
-
|
|
65
|
+
* Legacy format migration (`fields β sections`)
|
|
66
|
+
* Automatic ID normalization & uniqueness enforcement
|
|
67
|
+
* Stable `"lowercase_with_underscores"`-style IDs
|
|
68
|
+
* Fallback IDs for missing values: `section_1`, `field_1`, etc.
|
|
76
69
|
|
|
77
|
-
|
|
70
|
+
### π Schema Diffing
|
|
78
71
|
|
|
79
|
-
|
|
72
|
+
Detect:
|
|
80
73
|
|
|
81
|
-
|
|
74
|
+
* Added / removed / reordered sections
|
|
75
|
+
* Added / removed / reordered fields
|
|
76
|
+
* Modified fields
|
|
77
|
+
* Nested diffs inside repeat groups
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
import type { ReportTemplateSchema } from "@frt-platform/report-core";
|
|
79
|
+
### π¦ JSON Schema Export
|
|
85
80
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
title: "Incident follow-up",
|
|
89
|
-
description: "Collect a quick summary after an incident has been resolved.",
|
|
90
|
-
sections: [
|
|
91
|
-
{
|
|
92
|
-
id: "section-overview",
|
|
93
|
-
title: "Incident overview",
|
|
94
|
-
fields: [
|
|
95
|
-
{
|
|
96
|
-
id: "summary",
|
|
97
|
-
type: "longText",
|
|
98
|
-
label: "What happened?",
|
|
99
|
-
required: true,
|
|
100
|
-
minLength: 10,
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: "status",
|
|
104
|
-
type: "singleSelect",
|
|
105
|
-
label: "Incident status",
|
|
106
|
-
required: true,
|
|
107
|
-
options: ["Pending review", "Resolved", "Escalated"],
|
|
108
|
-
defaultValue: "Resolved",
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
};
|
|
114
|
-
```
|
|
81
|
+
* Export a template as a valid JSON Schema (2020-12 draft)
|
|
82
|
+
* Includes vendor extensions:
|
|
115
83
|
|
|
116
|
-
|
|
84
|
+
* `x-frt-visibleIf`
|
|
85
|
+
* `x-frt-requiredIf`
|
|
86
|
+
* `x-frt-minIf` / `x-frt-maxIf` for repeatGroup
|
|
87
|
+
* placeholders
|
|
88
|
+
* Useful for OpenAPI, Postman, or other backend runtimes.
|
|
117
89
|
|
|
118
|
-
|
|
90
|
+
### π Field Registry
|
|
119
91
|
|
|
120
|
-
|
|
121
|
-
import {
|
|
122
|
-
parseReportTemplateSchema,
|
|
123
|
-
normalizeReportTemplateSchema,
|
|
124
|
-
} from "@frt-platform/report-core";
|
|
92
|
+
Extend the system at runtime:
|
|
125
93
|
|
|
126
|
-
|
|
94
|
+
* Add custom types (`richText`, `fileUpload`, etc.)
|
|
95
|
+
* Override validation logic
|
|
96
|
+
* Provide metadata for UI packages
|
|
97
|
+
* Unknown/unregistered field types safely fall back to `z.any()` so they never break the core engine.
|
|
127
98
|
|
|
128
|
-
|
|
129
|
-
const parsed = parseReportTemplateSchema(raw);
|
|
99
|
+
### 𧬠Type Inference
|
|
130
100
|
|
|
131
|
-
|
|
132
|
-
const normalized = normalizeReportTemplateSchema(parsed);
|
|
101
|
+
Get a fully typed response type from a template:
|
|
133
102
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
103
|
+
```ts
|
|
104
|
+
type MyResponse = InferResponse<typeof template>;
|
|
105
|
+
````
|
|
137
106
|
|
|
138
|
-
###
|
|
107
|
+
### π§Ύ Serialization Helpers
|
|
139
108
|
|
|
140
|
-
|
|
141
|
-
import { serializeReportTemplateSchema } from "@frt-platform/report-core";
|
|
109
|
+
Deterministic JSON output with sorting options:
|
|
142
110
|
|
|
143
|
-
|
|
144
|
-
|
|
111
|
+
```ts
|
|
112
|
+
serializeReportTemplateSchema(template, {
|
|
113
|
+
pretty: true,
|
|
114
|
+
sortSectionsById: true,
|
|
115
|
+
sortFieldsById: true,
|
|
116
|
+
});
|
|
145
117
|
```
|
|
146
118
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
## Parsing from a JSON string
|
|
119
|
+
Perfect for Git diffs and storage.
|
|
150
120
|
|
|
151
|
-
|
|
121
|
+
---
|
|
152
122
|
|
|
153
|
-
|
|
154
|
-
import { parseReportTemplateSchemaFromString } from "@frt-platform/report-core";
|
|
123
|
+
# π¦ Installation
|
|
155
124
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// valid template here
|
|
159
|
-
} catch (err) {
|
|
160
|
-
// invalid JSON or schema β show error to user
|
|
161
|
-
}
|
|
125
|
+
```bash
|
|
126
|
+
npm install @frt-platform/report-core zod
|
|
162
127
|
```
|
|
163
128
|
|
|
164
|
-
|
|
129
|
+
`zod` is a peer dependency.
|
|
165
130
|
|
|
166
|
-
|
|
131
|
+
---
|
|
167
132
|
|
|
168
|
-
|
|
133
|
+
# π§Ή Parsing & Normalization
|
|
169
134
|
|
|
170
|
-
|
|
135
|
+
The core exposes helpers for safely **parsing**, **migrating**, and **normalizing** templates.
|
|
171
136
|
|
|
172
137
|
```ts
|
|
173
|
-
import {
|
|
174
|
-
|
|
175
|
-
|
|
138
|
+
import {
|
|
139
|
+
parseReportTemplateSchema,
|
|
140
|
+
parseReportTemplateSchemaFromString,
|
|
141
|
+
} from "@frt-platform/report-core";
|
|
176
142
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
status: "Resolved",
|
|
180
|
-
};
|
|
143
|
+
// From a raw JS object (e.g. loaded from DB, file, etc.)
|
|
144
|
+
const template = parseReportTemplateSchema(rawTemplateObject);
|
|
181
145
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
146
|
+
// From a JSON string
|
|
147
|
+
const templateFromString = parseReportTemplateSchemaFromString(jsonString);
|
|
184
148
|
```
|
|
185
149
|
|
|
186
|
-
`
|
|
150
|
+
Under the hood, `parseReportTemplateSchema` does:
|
|
187
151
|
|
|
188
|
-
|
|
189
|
-
* `minLength` / `maxLength` (short/long text)
|
|
190
|
-
* `minValue` / `maxValue` (numbers)
|
|
191
|
-
* `minSelections` / `maxSelections` (multiSelect)
|
|
192
|
-
* `options` (singleSelect + multiSelect)
|
|
193
|
-
* `checkbox` semantics (required checkbox must be `true`)
|
|
194
|
-
* **Conditional logic**:
|
|
152
|
+
1. **Legacy migration**
|
|
195
153
|
|
|
196
|
-
|
|
197
|
-
* `requiredIf`: if true, the field is treated as required for this specific response
|
|
154
|
+
* Accepts both the new `{ sections: [...] }` format and legacy flat:
|
|
198
155
|
|
|
199
|
-
|
|
156
|
+
```ts
|
|
157
|
+
{
|
|
158
|
+
version: 1,
|
|
159
|
+
fields: [ /* ... */ ]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
200
162
|
|
|
201
|
-
|
|
202
|
-
import { buildResponseSchemaWithConditions } from "@frt-platform/report-core";
|
|
163
|
+
* Legacy `fields` are automatically wrapped into a single `section_1`.
|
|
203
164
|
|
|
204
|
-
|
|
205
|
-
const safeResponse = responseSchema.parse(responseData);
|
|
206
|
-
```
|
|
165
|
+
2. **Schema validation**
|
|
207
166
|
|
|
208
|
-
|
|
167
|
+
* Uses a Zod validator for `ReportTemplateSchema` to ensure the template is structurally valid.
|
|
209
168
|
|
|
210
|
-
|
|
169
|
+
3. **Normalization**
|
|
211
170
|
|
|
212
|
-
|
|
171
|
+
* Section and field IDs are normalized to **lowercase** with spaces β underscores:
|
|
213
172
|
|
|
214
|
-
|
|
173
|
+
* `"My Field!"` β `"my_field"`
|
|
215
174
|
|
|
216
|
-
|
|
217
|
-
{
|
|
218
|
-
"id": "injury_details",
|
|
219
|
-
"type": "longText",
|
|
220
|
-
"label": "Describe the injury",
|
|
221
|
-
"visibleIf": { "equals": { "injured": true } },
|
|
222
|
-
"requiredIf": { "equals": { "severity": "High" } }
|
|
223
|
-
}
|
|
224
|
-
```
|
|
175
|
+
* Invalid characters are stripped (only `[a-z0-9_-]` are kept).
|
|
225
176
|
|
|
226
|
-
|
|
177
|
+
* IDs are made **unique per namespace** by appending `-1`, `-2`, β¦ as needed:
|
|
227
178
|
|
|
228
|
-
* `
|
|
229
|
-
* `{"any": [cond1, cond2, ...] }`
|
|
230
|
-
* `{"all": [cond1, cond2, ...] }`
|
|
231
|
-
* `{"not": cond }`
|
|
179
|
+
* `my_field`, `my_field-1`, `my_field-2`, β¦
|
|
232
180
|
|
|
233
|
-
|
|
181
|
+
* Missing IDs get deterministic fallbacks:
|
|
234
182
|
|
|
235
|
-
*
|
|
236
|
-
*
|
|
183
|
+
* Sections: `section_1`, `section_2`, β¦
|
|
184
|
+
* Fields: `field_1`, `field_2`, β¦
|
|
185
|
+
|
|
186
|
+
This makes templates safe to store, diff, and round-trip in a stable way.
|
|
237
187
|
|
|
238
188
|
---
|
|
239
189
|
|
|
240
|
-
|
|
190
|
+
# π Quickstart
|
|
241
191
|
|
|
242
|
-
|
|
192
|
+
## 1. Define a template
|
|
243
193
|
|
|
244
194
|
```ts
|
|
245
|
-
import type {
|
|
195
|
+
import type { ReportTemplateSchema } from "@frt-platform/report-core";
|
|
246
196
|
|
|
247
|
-
const
|
|
197
|
+
const template: ReportTemplateSchema = {
|
|
248
198
|
version: 1,
|
|
249
199
|
sections: [
|
|
250
200
|
{
|
|
251
|
-
id: "
|
|
201
|
+
id: "general",
|
|
202
|
+
title: "General Info",
|
|
252
203
|
fields: [
|
|
253
204
|
{
|
|
254
|
-
id: "
|
|
255
|
-
type: "
|
|
256
|
-
label: "
|
|
205
|
+
id: "title",
|
|
206
|
+
type: "shortText",
|
|
207
|
+
label: "Incident title",
|
|
257
208
|
required: true,
|
|
258
|
-
minLength: 10,
|
|
259
209
|
},
|
|
260
210
|
{
|
|
261
|
-
id: "
|
|
211
|
+
id: "severity",
|
|
262
212
|
type: "singleSelect",
|
|
263
|
-
label: "
|
|
213
|
+
label: "Severity",
|
|
264
214
|
required: true,
|
|
265
|
-
options: ["
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
id: "follow_up_date",
|
|
269
|
-
type: "date",
|
|
270
|
-
label: "Follow-up date",
|
|
215
|
+
options: ["Low", "Medium", "High"],
|
|
271
216
|
},
|
|
272
217
|
{
|
|
273
|
-
id: "
|
|
274
|
-
type: "
|
|
275
|
-
label: "
|
|
276
|
-
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
id: "confirmed",
|
|
280
|
-
type: "checkbox",
|
|
281
|
-
label: "I confirm this report is accurate",
|
|
282
|
-
required: true,
|
|
218
|
+
id: "details",
|
|
219
|
+
type: "longText",
|
|
220
|
+
label: "Details",
|
|
221
|
+
minLength: 10,
|
|
283
222
|
},
|
|
284
223
|
],
|
|
285
224
|
},
|
|
286
225
|
],
|
|
287
|
-
} as const;
|
|
288
|
-
|
|
289
|
-
type IncidentResponse = InferResponse<typeof incidentTemplate>;
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
`IncidentResponse` will be inferred as:
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
type IncidentResponse = {
|
|
296
|
-
summary: string; // required
|
|
297
|
-
status: "Pending" | "Resolved" | "Escalated"; // required
|
|
298
|
-
follow_up_date?: string; // optional
|
|
299
|
-
tags?: ("Safety" | "Medical" | "Security")[]; // optional
|
|
300
|
-
confirmed: boolean; // required checkbox
|
|
301
226
|
};
|
|
302
227
|
```
|
|
303
228
|
|
|
304
|
-
This is extremely useful for:
|
|
305
|
-
|
|
306
|
-
* API handlers (`(body: IncidentResponse) => { ... }`)
|
|
307
|
-
* DB layers that store `data` blobs
|
|
308
|
-
* Frontend forms with type-safe state
|
|
309
|
-
|
|
310
229
|
---
|
|
311
230
|
|
|
312
|
-
##
|
|
313
|
-
|
|
314
|
-
The package includes core defaults for each field type (no UI, no icons):
|
|
231
|
+
## 2. Validate a response (throwing API)
|
|
315
232
|
|
|
316
233
|
```ts
|
|
317
|
-
import {
|
|
318
|
-
CORE_FIELD_DEFAULTS,
|
|
319
|
-
DEFAULT_FIELD_LABEL,
|
|
320
|
-
type ReportTemplateFieldType,
|
|
321
|
-
} from "@frt-platform/report-core";
|
|
322
|
-
|
|
323
|
-
function createDefaultField(type: ReportTemplateFieldType) {
|
|
324
|
-
const defaults = CORE_FIELD_DEFAULTS[type] ?? {};
|
|
325
|
-
return {
|
|
326
|
-
id: "your-id-here",
|
|
327
|
-
type,
|
|
328
|
-
label: (defaults.label as string) ?? DEFAULT_FIELD_LABEL,
|
|
329
|
-
...defaults,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
234
|
+
import { validateReportResponse } from "@frt-platform/report-core";
|
|
332
235
|
|
|
333
|
-
const
|
|
236
|
+
const parsed = validateReportResponse(template, {
|
|
237
|
+
title: "Broken fire alarm",
|
|
238
|
+
severity: "High",
|
|
239
|
+
details: "Triggered after smoke test",
|
|
240
|
+
});
|
|
334
241
|
```
|
|
335
242
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
* `shortText`, `longText`, `number`, `date`, `checkbox`, `singleSelect`, `multiSelect`
|
|
339
|
-
* `repeatGroup` (with an empty `fields` array; your builder populates nested fields)
|
|
340
|
-
|
|
341
|
-
This is especially useful in a form builder UI when the user adds a new field of a given type.
|
|
243
|
+
If invalid β throws a ZodError.
|
|
342
244
|
|
|
343
245
|
---
|
|
344
246
|
|
|
345
|
-
##
|
|
346
|
-
|
|
347
|
-
You can register **custom field types** or override core types at runtime:
|
|
247
|
+
## 3. Validate without throwing (UI-friendly)
|
|
348
248
|
|
|
349
249
|
```ts
|
|
350
|
-
import {
|
|
351
|
-
import { z } from "zod";
|
|
352
|
-
|
|
353
|
-
// Simple example: a "richText" field that behaves like a string
|
|
354
|
-
FieldRegistry.register("richText", {
|
|
355
|
-
defaults: {
|
|
356
|
-
label: "Details",
|
|
357
|
-
placeholder: "Write here...",
|
|
358
|
-
},
|
|
359
|
-
buildResponseSchema: (field) => {
|
|
360
|
-
let schema = z.string();
|
|
250
|
+
import { validateReportResponseDetailed } from "@frt-platform/report-core";
|
|
361
251
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (typeof field.maxLength === "number") {
|
|
366
|
-
schema = schema.max(field.maxLength);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return field.required ? schema : schema.optional();
|
|
370
|
-
},
|
|
252
|
+
const result = validateReportResponseDetailed(template, {
|
|
253
|
+
title: "",
|
|
254
|
+
severity: "High",
|
|
371
255
|
});
|
|
372
|
-
```
|
|
373
256
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
257
|
+
if (!result.success) {
|
|
258
|
+
console.log(result.errors);
|
|
259
|
+
}
|
|
260
|
+
```
|
|
378
261
|
|
|
379
|
-
|
|
262
|
+
Produces:
|
|
380
263
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
264
|
+
```ts
|
|
265
|
+
[
|
|
266
|
+
{
|
|
267
|
+
fieldId: "title",
|
|
268
|
+
sectionId: "general",
|
|
269
|
+
sectionTitle: "General Info",
|
|
270
|
+
label: "Incident title",
|
|
271
|
+
code: "field.too_small",
|
|
272
|
+
message:
|
|
273
|
+
'Section "General Info" β Field "Incident title": String must contain at least 1 character(s).'
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
```
|
|
384
277
|
|
|
385
278
|
---
|
|
386
279
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
The core package exposes a generic `createUniqueId` helper:
|
|
280
|
+
# π Conditional Logic Example
|
|
390
281
|
|
|
391
282
|
```ts
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
283
|
+
{
|
|
284
|
+
id: "follow_up_notes",
|
|
285
|
+
type: "longText",
|
|
286
|
+
label: "Follow-up notes",
|
|
287
|
+
visibleIf: { equals: { follow_up_required: true } },
|
|
288
|
+
requiredIf: { equals: { follow_up_required: true } },
|
|
289
|
+
}
|
|
397
290
|
```
|
|
398
291
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Legacy schema migration
|
|
292
|
+
Behavior:
|
|
404
293
|
|
|
405
|
-
If
|
|
294
|
+
* If `follow_up_required = false` β field is **hidden** and **ignored**
|
|
295
|
+
* If `true` β field becomes **required**
|
|
406
296
|
|
|
407
|
-
|
|
408
|
-
* Used older field type names (`text`, `textarea`, `dropdown`, `multiselect`, β¦)
|
|
297
|
+
---
|
|
409
298
|
|
|
410
|
-
|
|
299
|
+
# π Repeat Group Example
|
|
411
300
|
|
|
412
301
|
```ts
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
title: "Old template",
|
|
420
|
-
description: "Using flat fields",
|
|
302
|
+
{
|
|
303
|
+
id: "injured",
|
|
304
|
+
type: "repeatGroup",
|
|
305
|
+
label: "Injured people",
|
|
306
|
+
min: 1,
|
|
307
|
+
max: 5,
|
|
421
308
|
fields: [
|
|
422
|
-
{ id: "
|
|
423
|
-
{ id: "
|
|
424
|
-
]
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const migrated = migrateLegacySchema(legacy);
|
|
428
|
-
const parsed = parseReportTemplateSchema(migrated);
|
|
429
|
-
|
|
430
|
-
console.log(parsed.sections.length); // 1
|
|
431
|
-
console.log(parsed.sections[0].fields[0].type); // "longText"
|
|
309
|
+
{ id: "name", type: "shortText", label: "Name", required: true },
|
|
310
|
+
{ id: "injury", type: "longText", label: "Injury description" }
|
|
311
|
+
]
|
|
312
|
+
}
|
|
432
313
|
```
|
|
433
314
|
|
|
434
|
-
|
|
315
|
+
Response shape:
|
|
435
316
|
|
|
436
317
|
```ts
|
|
437
|
-
|
|
438
|
-
const migrated = migrateLegacySchema(raw);
|
|
439
|
-
const parsed = ReportTemplateSchemaValidator.parse(migrated);
|
|
318
|
+
injured: Array<{ name: string; injury?: string }>;
|
|
440
319
|
```
|
|
441
320
|
|
|
442
|
-
|
|
321
|
+
### π repeatGroup behavior & limitations
|
|
443
322
|
|
|
444
|
-
|
|
323
|
+
* **Base constraints**
|
|
445
324
|
|
|
446
|
-
|
|
325
|
+
* `min` / `max` are always enforced on the row array.
|
|
326
|
+
* Each row is an object keyed by nested field IDs.
|
|
447
327
|
|
|
448
|
-
|
|
449
|
-
import { diffTemplates } from "@frt-platform/report-core";
|
|
328
|
+
* **Conditional `minIf` / `maxIf`**
|
|
450
329
|
|
|
451
|
-
|
|
452
|
-
|
|
330
|
+
* If `minIf` / `maxIf` are present, they are evaluated against the **current response**.
|
|
331
|
+
* When the condition is `true`, the conditional value overrides the static `min` / `max` for that validation pass.
|
|
332
|
+
* When the condition is `false`, the engine falls back to the static `min` / `max` (if any).
|
|
453
333
|
|
|
454
|
-
|
|
334
|
+
* **Conditional logic inside rows**
|
|
455
335
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
console.log(diff.modifiedSections);
|
|
336
|
+
* Nested fields in a repeatGroup support the same `visibleIf` / `requiredIf` semantics as top-level fields.
|
|
337
|
+
* Hidden nested fields are treated as **optional** and are **stripped** from the parsed response, just like hidden top-level fields.
|
|
338
|
+
* For now, row-level conditions see the **full response object**, not just the row. This matches top-level behavior and keeps the logic model simple.
|
|
460
339
|
|
|
461
|
-
|
|
462
|
-
console.log(diff.removedFields);
|
|
463
|
-
console.log(diff.reorderedFields);
|
|
464
|
-
console.log(diff.modifiedFields);
|
|
465
|
-
```
|
|
340
|
+
* **JSON Schema export**
|
|
466
341
|
|
|
467
|
-
`
|
|
468
|
-
|
|
469
|
-
```ts
|
|
470
|
-
{
|
|
471
|
-
sectionId: "section-overview",
|
|
472
|
-
fieldId: "summary",
|
|
473
|
-
before: { label: "What happened?", required: false, ... },
|
|
474
|
-
after: { label: "What happened exactly?", required: true, ... },
|
|
475
|
-
changes: [
|
|
476
|
-
{ key: "label", before: "What happened?", after: "What happened exactly?" },
|
|
477
|
-
{ key: "required", before: false, after: true },
|
|
478
|
-
],
|
|
479
|
-
}
|
|
480
|
-
```
|
|
481
|
-
|
|
482
|
-
Use this for:
|
|
483
|
-
|
|
484
|
-
* βChangesβ tabs in your UI
|
|
485
|
-
* Migration & compatibility warnings
|
|
486
|
-
* Audit trails
|
|
487
|
-
* Showing ordered changes (section/field reorder is explicitly tracked)
|
|
342
|
+
* repeatGroup constraints and conditions are exported via `x-frt-*` vendor extensions (e.g. `x-frt-minIf`, `x-frt-maxIf`, `x-frt-visibleIf`, `x-frt-requiredIf`), so you can mirror this behavior in other runtimes.
|
|
488
343
|
|
|
489
344
|
---
|
|
490
345
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
You can export a template as a **JSON Schema** object for use with OpenAPI, Swagger, Postman, or other runtimes that donβt speak TypeScript/Zod.
|
|
346
|
+
# π§© Field Registry (Custom Types)
|
|
494
347
|
|
|
495
348
|
```ts
|
|
496
|
-
import {
|
|
349
|
+
import { FieldRegistry } from "@frt-platform/report-core";
|
|
350
|
+
import { z } from "zod";
|
|
497
351
|
|
|
498
|
-
|
|
352
|
+
FieldRegistry.register("richText", {
|
|
353
|
+
defaults: { label: "Details" },
|
|
354
|
+
buildResponseSchema(field) {
|
|
355
|
+
let schema = z.string();
|
|
356
|
+
if (field.minLength) schema = schema.min(field.minLength);
|
|
357
|
+
return field.required ? schema : schema.optional();
|
|
358
|
+
},
|
|
359
|
+
});
|
|
499
360
|
```
|
|
500
361
|
|
|
501
|
-
|
|
362
|
+
Now templates may include fields like:
|
|
502
363
|
|
|
503
|
-
```
|
|
504
|
-
{
|
|
505
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
506
|
-
"type": "object",
|
|
507
|
-
"properties": {
|
|
508
|
-
"summary": { "type": "string", "minLength": 10 },
|
|
509
|
-
"status": { "type": "string", "enum": ["Pending", "Resolved", "Escalated"] }
|
|
510
|
-
},
|
|
511
|
-
"required": ["summary", "status"],
|
|
512
|
-
"additionalProperties": false,
|
|
513
|
-
"x-frt-templateTitle": "Incident follow-up",
|
|
514
|
-
"x-frt-version": 1
|
|
515
|
-
}
|
|
364
|
+
```ts
|
|
365
|
+
{ id: "body", type: "richText", label: "Report body" }
|
|
516
366
|
```
|
|
517
367
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
* String/number/boolean/array mapping matches the runtime validation semantics.
|
|
521
|
-
* `required` only includes **unconditionally required** fields (no `visibleIf` / `requiredIf`), but we expose conditional logic as vendor extensions.
|
|
522
|
-
* Extra metadata is available under `x-frt-*` keys, e.g. `x-frt-placeholder`, `x-frt-dataClassification`, `x-frt-visibleIf`, `x-frt-requiredIf`.
|
|
368
|
+
If a template uses a field type that is **not** registered and not one of the built-in core types, the engine safely falls back to `z.any()` so unknown types never crash validation.
|
|
523
369
|
|
|
524
370
|
---
|
|
525
371
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
### Types
|
|
372
|
+
# π§Ύ JSON Schema Export
|
|
529
373
|
|
|
530
374
|
```ts
|
|
531
|
-
import
|
|
532
|
-
ReportTemplateSchema,
|
|
533
|
-
ReportTemplateSection,
|
|
534
|
-
ReportTemplateField,
|
|
535
|
-
ReportTemplateFieldType,
|
|
536
|
-
InferResponse,
|
|
537
|
-
} from "@frt-platform/report-core";
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
* `ReportTemplateFieldType` β union of all field type strings
|
|
541
|
-
* `ReportTemplateField` β one question/field in a section
|
|
542
|
-
* `ReportTemplateSection` β a logical grouping of fields
|
|
543
|
-
* `ReportTemplateSchema` β full template
|
|
544
|
-
* `InferResponse<TTemplate>` β builds a typed response object from a static template
|
|
545
|
-
|
|
546
|
-
### Constants
|
|
375
|
+
import { exportJSONSchema } from "@frt-platform/report-core";
|
|
547
376
|
|
|
548
|
-
|
|
549
|
-
import {
|
|
550
|
-
REPORT_TEMPLATE_VERSION,
|
|
551
|
-
REPORT_TEMPLATE_FIELD_TYPES,
|
|
552
|
-
DEFAULT_FIELD_LABEL,
|
|
553
|
-
CORE_FIELD_DEFAULTS,
|
|
554
|
-
} from "@frt-platform/report-core";
|
|
377
|
+
const jsonSchema = exportJSONSchema(template);
|
|
555
378
|
```
|
|
556
379
|
|
|
557
|
-
|
|
558
|
-
* `REPORT_TEMPLATE_FIELD_TYPES: readonly ReportTemplateFieldType[]`
|
|
559
|
-
* `DEFAULT_FIELD_LABEL: string`
|
|
560
|
-
* `CORE_FIELD_DEFAULTS: Record<ReportTemplateFieldType, Record<string, unknown>>`
|
|
380
|
+
Produces JSON Schema with:
|
|
561
381
|
|
|
562
|
-
|
|
382
|
+
* field types
|
|
383
|
+
* enums
|
|
384
|
+
* min/max constraints
|
|
385
|
+
* default values
|
|
386
|
+
* conditional logic preserved as custom `x-frt-*` properties
|
|
563
387
|
|
|
564
|
-
|
|
565
|
-
import {
|
|
566
|
-
ReportTemplateFieldSchema,
|
|
567
|
-
ReportTemplateSectionSchema,
|
|
568
|
-
ReportTemplateSchemaValidator,
|
|
569
|
-
parseReportTemplateSchema,
|
|
570
|
-
parseReportTemplateSchemaFromString,
|
|
571
|
-
normalizeReportTemplateSchema,
|
|
572
|
-
serializeReportTemplateSchema,
|
|
573
|
-
} from "@frt-platform/report-core";
|
|
574
|
-
```
|
|
388
|
+
---
|
|
575
389
|
|
|
576
|
-
|
|
390
|
+
# π Diff Templates
|
|
577
391
|
|
|
578
392
|
```ts
|
|
579
|
-
import {
|
|
393
|
+
import { diffTemplates } from "@frt-platform/report-core";
|
|
580
394
|
|
|
581
|
-
const
|
|
582
|
-
if (!result.success) {
|
|
583
|
-
// handle validation errors
|
|
584
|
-
}
|
|
395
|
+
const diff = diffTemplates(oldTemplate, newTemplate);
|
|
585
396
|
```
|
|
586
397
|
|
|
587
|
-
|
|
398
|
+
Detects:
|
|
588
399
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
400
|
+
* added/removed/reordered sections
|
|
401
|
+
* added/removed/reordered fields
|
|
402
|
+
* modified fields
|
|
403
|
+
* nested diffs for repeat groups
|
|
592
404
|
|
|
593
|
-
|
|
405
|
+
Perfect for:
|
|
594
406
|
|
|
595
|
-
|
|
407
|
+
* Version history
|
|
408
|
+
* Audit logs
|
|
409
|
+
* Template editing UI
|
|
596
410
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
buildResponseSchemaWithConditions,
|
|
601
|
-
type InferResponse,
|
|
602
|
-
} from "@frt-platform/report-core";
|
|
603
|
-
```
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
# 𧬠Type Inference
|
|
604
414
|
|
|
605
|
-
|
|
415
|
+
Given a template:
|
|
606
416
|
|
|
607
417
|
```ts
|
|
608
|
-
|
|
418
|
+
export const myTemplate = {
|
|
419
|
+
version: 1,
|
|
420
|
+
sections: [
|
|
421
|
+
{
|
|
422
|
+
id: "s",
|
|
423
|
+
fields: [
|
|
424
|
+
{ id: "title", type: "shortText", required: true },
|
|
425
|
+
{ id: "tags", type: "multiSelect", options: ["A", "B"] },
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
]
|
|
429
|
+
} as const;
|
|
609
430
|
```
|
|
610
431
|
|
|
611
|
-
|
|
432
|
+
Infer response type:
|
|
612
433
|
|
|
613
434
|
```ts
|
|
614
|
-
|
|
435
|
+
type MyResponse = InferResponse<typeof myTemplate>;
|
|
615
436
|
```
|
|
616
437
|
|
|
617
|
-
|
|
438
|
+
Produces:
|
|
618
439
|
|
|
619
440
|
```ts
|
|
620
|
-
|
|
441
|
+
type MyResponse = {
|
|
442
|
+
title: string;
|
|
443
|
+
tags?: ("A" | "B")[];
|
|
444
|
+
};
|
|
621
445
|
```
|
|
622
446
|
|
|
623
447
|
---
|
|
624
448
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
This package is intentionally minimal. You can layer it into:
|
|
449
|
+
# π§Ύ Serialization
|
|
628
450
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
* Template builders
|
|
632
|
-
* Dynamic report forms
|
|
633
|
-
* Admin panels for managing report templates
|
|
634
|
-
* **Node/Edge backends** for:
|
|
635
|
-
|
|
636
|
-
* Validating templates on save
|
|
637
|
-
* Migrating old template shapes
|
|
638
|
-
* Ensuring consistent data before persisting to a DB
|
|
639
|
-
* Validating report responses before saving them
|
|
640
|
-
* Serving JSON Schema to other services
|
|
641
|
-
|
|
642
|
-
Typical pattern in a full stack app:
|
|
643
|
-
|
|
644
|
-
1. **Frontend**
|
|
645
|
-
|
|
646
|
-
* Build a visual editor for templates
|
|
647
|
-
* Send template JSON to server
|
|
648
|
-
|
|
649
|
-
2. **Backend**
|
|
451
|
+
```ts
|
|
452
|
+
import { serializeReportTemplateSchema } from "@frt-platform/report-core";
|
|
650
453
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
454
|
+
const json = serializeReportTemplateSchema(template, {
|
|
455
|
+
pretty: true,
|
|
456
|
+
sortSectionsById: true,
|
|
457
|
+
sortFieldsById: true,
|
|
458
|
+
});
|
|
459
|
+
```
|
|
654
460
|
|
|
655
|
-
|
|
656
|
-
* `validateReportResponse(template, data)` before insert
|
|
461
|
+
Useful for deterministic output in Git and stable diffs across environments.
|
|
657
462
|
|
|
658
|
-
|
|
463
|
+
---
|
|
659
464
|
|
|
660
|
-
|
|
661
|
-
* Use them to render dynamic forms
|
|
662
|
-
* Use `validateReportResponse` / `buildResponseSchemaWithConditions` to validate and normalize responses
|
|
663
|
-
* Use `exportJSONSchema` for interoperability with other stacks
|
|
465
|
+
# π§± Roadmap
|
|
664
466
|
|
|
665
|
-
|
|
467
|
+
### Phase 1 β Core Maturation (βοΈ COMPLETE)
|
|
666
468
|
|
|
667
|
-
|
|
469
|
+
* Validation
|
|
470
|
+
* Conditional logic
|
|
471
|
+
* Diffing
|
|
472
|
+
* Field Registry
|
|
473
|
+
* Error helpers
|
|
474
|
+
* Serialization features
|
|
475
|
+
* Parsing & normalization helpers
|
|
668
476
|
|
|
669
|
-
|
|
477
|
+
### Phase 2 β Advanced Field System (IN PROGRESS)
|
|
670
478
|
|
|
671
|
-
*
|
|
479
|
+
* Richer repeatGroup UX
|
|
480
|
+
* Computed fields (design)
|
|
481
|
+
* RichText / FileUpload via registry
|
|
672
482
|
|
|
673
|
-
|
|
674
|
-
* Field editors/renderers (short text, selects, etc.)
|
|
675
|
-
* Auto-generated `<ReportForm />` that consumes a template
|
|
676
|
-
* More advanced field types:
|
|
483
|
+
### Phase 3 β Reactions & Analytics (Planned)
|
|
677
484
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
* Fully-featured `repeatGroup` UI & runtime
|
|
682
|
-
* Richer conditional logic:
|
|
485
|
+
* Scoring rules
|
|
486
|
+
* Auto-tagging
|
|
487
|
+
* Suggested outcomes
|
|
683
488
|
|
|
684
|
-
|
|
685
|
-
* `requiredIf`
|
|
686
|
-
* multi-step branching
|
|
687
|
-
* Multi-language labels & descriptions
|
|
489
|
+
### Phase 4 β React UI Package (Planned)
|
|
688
490
|
|
|
689
|
-
|
|
491
|
+
* Form renderer
|
|
492
|
+
* Template builder
|
|
493
|
+
* Field palette
|
|
494
|
+
* Full ShadCN integration
|
|
690
495
|
|
|
691
496
|
---
|
|
692
497
|
|
|
693
|
-
|
|
498
|
+
# π License
|
|
694
499
|
|
|
695
|
-
MIT
|
|
500
|
+
MIT β feel free to use, extend, or contribute.
|