@frt-platform/report-core 1.2.0 → 1.2.1

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 (2) hide show
  1. package/README.md +193 -49
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # @frt/report-core
1
+ # `@frt-platform/report-core`
2
2
 
3
- Core engine for building, validating, and migrating **report templates** – plus validating **report responses**.
3
+ Core engine for building, validating, and migrating **report templates** – plus validating **report responses** and exporting **JSON Schema**.
4
4
 
5
5
  This package is:
6
6
 
@@ -15,9 +15,12 @@ It gives you:
15
15
  * **Legacy schema migration** (flat `fields` → `sections`, old type names, etc.)
16
16
  * **Normalization** helpers to ensure safe IDs & consistent structure
17
17
  * **Field defaults** and **ID utilities** you can use to build your own UI
18
- * **Response validation** based on templates (`buildResponseSchema`, `validateReportResponse`)
19
- * **Template diffing** (`diffTemplates`) to compare template versions
18
+ * **Response validation** based on templates (`validateReportResponse`)
19
+ * **Conditional logic** for fields (`visibleIf` / `requiredIf`)
20
+ * **Template diffing** (`diffTemplates`) to compare template versions (including ordering)
20
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
21
24
 
22
25
  It’s the core that powers a flexible incident/report builder, but it’s generic enough to be reused in any app.
23
26
 
@@ -27,13 +30,13 @@ It’s the core that powers a flexible incident/report builder, but it’s gener
27
30
 
28
31
  ```bash
29
32
  # npm
30
- npm install @frt/report-core zod
33
+ npm install @frt-platform/report-core zod
31
34
 
32
35
  # yarn
33
- yarn add @frt/report-core zod
36
+ yarn add @frt-platform/report-core zod
34
37
 
35
38
  # pnpm
36
- pnpm add @frt/report-core zod
39
+ pnpm add @frt-platform/report-core zod
37
40
  ```
38
41
 
39
42
  `zod` is a peer dependency and is used for schema validation & parsing.
@@ -47,11 +50,12 @@ The core model looks like this:
47
50
  * A **template** contains multiple **sections**
48
51
  * A **section** contains multiple **fields**
49
52
  * A **field** has a `type` (short text, long text, number, select, etc.) and optional constraints
53
+ * Fields can have **conditional logic** (`visibleIf`, `requiredIf`)
50
54
 
51
55
  Supported field types:
52
56
 
53
57
  ```ts
54
- import { REPORT_TEMPLATE_FIELD_TYPES } from "@frt/report-core";
58
+ import { REPORT_TEMPLATE_FIELD_TYPES } from "@frt-platform/report-core";
55
59
 
56
60
  console.log(REPORT_TEMPLATE_FIELD_TYPES);
57
61
  // [
@@ -62,9 +66,12 @@ console.log(REPORT_TEMPLATE_FIELD_TYPES);
62
66
  // "checkbox",
63
67
  // "singleSelect",
64
68
  // "multiSelect",
69
+ // "repeatGroup", // reserved for repeating groups / fieldsets
65
70
  // ]
66
71
  ```
67
72
 
73
+ > `repeatGroup` is reserved for repeating fieldsets (e.g. multiple injured people); the core schema knows about it and provides defaults, and the full runtime semantics can be layered on top in your app/React package.
74
+
68
75
  ---
69
76
 
70
77
  ## Quick Start
@@ -74,7 +81,7 @@ console.log(REPORT_TEMPLATE_FIELD_TYPES);
74
81
  You can define templates statically or load them from DB / API:
75
82
 
76
83
  ```ts
77
- import type { ReportTemplateSchema } from "@frt/report-core";
84
+ import type { ReportTemplateSchema } from "@frt-platform/report-core";
78
85
 
79
86
  const templateJson: ReportTemplateSchema = {
80
87
  version: 1,
@@ -114,14 +121,14 @@ Use the helpers to safely parse and normalize unknown input:
114
121
  import {
115
122
  parseReportTemplateSchema,
116
123
  normalizeReportTemplateSchema,
117
- } from "@frt/report-core";
124
+ } from "@frt-platform/report-core";
118
125
 
119
126
  const raw = /* from DB, file, API, etc. */ templateJson;
120
127
 
121
128
  // 1) parse + validate (throws on invalid input)
122
129
  const parsed = parseReportTemplateSchema(raw);
123
130
 
124
- // 2) optional extra normalization step (IDs, trimming, etc.)
131
+ // 2) optional extra normalization step (IDs, trimming, de-duplication, etc.)
125
132
  const normalized = normalizeReportTemplateSchema(parsed);
126
133
 
127
134
  console.log(normalized.sections[0].fields[0].id);
@@ -131,7 +138,7 @@ console.log(normalized.sections[0].fields[0].id);
131
138
  ### 3. Serialize for storage
132
139
 
133
140
  ```ts
