@streamline-pulse/formkrafter-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/brick/brick.d.ts +63 -0
- package/dist/brick/types.d.ts +25 -0
- package/dist/brick/utils.d.ts +20 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +1006 -0
- package/dist/index.js.map +25 -0
- package/dist/ops/history.d.ts +12 -0
- package/dist/ops/ops.d.ts +19 -0
- package/dist/ops/pointer.d.ts +4 -0
- package/dist/rules/effect.d.ts +12 -0
- package/dist/rules/rule.d.ts +9 -0
- package/dist/services/data_source_service.d.ts +16 -0
- package/dist/services/file_upload_service.d.ts +18 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/js_runner_service.d.ts +16 -0
- package/dist/services/sandbox_interpreter.d.ts +1 -0
- package/dist/services/unsafe_js_runner_service.d.ts +9 -0
- package/dist/utils/brick-mold.d.ts +10 -0
- package/dist/utils/brick-spec.d.ts +38 -0
- package/dist/utils/brick-styles.d.ts +1 -0
- package/dist/utils/brick-type.d.ts +1 -0
- package/dist/utils/common-brick-props.d.ts +14 -0
- package/dist/utils/drop-type.d.ts +10 -0
- package/dist/utils/fast-json-patch.d.ts +3 -0
- package/dist/utils/localized-text.d.ts +4 -0
- package/dist/validators/default-messages.d.ts +2 -0
- package/dist/validators/json-schema-type.d.ts +3 -0
- package/dist/validators/validation.d.ts +8 -0
- package/dist/validators/validator.d.ts +9 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Streamline Pulse
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @streamline-pulse/formkrafter-core
|
|
2
|
+
|
|
3
|
+
The framework-agnostic heart of FormKrafter: form **specs**, immutable **mutation ops**, **validation**, a sandboxed **rules engine**, and injectable **services**. No DOM, no framework — it runs in browsers, Node, Bun, and workers alike.
|
|
4
|
+
|
|
5
|
+
## The spec
|
|
6
|
+
|
|
7
|
+
A form is a tree of `BrickSpec` nodes. Each brick has a `type` (`input`, `panel`, `collection`, `output`, `action`), an `id` (which registered brick renders it), `configs` (with a unique `uid` and a data `key`), plus optional `validations`, `rules`, `styles`, and `children`.
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TD
|
|
11
|
+
F["panel · column<br/>key: form"] --> N["input · text<br/>key: name · required"]
|
|
12
|
+
F --> A["input · address<br/>key: address"]
|
|
13
|
+
F --> G["collection · data-grid<br/>key: contacts"]
|
|
14
|
+
G --> E["input · email<br/>key: email (per row)"]
|
|
15
|
+
G --> R["input · text<br/>key: role (per row)"]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Form data is a flat `Record<key, value>` — except inside a `collection`, where each row is its own scoped record: `{ name: 'Ada', contacts: [{ email, role }, …] }`.
|
|
19
|
+
|
|
20
|
+
## Mutating specs: ops, patches, undo
|
|
21
|
+
|
|
22
|
+
Never mutate a spec in place. Every operation returns a new spec plus RFC 6902 patches in both directions:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { addBrick, moveBrick, removeBrick, updateBrickConfigs, SpecHistory } from '@streamline-pulse/formkrafter-core'
|
|
26
|
+
|
|
27
|
+
const update = moveBrick(spec, '0.0', '0.2.0') // dot-paths, pre-removal coordinates
|
|
28
|
+
// update = { spec, patches, inverse } // RFC 6902 both ways
|
|
29
|
+
|
|
30
|
+
const history = new SpecHistory()
|
|
31
|
+
history.record(update)
|
|
32
|
+
spec = history.undo(update.spec) // and .redo(), .canUndo, .canRedo
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
flowchart LR
|
|
37
|
+
S0[spec] -->|"moveBrick(spec, from, to)"| U["{ spec', patches, inverse }"]
|
|
38
|
+
U -->|record| H[(SpecHistory)]
|
|
39
|
+
H -->|undo → apply inverse| S0
|
|
40
|
+
H -->|redo → apply patches| S1[spec']
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Available ops: `addBrick`, `removeBrick`, `moveBrick`, `duplicateBrick` (fresh uids), `updateBrickConfigs`, `updateBrickStyles`, `updateBrickValidations`, `updateBrickRules`. Addressing helpers: `getBrickAt(spec, path)`, `pointerOfUid(spec, uid)`, `iterateBricks(spec)`.
|
|
44
|
+
|
|
45
|
+
Patches are plain RFC 6902 — persist them, sync them, audit them.
|
|
46
|
+
|
|
47
|
+
## Validation
|
|
48
|
+
|
|
49
|
+
Validations live on each brick and compile to a JSON Schema (Ajv + ajv-errors + ajv-formats):
|
|
50
|
+
|
|
51
|
+
| Validator | Applies to | Parameter |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `required` | all | — |
|
|
54
|
+
| `minLength` / `maxLength` / `pattern` / `email` / `url` | string | number / regex |
|
|
55
|
+
| `min` / `max` | number | number |
|
|
56
|
+
| `minItems` / `maxItems` | array | number |
|
|
57
|
+
| `custom` | all | JavaScript (sandboxed) |
|
|
58
|
+
|
|
59
|
+
**Backend entry point** — same verdict as the frontend, including collection rows and `''`-as-missing semantics:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { validateFormData } from '@streamline-pulse/formkrafter-core'
|
|
63
|
+
|
|
64
|
+
const { valid, errors } = validateFormData(spec, payload, 'fr')
|
|
65
|
+
// errors: { name: 'Ce champ est obligatoire', 'contacts[0].email': 'Email invalide' }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Messages resolve in cascade: author's message (optionally localized `{ en, fr }`) → built-in localized defaults (`en`/`fr` shipped, extend with `registerValidationMessages('de', {...})`).
|
|
69
|
+
|
|
70
|
+
## Rules engine — no `eval`
|
|
71
|
+
|
|
72
|
+
Rules drive dynamic behavior (`hidden`, `disabled`, `required`, computed `value`). Two modes:
|
|
73
|
+
|
|
74
|
+
- **`jsonLogic`** (default): serializable, declarative — `{ "!": { "in": [{ "var": "country" }, ["FR","DE"]] } }`
|
|
75
|
+
- **`javaScript`**: the escape hatch — executed by a built-in **AST interpreter** (Acorn-based), not `eval`:
|
|
76
|
+
- CSP-safe (no `unsafe-eval` needed anywhere)
|
|
77
|
+
- sandboxed: only `dataMap`/`value` plus whitelisted builtins (`Math`, `JSON`, `String`, …) — no `fetch`, no `globalThis`, no `constructor`/`__proto__` escapes
|
|
78
|
+
- supports expressions, ternaries, `const`/`if`/`return`, optional chaining, arrow functions (`.filter(x => …)`)
|
|
79
|
+
- unsupported syntax is rejected, runaway code is cut by an execution budget
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { runSandboxed, UnsafeEvalJsRunnerService, services } from '@streamline-pulse/formkrafter-core'
|
|
83
|
+
|
|
84
|
+
runSandboxed('return items.filter((i) => i > 2)', { items: [1, 2, 3] }) // [3]
|
|
85
|
+
|
|
86
|
+
// trusted environments can opt back into full JS:
|
|
87
|
+
services.jsRunnerService = new UnsafeEvalJsRunnerService()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Services (dependency injection points)
|
|
91
|
+
|
|
92
|
+
| Service | Default | Replace when… |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `services.jsRunnerService` | AST sandbox | you need full JS in a trusted context |
|
|
95
|
+
| `services.dataSourceService` | `fetch` + per-URL/headers cache | auth, base URL, retry policy:<br/>`new FetchDataSourceService({ credentials: 'include', headers: {...} })` |
|
|
96
|
+
| `services.fileUploadService` | base64 data-URL | real uploads (S3, API): return `{ name, type, size, url }` |
|
|
97
|
+
|
|
98
|
+
## Shared singleton state
|
|
99
|
+
|
|
100
|
+
`services` is a **page-wide singleton**, stored on `globalThis` under `Symbol.for("formkrafter.core.services")`. Bundlers routinely end up with several copies of this module in one app (your bundle imports core directly, and the Web Components bundle embeds its own copy) — without the shared store, replacing a service from your app would mutate a copy the components never read.
|
|
101
|
+
|
|
102
|
+
What this means in practice:
|
|
103
|
+
|
|
104
|
+
- `services.dataSourceService = …` (or any other override) takes effect **everywhere**, whichever bundle copy executes it — configure once at app startup.
|
|
105
|
+
- Every FormKrafter instance on the page shares the same services, brick registry, and chrome translations. That is the intended behavior for host-level configuration; per-form differences belong in the spec, not in services.
|
|
106
|
+
- The same pattern backs the brick registry and the i18n store in `formkrafter-wc`.
|
|
107
|
+
|
|
108
|
+
## Localized content
|
|
109
|
+
|
|
110
|
+
Any text in a spec may be a string or a per-locale object; resolution helpers are exported:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
{ "label": { "en": "Full name", "fr": "Nom complet" } }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`resolveLocalizedText(value, locale)`, `resolveLocalizedRecord(configs, locale)`, `isLocalizedObject(value)` — validation messages resolve through the same mechanism.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Validator } from "../validators/validator";
|
|
2
|
+
import type { Validation } from "../validators/validation";
|
|
3
|
+
import type { JSONSchemaType } from "../validators/json-schema-type";
|
|
4
|
+
import { type Utils } from "./utils";
|
|
5
|
+
import type { Rule } from "../rules/rule";
|
|
6
|
+
import type { BrickSpec } from "../utils/brick-spec";
|
|
7
|
+
import type { BrickType } from "../utils/brick-type";
|
|
8
|
+
import type { BrickBaseConfigs, CommonBrickProps } from "../utils/common-brick-props";
|
|
9
|
+
import type { BrickMoldDropType } from "../utils/drop-type";
|
|
10
|
+
export interface BrickProps<DataType, Configs extends BrickBaseConfigs, Styles> extends CommonBrickProps<Configs, Styles> {
|
|
11
|
+
onConfigsChange?: (configs: BrickBaseConfigs & Record<string, unknown>, uid?: string) => void;
|
|
12
|
+
onStylesChange?: (styles: Record<string, unknown>, uid?: string) => void;
|
|
13
|
+
data?: DataType;
|
|
14
|
+
dataMap?: Record<string, unknown>;
|
|
15
|
+
onDataChange?: (value?: DataType) => void;
|
|
16
|
+
onMove?: (from: string, to: string) => void;
|
|
17
|
+
onDuplicate?: (path: string) => void;
|
|
18
|
+
onDelete?: (path: string) => void;
|
|
19
|
+
onAddBrick?: (brickMold: BrickMoldDropType, path: string) => void;
|
|
20
|
+
path: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
locale?: string;
|
|
24
|
+
children?: unknown;
|
|
25
|
+
brickSpec?: BrickSpec;
|
|
26
|
+
editable?: boolean;
|
|
27
|
+
validations?: Validation[];
|
|
28
|
+
onValidationsChange?: (validations: Validation[], uid?: string) => void;
|
|
29
|
+
rules?: Rule[];
|
|
30
|
+
onRulesChange?: (rules: Rule[], uid?: string) => void;
|
|
31
|
+
utils: Utils;
|
|
32
|
+
}
|
|
33
|
+
export declare abstract class Brick<DataType, Configs extends BrickBaseConfigs, Styles extends Record<string, unknown>> {
|
|
34
|
+
readonly type: BrickType;
|
|
35
|
+
readonly dataType: JSONSchemaType;
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly configsForm?: BrickSpec;
|
|
39
|
+
readonly category?: string;
|
|
40
|
+
readonly styles?: Styles;
|
|
41
|
+
readonly unWrapData: boolean;
|
|
42
|
+
readonly isPrivate: boolean;
|
|
43
|
+
readonly validators: Validator[];
|
|
44
|
+
readonly defaultConfigs?: Record<string, unknown>;
|
|
45
|
+
constructor(params: {
|
|
46
|
+
type: BrickType;
|
|
47
|
+
dataType: JSONSchemaType;
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
configsForm?: BrickSpec;
|
|
51
|
+
category?: string;
|
|
52
|
+
styles?: Styles;
|
|
53
|
+
unWrapData?: boolean;
|
|
54
|
+
isPrivate?: boolean;
|
|
55
|
+
validators?: Validator[];
|
|
56
|
+
defaultConfigs?: Record<string, unknown>;
|
|
57
|
+
});
|
|
58
|
+
abstract render(props: BrickProps<DataType, Configs, Styles>): unknown;
|
|
59
|
+
abstract renderBuilder(props: BrickProps<DataType, Configs, Styles>): unknown;
|
|
60
|
+
abstract getComponent(props: BrickProps<DataType, Configs, Styles>): unknown;
|
|
61
|
+
abstract getComponentWithActions(props: BrickProps<DataType, Configs, Styles>): unknown;
|
|
62
|
+
getBrickEmptySpec(): BrickSpec;
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BrickBaseConfigs, CommonBrickProps } from "../utils/common-brick-props";
|
|
2
|
+
import type { BrickSpec } from "../utils/brick-spec";
|
|
3
|
+
import type { CollectionUtils, InputUtils, PanelUtils } from "./utils";
|
|
4
|
+
interface BrickProps<Configs extends BrickBaseConfigs, Styles> extends CommonBrickProps<Configs, Styles> {
|
|
5
|
+
path: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
editable?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface PanelBrickProps<Configs extends BrickBaseConfigs, Styles> extends BrickProps<Configs, Styles> {
|
|
10
|
+
children?: unknown;
|
|
11
|
+
utils: PanelUtils;
|
|
12
|
+
}
|
|
13
|
+
export interface InputBrickProps<DataType, Configs extends BrickBaseConfigs, Styles> extends BrickProps<Configs, Styles> {
|
|
14
|
+
data?: DataType;
|
|
15
|
+
onDataChange?: (value?: DataType) => void;
|
|
16
|
+
utils: InputUtils;
|
|
17
|
+
}
|
|
18
|
+
export interface CollectionBrickProps<DataType, Configs extends BrickBaseConfigs, Styles> extends BrickProps<Configs, Styles> {
|
|
19
|
+
brickSpec?: BrickSpec;
|
|
20
|
+
children?: unknown;
|
|
21
|
+
data?: DataType;
|
|
22
|
+
onDataChange?: (value?: DataType) => void;
|
|
23
|
+
utils: CollectionUtils;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Rule } from "../rules/rule";
|
|
2
|
+
export type CommonUtils = object;
|
|
3
|
+
export type InputUtils = CommonUtils;
|
|
4
|
+
export type PanelUtils = CommonUtils;
|
|
5
|
+
export type CollectionUtils = CommonUtils;
|
|
6
|
+
export type ActionUtils = {
|
|
7
|
+
validateForm: () => {
|
|
8
|
+
isValid: boolean;
|
|
9
|
+
};
|
|
10
|
+
} & CommonUtils;
|
|
11
|
+
export type Utils = InputUtils & PanelUtils & CollectionUtils & ActionUtils;
|
|
12
|
+
export declare const evalBrickCode: (code: string, data?: Record<string, unknown>) => unknown;
|
|
13
|
+
type Properties = {
|
|
14
|
+
value?: string;
|
|
15
|
+
required?: boolean;
|
|
16
|
+
hidden?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare const getAffectedProperties: (rules?: Rule[], dataMap?: Record<string, unknown>) => Properties;
|
|
20
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export { BrickType } from "./utils/brick-type";
|
|
2
|
+
export { BrickMold } from "./utils/brick-mold";
|
|
3
|
+
export { DropType, BrickMoldDropType, BrickSpecDropType } from "./utils/drop-type";
|
|
4
|
+
export { BrickStyles } from "./utils/brick-styles";
|
|
5
|
+
export { BrickBaseConfigs, CommonBrickProps, WithBrickBaseConfigs } from "./utils/common-brick-props";
|
|
6
|
+
export { BrickSpec, buildValidationSchema, iterateBricks, iterateSchemaBricks } from "./utils/brick-spec";
|
|
7
|
+
export { PanelBrickProps, InputBrickProps, CollectionBrickProps } from "./brick/types";
|
|
8
|
+
export { Brick, BrickProps } from "./brick/brick";
|
|
9
|
+
export { CommonUtils, PanelUtils, InputUtils, CollectionUtils, ActionUtils, Utils, getAffectedProperties, evalBrickCode } from "./brick/utils";
|
|
10
|
+
export { Rule } from "./rules/rule";
|
|
11
|
+
export { Effect } from "./rules/effect";
|
|
12
|
+
export { Validator, ValidationResult, validateBrickSpecData, validateBrickSpecDataDetailed, validateFormData } from "./validators/validator";
|
|
13
|
+
export { defaultValidationMessage, registerValidationMessages } from "./validators/default-messages";
|
|
14
|
+
export { Validation } from "./validators/validation";
|
|
15
|
+
export { JSONSchemaType } from "./validators/json-schema-type";
|
|
16
|
+
export { services } from "./services";
|
|
17
|
+
export { JsRunnerService, SandboxJsRunnerService, JsRunnerServiceImplementation } from "./services/js_runner_service";
|
|
18
|
+
export { UnsafeEvalJsRunnerService } from "./services/unsafe_js_runner_service";
|
|
19
|
+
export { runSandboxed } from "./services/sandbox_interpreter";
|
|
20
|
+
export { DataSourceService, FetchDataSourceService } from "./services/data_source_service";
|
|
21
|
+
export { FileLike, UploadedFile, FileUploadService, Base64FileUploadService } from "./services/file_upload_service";
|
|
22
|
+
export { LocalizedText, isLocalizedObject, resolveLocalizedText, resolveLocalizedRecord } from "./utils/localized-text";
|
|
23
|
+
export { pointerFromPath, pointerOfUid, getBrickAt } from "./ops/pointer";
|
|
24
|
+
export { SpecUpdate, addBrick, removeBrick, moveBrick, duplicateBrick, updateBrickConfigs, updateBrickStyles, updateBrickValidations, updateBrickRules } from "./ops/ops";
|
|
25
|
+
export { SpecHistory } from "./ops/history";
|
|
26
|
+
export type { Operation } from "fast-json-patch";
|