@frt-platform/report-core 1.2.1 β 1.3.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 +239 -520
- package/dist/index.d.mts +127 -95
- package/dist/index.d.ts +127 -95
- package/dist/index.js +506 -398
- package/dist/index.mjs +508 -398
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,496 +1,289 @@
|
|
|
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
|
+
* Throwing API: `validateReportResponse()`
|
|
53
|
+
* Non-throwing API: `validateReportResponseDetailed()`
|
|
54
|
+
* Rich `ResponseFieldError` objects with:
|
|
56
55
|
|
|
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
|
-
```
|
|
56
|
+
* section title
|
|
57
|
+
* field label
|
|
58
|
+
* error code
|
|
59
|
+
* full message
|
|
60
|
+
* nested repeatGroup row context
|
|
72
61
|
|
|
73
|
-
|
|
62
|
+
### π Template Migration & Normalization
|
|
74
63
|
|
|
75
|
-
|
|
64
|
+
* Legacy format migration (`fields β sections`)
|
|
65
|
+
* Automatic ID normalization & uniqueness enforcement
|
|
66
|
+
* Safe parsing from JSON or raw objects
|
|
76
67
|
|
|
77
|
-
|
|
68
|
+
### π Schema Diffing
|
|
78
69
|
|
|
79
|
-
|
|
70
|
+
Detect:
|
|
80
71
|
|
|
81
|
-
|
|
72
|
+
* Added / removed / reordered sections
|
|
73
|
+
* Added / removed / reordered fields
|
|
74
|
+
* Modified fields
|
|
75
|
+
* Nested diffs inside repeat groups
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
import type { ReportTemplateSchema } from "@frt-platform/report-core";
|
|
77
|
+
### π¦ JSON Schema Export
|
|
85
78
|
|
|
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
|
-
```
|
|
115
|
-
|
|
116
|
-
### 2. Validate and normalize the template
|
|
117
|
-
|
|
118
|
-
Use the helpers to safely parse and normalize unknown input:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
import {
|
|
122
|
-
parseReportTemplateSchema,
|
|
123
|
-
normalizeReportTemplateSchema,
|
|
124
|
-
} from "@frt-platform/report-core";
|
|
125
|
-
|
|
126
|
-
const raw = /* from DB, file, API, etc. */ templateJson;
|
|
127
|
-
|
|
128
|
-
// 1) parse + validate (throws on invalid input)
|
|
129
|
-
const parsed = parseReportTemplateSchema(raw);
|
|
130
|
-
|
|
131
|
-
// 2) optional extra normalization step (IDs, trimming, de-duplication, etc.)
|
|
132
|
-
const normalized = normalizeReportTemplateSchema(parsed);
|
|
133
|
-
|
|
134
|
-
console.log(normalized.sections[0].fields[0].id);
|
|
135
|
-
// always a non-empty, safe identifier
|
|
136
|
-
```
|
|
79
|
+
* Export a template as a valid JSON Schema (2020-12 draft)
|
|
80
|
+
* Includes vendor extensions:
|
|
137
81
|
|
|
138
|
-
|
|
82
|
+
* `x-frt-visibleIf`
|
|
83
|
+
* `x-frt-requiredIf`
|
|
84
|
+
* repeatGroup min/max
|
|
85
|
+
* placeholders
|
|
86
|
+
* Useful for OpenAPI, Postman, or other backend runtimes.
|
|
139
87
|
|
|
140
|
-
|
|
141
|
-
import { serializeReportTemplateSchema } from "@frt-platform/report-core";
|
|
88
|
+
### π Field Registry
|
|
142
89
|
|
|
143
|
-
|
|
144
|
-
// ready to store in DB, file, etc.
|
|
145
|
-
```
|
|
90
|
+
Extend the system at runtime:
|
|
146
91
|
|
|
147
|
-
|
|
92
|
+
* Add custom types (`richText`, `fileUpload`, etc.)
|
|
93
|
+
* Override validation logic
|
|
94
|
+
* Provide metadata for UI packages
|
|
148
95
|
|
|
149
|
-
|
|
96
|
+
### 𧬠Type Inference
|
|
150
97
|
|
|
151
|
-
|
|
98
|
+
Get a fully typed response type from a template:
|
|
152
99
|
|
|
153
100
|
```ts
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
const template = parseReportTemplateSchemaFromString(userInputString);
|
|
158
|
-
// valid template here
|
|
159
|
-
} catch (err) {
|
|
160
|
-
// invalid JSON or schema β show error to user
|
|
161
|
-
}
|
|
101
|
+
type MyResponse = InferResponse<typeof template>;
|
|
162
102
|
```
|
|
163
103
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
## Validating report responses
|
|
104
|
+
### π§Ύ Serialization Helpers
|
|
167
105
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
### Runtime validation
|
|
106
|
+
Deterministic JSON output with sorting options:
|
|
171
107
|
|
|
172
108
|
```ts
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
summary: "Student slipped in the hallway, no serious injuries.",
|
|
179
|
-
status: "Resolved",
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// Validate (throws ZodError on failure)
|
|
183
|
-
const safeResponse = validateReportResponse(template, responseData);
|
|
109
|
+
serializeReportTemplateSchema(template, {
|
|
110
|
+
pretty: true,
|
|
111
|
+
sortSectionsById: true,
|
|
112
|
+
sortFieldsById: true,
|
|
113
|
+
});
|
|
184
114
|
```
|
|
185
115
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
* `required` flags
|
|
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**:
|
|
195
|
-
|
|
196
|
-
* `visibleIf`: if false, the field is treated as invisible and removed from the parsed output
|
|
197
|
-
* `requiredIf`: if true, the field is treated as required for this specific response
|
|
198
|
-
|
|
199
|
-
You can still build a raw schema yourself using the conditional builder:
|
|
200
|
-
|
|
201
|
-
```ts
|
|
202
|
-
import { buildResponseSchemaWithConditions } from "@frt-platform/report-core";
|
|
203
|
-
|
|
204
|
-
const responseSchema = buildResponseSchemaWithConditions(template, responseData);
|
|
205
|
-
const safeResponse = responseSchema.parse(responseData);
|
|
206
|
-
```
|
|
116
|
+
Perfect for Git diffs and storage.
|
|
207
117
|
|
|
208
118
|
---
|
|
209
119
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Fields can declare conditions that refer to other field values.
|
|
213
|
-
|
|
214
|
-
Example:
|
|
120
|
+
# π¦ Installation
|
|
215
121
|
|
|
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
|
-
}
|
|
122
|
+
```bash
|
|
123
|
+
npm install @frt-platform/report-core zod
|
|
224
124
|
```
|
|
225
125
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* `{"equals": { "fieldId": value } }`
|
|
229
|
-
* `{"any": [cond1, cond2, ...] }`
|
|
230
|
-
* `{"all": [cond1, cond2, ...] }`
|
|
231
|
-
* `{"not": cond }`
|
|
232
|
-
|
|
233
|
-
At runtime:
|
|
234
|
-
|
|
235
|
-
* If `visibleIf` is **false** β field is treated as **invisible**, becomes optional, and is stripped from the validated response.
|
|
236
|
-
* If `requiredIf` is **true** β field is treated as **required** for that specific response.
|
|
126
|
+
`zod` is a peer dependency.
|
|
237
127
|
|
|
238
128
|
---
|
|
239
129
|
|
|
240
|
-
|
|
130
|
+
# π Quickstart
|
|
241
131
|
|
|
242
|
-
|
|
132
|
+
## 1. Define a template
|
|
243
133
|
|
|
244
134
|
```ts
|
|
245
|
-
import type {
|
|
135
|
+
import type { ReportTemplateSchema } from "@frt-platform/report-core";
|
|
246
136
|
|
|
247
|
-
const
|
|
137
|
+
const template: ReportTemplateSchema = {
|
|
248
138
|
version: 1,
|
|
249
139
|
sections: [
|
|
250
140
|
{
|
|
251
|
-
id: "
|
|
141
|
+
id: "general",
|
|
142
|
+
title: "General Info",
|
|
252
143
|
fields: [
|
|
253
144
|
{
|
|
254
|
-
id: "
|
|
255
|
-
type: "
|
|
256
|
-
label: "
|
|
145
|
+
id: "title",
|
|
146
|
+
type: "shortText",
|
|
147
|
+
label: "Incident title",
|
|
257
148
|
required: true,
|
|
258
|
-
minLength: 10,
|
|
259
149
|
},
|
|
260
150
|
{
|
|
261
|
-
id: "
|
|
151
|
+
id: "severity",
|
|
262
152
|
type: "singleSelect",
|
|
263
|
-
label: "
|
|
153
|
+
label: "Severity",
|
|
264
154
|
required: true,
|
|
265
|
-
options: ["
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
id: "follow_up_date",
|
|
269
|
-
type: "date",
|
|
270
|
-
label: "Follow-up date",
|
|
155
|
+
options: ["Low", "Medium", "High"],
|
|
271
156
|
},
|
|
272
157
|
{
|
|
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,
|
|
158
|
+
id: "details",
|
|
159
|
+
type: "longText",
|
|
160
|
+
label: "Details",
|
|
161
|
+
minLength: 10,
|
|
283
162
|
},
|
|
284
163
|
],
|
|
285
164
|
},
|
|
286
165
|
],
|
|
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
166
|
};
|
|
302
167
|
```
|
|
303
168
|
|
|
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
169
|
---
|
|
311
170
|
|
|
312
|
-
##
|
|
313
|
-
|
|
314
|
-
The package includes core defaults for each field type (no UI, no icons):
|
|
171
|
+
## 2. Validate a response (throwing API)
|
|
315
172
|
|
|
316
173
|
```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
|
-
}
|
|
174
|
+
import { validateReportResponse } from "@frt-platform/report-core";
|
|
332
175
|
|
|
333
|
-
const
|
|
176
|
+
const parsed = validateReportResponse(template, {
|
|
177
|
+
title: "Broken fire alarm",
|
|
178
|
+
severity: "High",
|
|
179
|
+
details: "Triggered after smoke test",
|
|
180
|
+
});
|
|
334
181
|
```
|
|
335
182
|
|
|
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.
|
|
183
|
+
If invalid β throws a ZodError.
|
|
342
184
|
|
|
343
185
|
---
|
|
344
186
|
|
|
345
|
-
##
|
|
346
|
-
|
|
347
|
-
You can register **custom field types** or override core types at runtime:
|
|
187
|
+
## 3. Validate without throwing (UI-friendly)
|
|
348
188
|
|
|
349
189
|
```ts
|
|
350
|
-
import {
|
|
351
|
-
import { z } from "zod";
|
|
190
|
+
import { validateReportResponseDetailed } from "@frt-platform/report-core";
|
|
352
191
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
label: "Details",
|
|
357
|
-
placeholder: "Write here...",
|
|
358
|
-
},
|
|
359
|
-
buildResponseSchema: (field) => {
|
|
360
|
-
let schema = z.string();
|
|
361
|
-
|
|
362
|
-
if (typeof field.minLength === "number") {
|
|
363
|
-
schema = schema.min(field.minLength);
|
|
364
|
-
}
|
|
365
|
-
if (typeof field.maxLength === "number") {
|
|
366
|
-
schema = schema.max(field.maxLength);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return field.required ? schema : schema.optional();
|
|
370
|
-
},
|
|
192
|
+
const result = validateReportResponseDetailed(template, {
|
|
193
|
+
title: "",
|
|
194
|
+
severity: "High",
|
|
371
195
|
});
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
At validation time, `validateReportResponse` and `buildBaseFieldSchema` will:
|
|
375
196
|
|
|
376
|
-
|
|
377
|
-
|
|
197
|
+
if (!result.success) {
|
|
198
|
+
console.log(result.errors);
|
|
199
|
+
}
|
|
200
|
+
```
|
|
378
201
|
|
|
379
|
-
|
|
202
|
+
Produces:
|
|
380
203
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
204
|
+
```ts
|
|
205
|
+
[
|
|
206
|
+
{
|
|
207
|
+
fieldId: "title",
|
|
208
|
+
sectionId: "general",
|
|
209
|
+
sectionTitle: "General Info",
|
|
210
|
+
label: "Incident title",
|
|
211
|
+
code: "field.too_small",
|
|
212
|
+
message: 'Section "General Info" β Field "Incident title": String must contain at least 1 character(s).'
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
```
|
|
384
216
|
|
|
385
217
|
---
|
|
386
218
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
The core package exposes a generic `createUniqueId` helper:
|
|
219
|
+
# π Conditional Logic Example
|
|
390
220
|
|
|
391
221
|
```ts
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
222
|
+
{
|
|
223
|
+
id: "follow_up_notes",
|
|
224
|
+
type: "longText",
|
|
225
|
+
label: "Follow-up notes",
|
|
226
|
+
visibleIf: { equals: { follow_up_required: true } },
|
|
227
|
+
requiredIf: { equals: { follow_up_required: true } },
|
|
228
|
+
}
|
|
397
229
|
```
|
|
398
230
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
## Legacy schema migration
|
|
231
|
+
Behavior:
|
|
404
232
|
|
|
405
|
-
If
|
|
233
|
+
* If `follow_up_required = false` β field is **hidden** and **ignored**
|
|
234
|
+
* If `true` β field becomes **required**
|
|
406
235
|
|
|
407
|
-
|
|
408
|
-
* Used older field type names (`text`, `textarea`, `dropdown`, `multiselect`, β¦)
|
|
236
|
+
---
|
|
409
237
|
|
|
410
|
-
|
|
238
|
+
# π Repeat Group Example
|
|
411
239
|
|
|
412
240
|
```ts
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
title: "Old template",
|
|
420
|
-
description: "Using flat fields",
|
|
241
|
+
{
|
|
242
|
+
id: "injured",
|
|
243
|
+
type: "repeatGroup",
|
|
244
|
+
label: "Injured people",
|
|
245
|
+
min: 1,
|
|
246
|
+
max: 5,
|
|
421
247
|
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"
|
|
248
|
+
{ id: "name", type: "shortText", label: "Name", required: true },
|
|
249
|
+
{ id: "injury", type: "longText", label: "Injury description" }
|
|
250
|
+
]
|
|
251
|
+
}
|
|
432
252
|
```
|
|
433
253
|
|
|
434
|
-
|
|
254
|
+
Response shape:
|
|
435
255
|
|
|
436
256
|
```ts
|
|
437
|
-
|
|
438
|
-
const migrated = migrateLegacySchema(raw);
|
|
439
|
-
const parsed = ReportTemplateSchemaValidator.parse(migrated);
|
|
257
|
+
injured: Array<{ name: string; injury?: string }>;
|
|
440
258
|
```
|
|
441
259
|
|
|
442
260
|
---
|
|
443
261
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
When you version templates (e.g. editing in a UI), you might want to see what changed between versions. The core exposes a diff utility:
|
|
262
|
+
# π§© Field Registry (Custom Types)
|
|
447
263
|
|
|
448
264
|
```ts
|
|
449
|
-
import {
|
|
450
|
-
|
|
451
|
-
const beforeTemplate = /* old template */;
|
|
452
|
-
const afterTemplate = /* updated template */;
|
|
453
|
-
|
|
454
|
-
const diff = diffTemplates(beforeTemplate, afterTemplate);
|
|
455
|
-
|
|
456
|
-
console.log(diff.addedSections);
|
|
457
|
-
console.log(diff.removedSections);
|
|
458
|
-
console.log(diff.reorderedSections);
|
|
459
|
-
console.log(diff.modifiedSections);
|
|
265
|
+
import { FieldRegistry } from "@frt-platform/report-core";
|
|
266
|
+
import { z } from "zod";
|
|
460
267
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
268
|
+
FieldRegistry.register("richText", {
|
|
269
|
+
defaults: { label: "Details" },
|
|
270
|
+
buildResponseSchema(field) {
|
|
271
|
+
let schema = z.string();
|
|
272
|
+
if (field.minLength) schema = schema.min(field.minLength);
|
|
273
|
+
return field.required ? schema : schema.optional();
|
|
274
|
+
},
|
|
275
|
+
});
|
|
465
276
|
```
|
|
466
277
|
|
|
467
|
-
|
|
278
|
+
Now templates may include fields like:
|
|
468
279
|
|
|
469
280
|
```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
|
-
}
|
|
281
|
+
{ id: "body", type: "richText", label: "Report body" }
|
|
480
282
|
```
|
|
481
283
|
|
|
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)
|
|
488
|
-
|
|
489
284
|
---
|
|
490
285
|
|
|
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.
|
|
286
|
+
# π§Ύ JSON Schema Export
|
|
494
287
|
|
|
495
288
|
```ts
|
|
496
289
|
import { exportJSONSchema } from "@frt-platform/report-core";
|
|
@@ -498,198 +291,124 @@ import { exportJSONSchema } from "@frt-platform/report-core";
|
|
|
498
291
|
const jsonSchema = exportJSONSchema(template);
|
|
499
292
|
```
|
|
500
293
|
|
|
501
|
-
|
|
294
|
+
Produces JSON Schema with:
|
|
502
295
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
-
}
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
Key points:
|
|
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`.
|
|
296
|
+
* field types
|
|
297
|
+
* enums
|
|
298
|
+
* min/max constraints
|
|
299
|
+
* default values
|
|
300
|
+
* conditional logic preserved as custom `x-frt-*` properties
|
|
523
301
|
|
|
524
302
|
---
|
|
525
303
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
### Types
|
|
529
|
-
|
|
530
|
-
```ts
|
|
531
|
-
import type {
|
|
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
|
|
304
|
+
# π Diff Templates
|
|
547
305
|
|
|
548
306
|
```ts
|
|
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";
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
* `REPORT_TEMPLATE_VERSION: number`
|
|
558
|
-
* `REPORT_TEMPLATE_FIELD_TYPES: readonly ReportTemplateFieldType[]`
|
|
559
|
-
* `DEFAULT_FIELD_LABEL: string`
|
|
560
|
-
* `CORE_FIELD_DEFAULTS: Record<ReportTemplateFieldType, Record<string, unknown>>`
|
|
561
|
-
|
|
562
|
-
### Validation & parsing
|
|
307
|
+
import { diffTemplates } from "@frt-platform/report-core";
|
|
563
308
|
|
|
564
|
-
|
|
565
|
-
import {
|
|
566
|
-
ReportTemplateFieldSchema,
|
|
567
|
-
ReportTemplateSectionSchema,
|
|
568
|
-
ReportTemplateSchemaValidator,
|
|
569
|
-
parseReportTemplateSchema,
|
|
570
|
-
parseReportTemplateSchemaFromString,
|
|
571
|
-
normalizeReportTemplateSchema,
|
|
572
|
-
serializeReportTemplateSchema,
|
|
573
|
-
} from "@frt-platform/report-core";
|
|
309
|
+
const diff = diffTemplates(oldTemplate, newTemplate);
|
|
574
310
|
```
|
|
575
311
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
```ts
|
|
579
|
-
import { ReportTemplateSchemaValidator } from "@frt-platform/report-core";
|
|
580
|
-
|
|
581
|
-
const result = ReportTemplateSchemaValidator.safeParse(raw);
|
|
582
|
-
if (!result.success) {
|
|
583
|
-
// handle validation errors
|
|
584
|
-
}
|
|
585
|
-
```
|
|
312
|
+
Detects:
|
|
586
313
|
|
|
587
|
-
|
|
314
|
+
* added/removed/reordered sections
|
|
315
|
+
* added/removed/reordered fields
|
|
316
|
+
* modified fields
|
|
317
|
+
* nested diffs for repeat groups
|
|
588
318
|
|
|
589
|
-
|
|
590
|
-
import { migrateLegacySchema } from "@frt-platform/report-core";
|
|
591
|
-
```
|
|
319
|
+
Perfect for:
|
|
592
320
|
|
|
593
|
-
|
|
321
|
+
* Version history
|
|
322
|
+
* Audit logs
|
|
323
|
+
* Template editing UI
|
|
594
324
|
|
|
595
|
-
|
|
325
|
+
---
|
|
596
326
|
|
|
597
|
-
|
|
598
|
-
import {
|
|
599
|
-
validateReportResponse,
|
|
600
|
-
buildResponseSchemaWithConditions,
|
|
601
|
-
type InferResponse,
|
|
602
|
-
} from "@frt-platform/report-core";
|
|
603
|
-
```
|
|
327
|
+
# 𧬠Type Inference
|
|
604
328
|
|
|
605
|
-
|
|
329
|
+
Given a template:
|
|
606
330
|
|
|
607
331
|
```ts
|
|
608
|
-
|
|
332
|
+
export const myTemplate = {
|
|
333
|
+
version: 1,
|
|
334
|
+
sections: [
|
|
335
|
+
{
|
|
336
|
+
id: "s",
|
|
337
|
+
fields: [
|
|
338
|
+
{ id: "title", type: "shortText", required: true },
|
|
339
|
+
{ id: "tags", type: "multiSelect", options: ["A", "B"] },
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
} as const;
|
|
609
344
|
```
|
|
610
345
|
|
|
611
|
-
|
|
346
|
+
Infer response type:
|
|
612
347
|
|
|
613
348
|
```ts
|
|
614
|
-
|
|
349
|
+
type MyResponse = InferResponse<typeof myTemplate>;
|
|
615
350
|
```
|
|
616
351
|
|
|
617
|
-
|
|
352
|
+
Produces:
|
|
618
353
|
|
|
619
354
|
```ts
|
|
620
|
-
|
|
355
|
+
type MyResponse = {
|
|
356
|
+
title: string;
|
|
357
|
+
tags?: ("A" | "B")[];
|
|
358
|
+
};
|
|
621
359
|
```
|
|
622
360
|
|
|
623
361
|
---
|
|
624
362
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
This package is intentionally minimal. You can layer it into:
|
|
363
|
+
# π§Ύ Serialization
|
|
628
364
|
|
|
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**
|
|
365
|
+
```ts
|
|
366
|
+
import { serializeReportTemplateSchema } from "@frt-platform/report-core";
|
|
650
367
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
368
|
+
const json = serializeReportTemplateSchema(template, {
|
|
369
|
+
pretty: true,
|
|
370
|
+
sortSectionsById: true,
|
|
371
|
+
sortFieldsById: true,
|
|
372
|
+
});
|
|
373
|
+
```
|
|
654
374
|
|
|
655
|
-
|
|
656
|
-
* `validateReportResponse(template, data)` before insert
|
|
375
|
+
Useful for deterministic output in Git.
|
|
657
376
|
|
|
658
|
-
|
|
377
|
+
---
|
|
659
378
|
|
|
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
|
|
379
|
+
# π§± Roadmap
|
|
664
380
|
|
|
665
|
-
|
|
381
|
+
### Phase 1 β Core Maturation (βοΈ COMPLETE)
|
|
666
382
|
|
|
667
|
-
|
|
383
|
+
* Validation
|
|
384
|
+
* Conditional logic
|
|
385
|
+
* Diffing
|
|
386
|
+
* Field Registry
|
|
387
|
+
* Error helpers
|
|
388
|
+
* Serialization features
|
|
668
389
|
|
|
669
|
-
|
|
390
|
+
### Phase 2 β Advanced Field System (IN PROGRESS)
|
|
670
391
|
|
|
671
|
-
*
|
|
392
|
+
* Repeat group conditional rules (`minIf` / `maxIf`)
|
|
393
|
+
* Conditional logic inside group rows
|
|
394
|
+
* Computed fields (design)
|
|
395
|
+
* RichText / FileUpload via registry
|
|
672
396
|
|
|
673
|
-
|
|
674
|
-
* Field editors/renderers (short text, selects, etc.)
|
|
675
|
-
* Auto-generated `<ReportForm />` that consumes a template
|
|
676
|
-
* More advanced field types:
|
|
397
|
+
### Phase 3 β Reactions & Analytics (Planned)
|
|
677
398
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
* Fully-featured `repeatGroup` UI & runtime
|
|
682
|
-
* Richer conditional logic:
|
|
399
|
+
* Scoring rules
|
|
400
|
+
* Auto-tagging
|
|
401
|
+
* Suggested outcomes
|
|
683
402
|
|
|
684
|
-
|
|
685
|
-
* `requiredIf`
|
|
686
|
-
* multi-step branching
|
|
687
|
-
* Multi-language labels & descriptions
|
|
403
|
+
### Phase 4 β React UI Package (Planned)
|
|
688
404
|
|
|
689
|
-
|
|
405
|
+
* Form renderer
|
|
406
|
+
* Template builder
|
|
407
|
+
* Field palette
|
|
408
|
+
* Full ShadCN integration
|
|
690
409
|
|
|
691
410
|
---
|
|
692
411
|
|
|
693
|
-
|
|
412
|
+
# π License
|
|
694
413
|
|
|
695
|
-
MIT
|
|
414
|
+
MIT β feel free to use, extend, or contribute.
|