134
- import { serializeReportTemplateSchema } from "@frt/report-core";
141
+ import { serializeReportTemplateSchema } from "@frt-platform/report-core";
135
142
 
136
143
  const jsonString = serializeReportTemplateSchema(normalized);
137
144
  // ready to store in DB, file, etc.
@@ -144,7 +151,7 @@ const jsonString = serializeReportTemplateSchema(normalized);
144
151
  If you have a JSON string (e.g. from a form textarea):
145
152
 
146
153
  ```ts
147
- import { parseReportTemplateSchemaFromString } from "@frt/report-core";
154
+ import { parseReportTemplateSchemaFromString } from "@frt-platform/report-core";
148
155
 
149
156
  try {
150
157
  const template = parseReportTemplateSchemaFromString(userInputString);
@@ -160,33 +167,23 @@ try {
160
167
 
161
168
  Once you have a template, you can validate **user-submitted responses** against it.
162
169
 
163
- ### Runtime schema for responses
170
+ ### Runtime validation
164
171
 
165
172
  ```ts
166
- import {
167
- buildResponseSchema,
168
- validateReportResponse,
169
- } from "@frt/report-core";
173
+ import { validateReportResponse } from "@frt-platform/report-core";
170
174
 
171
175
  const template = /* a valid ReportTemplateSchema */ normalized;
172
176
 
173
- // Build a Zod schema for responses
174
- const responseSchema = buildResponseSchema(template);
175
-
176
- // Example response object
177
177
  const responseData = {
178
178
  summary: "Student slipped in the hallway, no serious injuries.",
179
179
  status: "Resolved",
180
180
  };
181
181
 
182
182
  // Validate (throws ZodError on failure)
183
- const safeResponse = responseSchema.parse(responseData);
184
-
185
- // Or use the convenience helper:
186
- const safeResponse2 = validateReportResponse(template, responseData);
183
+ const safeResponse = validateReportResponse(template, responseData);
187
184
  ```
188
185
 
189
- `buildResponseSchema` respects:
186
+ `validateReportResponse` respects:
190
187
 
191
188
  * `required` flags
192
189
  * `minLength` / `maxLength` (short/long text)
@@ -194,6 +191,49 @@ const safeResponse2 = validateReportResponse(template, responseData);
194
191
  * `minSelections` / `maxSelections` (multiSelect)
195
192
  * `options` (singleSelect + multiSelect)
196
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
+ ```
207
+
208
+ ---
209
+
210
+ ## Conditional logic (`visibleIf` / `requiredIf`)
211
+
212
+ Fields can declare conditions that refer to other field values.
213
+
214
+ Example:
215
+
216
+ ```json
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
+ ```
225
+
226
+ Supported condition shapes:
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.
197
237
 
198
238
  ---
199
239
 
@@ -202,7 +242,7 @@ const safeResponse2 = validateReportResponse(template, responseData);
202
242
  If you define templates statically (`as const`), you can get a fully-typed response type from the template:
203
243
 
204
244
  ```ts
205
- import type { InferResponse } from "@frt/report-core";
245
+ import type { InferResponse } from "@frt-platform/report-core";
206
246
 
207
247
  const incidentTemplate = {
208
248
  version: 1,
@@ -278,7 +318,7 @@ import {
278
318
  CORE_FIELD_DEFAULTS,
279
319
  DEFAULT_FIELD_LABEL,
280
320
  type ReportTemplateFieldType,
281
- } from "@frt/report-core";
321
+ } from "@frt-platform/report-core";
282
322
 
283
323
  function createDefaultField(type: ReportTemplateFieldType) {
284
324
  const defaults = CORE_FIELD_DEFAULTS[type] ?? {};
@@ -293,16 +333,63 @@ function createDefaultField(type: ReportTemplateFieldType) {
293
333
  const field = createDefaultField("shortText");
294
334
  ```
295
335
 
336
+ `CORE_FIELD_DEFAULTS` includes basic defaults for:
337
+
338
+ * `shortText`, `longText`, `number`, `date`, `checkbox`, `singleSelect`, `multiSelect`
339
+ * `repeatGroup` (with an empty `fields` array; your builder populates nested fields)
340
+
296
341
  This is especially useful in a form builder UI when the user adds a new field of a given type.
297
342
 
298
343
  ---
299
344
 
345
+ ## Field Registry (`FieldRegistry`)
346
+
347
+ You can register **custom field types** or override core types at runtime:
348
+
349
+ ```ts
350
+ import { FieldRegistry } from "@frt-platform/report-core";
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();
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
+ },
371
+ });
372
+ ```
373
+
374
+ At validation time, `validateReportResponse` and `buildBaseFieldSchema` will:
375
+
376
+ 1. Check the registry for a custom handler.
377
+ 2. Use the built-in fallback if none is registered.
378
+
379
+ Use this for:
380
+
381
+ * App-specific field types (e.g. `fileUpload`, `signature`, `mapLocation`)
382
+ * Tenant-specific extensions in a multi-tenant platform
383
+ * Gradually introducing new field behaviors without changing the core package
384
+
385
+ ---
386
+
300
387
  ## Generating unique IDs
301
388
 
302
389
  The core package exposes a generic `createUniqueId` helper:
303
390
 
304
391
  ```ts
305
- import { createUniqueId } from "@frt/report-core";
392
+ import { createUniqueId } from "@frt-platform/report-core";
306
393
 
307
394
  const existingSectionIds = ["section-1", "section-2"];
308
395
  const newId = createUniqueId("section", existingSectionIds);
@@ -326,7 +413,7 @@ you can pass them through the migration pipeline:
326
413
  import {
327
414
  migrateLegacySchema,
328
415
  parseReportTemplateSchema,
329
- } from "@frt/report-core";
416
+ } from "@frt-platform/report-core";
330
417
 
331
418
  const legacy = {
332
419
  title: "Old template",
@@ -356,10 +443,10 @@ const parsed = ReportTemplateSchemaValidator.parse(migrated);
356
443
 
357
444
  ## Diffing templates
358
445
 
359
- When you version templates (e.g. editing in a UI), you might want to see what changed between versions. The core exposes a simple diff utility:
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:
360
447
 
361
448
  ```ts
362
- import { diffTemplates } from "@frt/report-core";
449
+ import { diffTemplates } from "@frt-platform/report-core";
363
450
 
364
451
  const beforeTemplate = /* old template */;
365
452
  const afterTemplate = /* updated template */;
@@ -368,14 +455,19 @@ const diff = diffTemplates(beforeTemplate, afterTemplate);
368
455
 
369
456
  console.log(diff.addedSections);
370
457
  console.log(diff.removedSections);
371
- console.log(diff.fieldChanges);
458
+ console.log(diff.reorderedSections);
459
+ console.log(diff.modifiedSections);
460
+
461
+ console.log(diff.addedFields);
462
+ console.log(diff.removedFields);
463
+ console.log(diff.reorderedFields);
464
+ console.log(diff.modifiedFields);
372
465
  ```
373
466
 
374
- `diff.fieldChanges` contains entries like:
467
+ `diff.modifiedFields` contains entries like:
375
468
 
376
469
  ```ts
377
470
  {
378
- type: "modified",
379
471
  sectionId: "section-overview",
380
472
  fieldId: "summary",
381
473
  before: { label: "What happened?", required: false, ... },
@@ -392,6 +484,42 @@ Use this for:
392
484
  * “Changes” tabs in your UI
393
485
  * Migration & compatibility warnings
394
486
  * Audit trails
487
+ * Showing ordered changes (section/field reorder is explicitly tracked)
488
+
489
+ ---
490
+
491
+ ## JSON Schema export
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.
494
+
495
+ ```ts
496
+ import { exportJSONSchema } from "@frt-platform/report-core";
497
+
498
+ const jsonSchema = exportJSONSchema(template);
499
+ ```
500
+
501
+ Shape (simplified):
502
+
503
+ ```json
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
+ }
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`.
395
523
 
396
524
  ---
397
525
 
@@ -406,7 +534,7 @@ import type {
406
534
  ReportTemplateField,
407
535
  ReportTemplateFieldType,
408
536
  InferResponse,
409
- } from "@frt/report-core";
537
+ } from "@frt-platform/report-core";
410
538
  ```
411
539
 
412
540
  * `ReportTemplateFieldType` – union of all field type strings
@@ -423,7 +551,7 @@ import {
423
551
  REPORT_TEMPLATE_FIELD_TYPES,
424
552
  DEFAULT_FIELD_LABEL,
425
553
  CORE_FIELD_DEFAULTS,
426
- } from "@frt/report-core";
554
+ } from "@frt-platform/report-core";
427
555
  ```
428
556
 
429
557
  * `REPORT_TEMPLATE_VERSION: number`
@@ -442,13 +570,13 @@ import {
442
570
  parseReportTemplateSchemaFromString,
443
571
  normalizeReportTemplateSchema,
444
572
  serializeReportTemplateSchema,
445
- } from "@frt/report-core";
573
+ } from "@frt-platform/report-core";
446
574
  ```
447
575
 
448
576
  Use the Zod schemas directly if you want:
449
577
 
450
578
  ```ts
451
- import { ReportTemplateSchemaValidator } from "@frt/report-core";
579
+ import { ReportTemplateSchemaValidator } from "@frt-platform/report-core";
452
580
 
453
581
  const result = ReportTemplateSchemaValidator.safeParse(raw);
454
582
  if (!result.success) {
@@ -459,7 +587,7 @@ if (!result.success) {
459
587
  ### Migration
460
588
 
461
589
  ```ts
462
- import { migrateLegacySchema } from "@frt/report-core";
590
+ import { migrateLegacySchema } from "@frt-platform/report-core";
463
591
  ```
464
592
 
465
593
  Takes `unknown` legacy input and returns a shape better suited for current schema validation.
@@ -468,16 +596,28 @@ Takes `unknown` legacy input and returns a shape better suited for current schem
468
596
 
469
597
  ```ts
470
598
  import {
471
- buildResponseSchema,
472
599
  validateReportResponse,
600
+ buildResponseSchemaWithConditions,
473
601
  type InferResponse,
474
- } from "@frt/report-core";
602
+ } from "@frt-platform/report-core";
475
603
  ```
476
604
 
477
605
  ### Template diffing
478
606
 
479
607
  ```ts
480
- import { diffTemplates } from "@frt/report-core";
608
+ import { diffTemplates } from "@frt-platform/report-core";
609
+ ```
610
+
611
+ ### JSON Schema export
612
+
613
+ ```ts
614
+ import { exportJSONSchema } from "@frt-platform/report-core";
615
+ ```
616
+
617
+ ### Field registry
618
+
619
+ ```ts
620
+ import { FieldRegistry } from "@frt-platform/report-core";
481
621
  ```
482
622
 
483
623
  ---
@@ -497,6 +637,7 @@ This package is intentionally minimal. You can layer it into:
497
637
  * Migrating old template shapes
498
638
  * Ensuring consistent data before persisting to a DB
499
639
  * Validating report responses before saving them
640
+ * Serving JSON Schema to other services
500
641
 
501
642
  Typical pattern in a full stack app:
502
643
 
@@ -518,13 +659,14 @@ Typical pattern in a full stack app:
518
659
 
519
660
  * Load templates from DB
520
661
  * Use them to render dynamic forms
521
- * Use `buildResponseSchema` to validate and normalize responses
662
+ * Use `validateReportResponse` / `buildResponseSchemaWithConditions` to validate and normalize responses
663
+ * Use `exportJSONSchema` for interoperability with other stacks
522
664
 
523
665
  ---
524
666
 
525
667
  ## Roadmap / Extensions (ideas)
526
668
 
527
- Things you might add on top (in a separate package, e.g. `@frt/report-react`):
669
+ Things you might add on top (in a separate package, e.g. `@frt-platform/report-react`):
528
670
 
529
671
  * React components for:
530
672
 
@@ -536,16 +678,18 @@ Things you might add on top (in a separate package, e.g. `@frt/report-react`):
536
678
  * File uploads
537
679
  * Signature pads
538
680
  * Matrix / grid fields
539
- * Conditional logic:
681
+ * Fully-featured `repeatGroup` UI & runtime
682
+ * Richer conditional logic:
540
683
 
541
684
  * `visibleIf`
542
685
  * `requiredIf`
686
+ * multi-step branching
543
687
  * Multi-language labels & descriptions
544
688
 
545
- The core stays simple: **schema + validation + migration + response validation + diffing**.
689
+ The core stays simple: **schema + validation + conditional logic + migration + diffing + registry + JSON Schema export**.
546
690
 
547
691
  ---
548
692
 
549
693
  ## License
550
694
 
551
- MIT – use it in your projects, commercial or not. Contributions welcome.
695
+ MIT – use it in your projects, commercial or not. Contributions welcome.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frt-platform/report-core",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Core report template engine: schema, validation, normalization, and migration.",
5
5
  "author": "Sebastian Mostert",
6
6
  "license": "MIT",