silentium-validation 0.0.2

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.
@@ -0,0 +1,70 @@
1
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
+ import tsParser from "@typescript-eslint/parser";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import js from "@eslint/js";
6
+ import { FlatCompat } from "@eslint/eslintrc";
7
+ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const compat = new FlatCompat({
12
+ baseDirectory: __dirname,
13
+ recommendedConfig: js.configs.recommended,
14
+ allConfig: js.configs.all,
15
+ });
16
+
17
+ export default [
18
+ {
19
+ ignores: ["**/node_modules", "**/dist"],
20
+ },
21
+ ...compat.extends(
22
+ "eslint:recommended",
23
+ "plugin:@typescript-eslint/eslint-recommended",
24
+ "plugin:@typescript-eslint/recommended",
25
+ ),
26
+ {
27
+ rules: {
28
+ "require-await": ["error"],
29
+ "@typescript-eslint/no-explicit-any": ["off"],
30
+ "prettier.bracketSpacing": ["off"],
31
+ "@typescript-eslint/explicit-member-accessibility": [
32
+ "error",
33
+ {
34
+ accessibility: "explicit",
35
+ },
36
+ ],
37
+ indent: ["error", 2],
38
+ },
39
+ },
40
+ {
41
+ plugins: {
42
+ "@typescript-eslint": typescriptEslint,
43
+ },
44
+
45
+ languageOptions: {
46
+ parser: tsParser,
47
+ globals: {
48
+ window: "readonly",
49
+ },
50
+ },
51
+ },
52
+ eslintPluginPrettierRecommended,
53
+ {
54
+ files: ["**/*.cjs"],
55
+ languageOptions: {
56
+ globals: {
57
+ require: "readonly",
58
+ module: "readonly",
59
+ exports: "readonly",
60
+ __dirname: "readonly",
61
+ __filename: "readonly",
62
+ process: "readonly",
63
+ },
64
+ },
65
+ rules: {
66
+ "no-undef": "off", // Allow CommonJS globals
67
+ "@typescript-eslint/no-require-imports": "off", // Allow require in .cjs files
68
+ },
69
+ },
70
+ ];
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "silentium-validation",
3
+ "version": "0.0.2",
4
+ "description": "Validation library based on silentium",
5
+ "license": "MIT",
6
+ "author": "Silentium Lab",
7
+ "type": "module",
8
+ "main": "dist/silentium.js",
9
+ "module": "dist/silentium.mjs",
10
+ "typings": "dist/silentium.d.ts",
11
+ "keywords": [
12
+ "validation",
13
+ "vanilla",
14
+ "simple",
15
+ "pure"
16
+ ],
17
+ "scripts": {
18
+ "build": "rollup -c",
19
+ "lint": "eslint src",
20
+ "lint-fix": "eslint src --fix",
21
+ "test": "vitest run",
22
+ "push-head": "git push origin HEAD",
23
+ "release": "./beforeRelease.sh && npm run build && git add . && (git commit -m 'build: before release' || echo 'commit failed') && standard-version --no-verify && git push --follow-tags && npm publish",
24
+ "poor-release": "standard-version --no-verify",
25
+ "cz": "git cz"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/silentium-lab/silentium-validation.git"
30
+ },
31
+ "private": false,
32
+ "devDependencies": {
33
+ "@eslint/eslintrc": "^3.1.0",
34
+ "@eslint/js": "^9.12.0",
35
+ "@rollup/plugin-alias": "^6.0.0",
36
+ "@rollup/plugin-terser": "^0.4.4",
37
+ "@typescript-eslint/eslint-plugin": "^8.8.1",
38
+ "@typescript-eslint/parser": "^8.8.1",
39
+ "cz-customizable": "^7.5.1",
40
+ "eslint": "^9.12.0",
41
+ "eslint-config-prettier": "^9.1.0",
42
+ "eslint-plugin-prettier": "^5.2.1",
43
+ "rollup": "^4.24.0",
44
+ "rollup-plugin-dts": "^6.1.1",
45
+ "rollup-plugin-esbuild": "^6.1.1",
46
+ "standard-version": "^9.5.0",
47
+ "vitest": "^4.0.14"
48
+ },
49
+ "dependencies": {
50
+ "silentium": "^0.0.165",
51
+ "silentium-components": "^0.0.93"
52
+ },
53
+ "config": {
54
+ "commitizen": {
55
+ "path": "./node_modules/cz-customizable"
56
+ },
57
+ "cz-customizable": {
58
+ "config": "./commitizen.cjs"
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,66 @@
1
+ import dts from "rollup-plugin-dts";
2
+ import esbuild from "rollup-plugin-esbuild";
3
+ import terser from "@rollup/plugin-terser";
4
+ import alias from "@rollup/plugin-alias";
5
+ import { resolve } from "path";
6
+
7
+ const name = "dist/silentium_validation";
8
+
9
+ const bundle = (config) => {
10
+ const isDts = config.plugins && config.plugins.some((p) => p.name === "dts");
11
+ return {
12
+ ...config,
13
+ input: "src/index.ts",
14
+ external: isDts
15
+ ? () => false
16
+ : (id) => !/^[./]/.test(id) && !id.startsWith("@/"),
17
+ plugins: [
18
+ alias({
19
+ entries: [{ find: /^@\/(.*)$/, replacement: resolve("src/$1.ts") }],
20
+ }),
21
+ ...(config.plugins || []),
22
+ ],
23
+ };
24
+ };
25
+
26
+ export default [
27
+ bundle({
28
+ plugins: [esbuild()],
29
+ output: [
30
+ {
31
+ file: `${name}.cjs`,
32
+ format: "cjs",
33
+ sourcemap: true,
34
+ },
35
+ {
36
+ file: `${name}.js`,
37
+ format: "es",
38
+ sourcemap: true,
39
+ },
40
+ {
41
+ file: `${name}.mjs`,
42
+ format: "es",
43
+ sourcemap: true,
44
+ },
45
+ {
46
+ file: `${name}.min.mjs`,
47
+ format: "es",
48
+ plugins: [terser()],
49
+ sourcemap: true,
50
+ },
51
+ {
52
+ file: `${name}.min.js`,
53
+ format: "iife",
54
+ name: "silentium_validation",
55
+ plugins: [terser()],
56
+ },
57
+ ],
58
+ }),
59
+ bundle({
60
+ plugins: [dts()],
61
+ output: {
62
+ file: `${name}.d.ts`,
63
+ format: "es",
64
+ },
65
+ }),
66
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./models/Validated";
2
+ export * from "./models/ValidationErrors";
3
+ export * from "./models/ValidationErrorsHappened";
4
+ export * from "./models/ValidationErrorsSummary";
5
+ export * from "./models/ValidationErrorsTouched";
6
+ export * from "./models/ValidationItems";
7
+ export * from "./rules";
8
+ export * from "./types";
@@ -0,0 +1,16 @@
1
+ import { Computed, LateShared } from "silentium";
2
+ import { describe, expect, test } from "vitest";
3
+ import { Validated } from "@/models/Validated";
4
+
5
+ describe("Validated.test", () => {
6
+ test("should handle empty form", async () => {
7
+ const $errors = LateShared<any>({
8
+ name: ["too long"],
9
+ });
10
+ const $validated = Computed(Validated, $errors);
11
+ expect(await $validated).toBe(false);
12
+
13
+ $errors.use({});
14
+ expect(await $validated).toBe(true);
15
+ });
16
+ });
@@ -0,0 +1,11 @@
1
+ import { ValidationErrorType } from "@/types";
2
+
3
+ /**
4
+ * Check if there are any errors in the errors object
5
+ * Returns a boolean type
6
+ */
7
+ export function Validated(errors: ValidationErrorType) {
8
+ return !Object.values(errors).some(
9
+ (errorValues: any) => errorValues.length > 0,
10
+ );
11
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ValidationItem } from "@/types";
3
+ import { ValidationErrors } from "@/models/ValidationErrors";
4
+ import { Of } from "silentium";
5
+
6
+ describe("ValidationErrors", () => {
7
+ it("should return a MessageType for validation errors", async () => {
8
+ const mockForm: ValidationItem[] = [
9
+ {
10
+ key: "name",
11
+ value: "John",
12
+ rules: [() => "required"],
13
+ },
14
+ {
15
+ key: "age",
16
+ value: 30,
17
+ rules: [() => "must be number", (age) => age > 18 || "must be adult"],
18
+ },
19
+ {
20
+ key: "balance",
21
+ value: 5,
22
+ rules: [(v) => v > 3 || "low balance"],
23
+ },
24
+ {
25
+ key: "salary",
26
+ value: 100,
27
+ rules: [() => Of("not enough async or reactive")],
28
+ },
29
+ ];
30
+
31
+ const $errors = ValidationErrors(mockForm);
32
+ const errors = await $errors;
33
+
34
+ expect(errors).toStrictEqual({
35
+ age: ["must be number"],
36
+ balance: [],
37
+ name: ["required"],
38
+ salary: ["not enough async or reactive"],
39
+ });
40
+ });
41
+
42
+ it("should handle empty form", async () => {
43
+ const $errors = ValidationErrors([]);
44
+ const errors = await $errors;
45
+ expect(errors).toStrictEqual({});
46
+ });
47
+
48
+ it("should handle items with no rules", async () => {
49
+ const mockForm: ValidationItem[] = [
50
+ {
51
+ key: "name",
52
+ value: "John",
53
+ rules: [],
54
+ },
55
+ ];
56
+ const $errors = ValidationErrors(mockForm);
57
+ const errors = await $errors;
58
+ expect(errors).toStrictEqual({
59
+ name: [],
60
+ });
61
+ });
62
+
63
+ it("should filter out true results from rules", async () => {
64
+ const mockForm: ValidationItem[] = [
65
+ {
66
+ key: "age",
67
+ value: 25,
68
+ rules: [() => true, () => "error"],
69
+ },
70
+ ];
71
+ const $errors = ValidationErrors(mockForm);
72
+ const errors = await $errors;
73
+ expect(errors).toStrictEqual({
74
+ age: ["error"],
75
+ });
76
+ });
77
+
78
+ it("should convert false results to 'Error!'", async () => {
79
+ const mockForm: ValidationItem[] = [
80
+ {
81
+ key: "balance",
82
+ value: 0,
83
+ rules: [() => false],
84
+ },
85
+ ];
86
+ const $errors = ValidationErrors(mockForm);
87
+ const errors = await $errors;
88
+ expect(errors).toStrictEqual({
89
+ balance: ["Error!"],
90
+ });
91
+ });
92
+
93
+ it("should handle undefined results from rules", async () => {
94
+ const mockForm: ValidationItem[] = [
95
+ {
96
+ key: "test",
97
+ value: "value",
98
+ rules: [() => undefined as any],
99
+ },
100
+ ];
101
+ const $errors = ValidationErrors(mockForm);
102
+ const errors = await $errors;
103
+ expect(errors).toStrictEqual({
104
+ test: [undefined],
105
+ });
106
+ });
107
+
108
+ it("should handle duplicate keys, keeping the last", async () => {
109
+ const mockForm: ValidationItem[] = [
110
+ {
111
+ key: "field",
112
+ value: "first",
113
+ rules: [() => "first error"],
114
+ },
115
+ {
116
+ key: "field",
117
+ value: "second",
118
+ rules: [() => "second error"],
119
+ },
120
+ ];
121
+ const $errors = ValidationErrors(mockForm);
122
+ const errors = await $errors;
123
+ expect(errors).toStrictEqual({
124
+ field: ["second error"],
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,51 @@
1
+ import {
2
+ ActualMessage,
3
+ All,
4
+ Applied,
5
+ DestroyContainer,
6
+ MaybeMessage,
7
+ Message,
8
+ MessageType,
9
+ } from "silentium";
10
+ import { ValidationErrorType, ValidationItem } from "@/types";
11
+
12
+ /**
13
+ * Accepts a set of items that need to be validated
14
+ * and when rules produce values, returns the overall set
15
+ * of errors for the given configuration
16
+ */
17
+ export function ValidationErrors(
18
+ form: MaybeMessage<ValidationItem[]>,
19
+ ): MessageType<ValidationErrorType> {
20
+ const $form = ActualMessage(form);
21
+ return Message((resolve, reject) => {
22
+ const formDc = DestroyContainer();
23
+ $form.then((form) => {
24
+ formDc.destroy();
25
+ const entries = form.map((i) => {
26
+ return All(
27
+ i.key,
28
+ Applied(
29
+ All(
30
+ ...i.rules.map((rule) => {
31
+ return formDc.add(rule(i.value));
32
+ }),
33
+ ),
34
+ (items) => items.filter(ExcludeTrue).map(ErrorFormat),
35
+ ) as MessageType<string[]>,
36
+ );
37
+ });
38
+ Applied(All(...entries), (e: any) => Object.fromEntries(e))
39
+ .catch(reject)
40
+ .then(resolve);
41
+ });
42
+ });
43
+ }
44
+
45
+ function ErrorFormat(v: boolean | string) {
46
+ return v === false ? "Error!" : v;
47
+ }
48
+
49
+ function ExcludeTrue(v: boolean | string) {
50
+ return v !== true;
51
+ }
@@ -0,0 +1,16 @@
1
+ import { Computed } from "silentium";
2
+ import { describe, expect, test } from "vitest";
3
+ import { ValidationErrorsHappened } from "@/models/ValidationErrorsHappened";
4
+
5
+ describe("ValidationErrorsHappened.test", () => {
6
+ test("regular", async () => {
7
+ const errors = {
8
+ name: ["required"],
9
+ age: [],
10
+ };
11
+ const $happened = Computed(ValidationErrorsHappened, errors);
12
+ expect(await $happened).toStrictEqual({
13
+ name: ["required"],
14
+ });
15
+ });
16
+ });
@@ -0,0 +1,10 @@
1
+ import { ValidationErrorType } from "@/types";
2
+
3
+ /**
4
+ * Show only the errors that exist, fields without errors are not shown
5
+ */
6
+ export function ValidationErrorsHappened(base: ValidationErrorType) {
7
+ return Object.fromEntries(
8
+ Object.entries(base).filter((entry) => entry[1].length > 0),
9
+ );
10
+ }
@@ -0,0 +1,21 @@
1
+ import { Computed, LateShared } from "silentium";
2
+ import { describe, expect, test } from "vitest";
3
+ import { ValidationErrorType } from "@/types";
4
+ import { ValidationErrorsSummary } from "@/models/ValidationErrorsSummary";
5
+
6
+ describe("ValidationErrorsSummary.test", () => {
7
+ test("regular", async () => {
8
+ const $errors = LateShared<ValidationErrorType>({});
9
+ const $summary = Computed(ValidationErrorsSummary, $errors);
10
+ expect(await $summary).toStrictEqual([]);
11
+
12
+ $errors.use({
13
+ name: ["Must be more 20 chars"],
14
+ age: ["Must be adult"],
15
+ });
16
+ expect(await $summary).toStrictEqual([
17
+ "Must be more 20 chars",
18
+ "Must be adult",
19
+ ]);
20
+ });
21
+ });
@@ -0,0 +1,8 @@
1
+ import { ValidationErrorType } from "@/types";
2
+
3
+ /**
4
+ * Overall array of all errors
5
+ */
6
+ export function ValidationErrorsSummary(errors: ValidationErrorType) {
7
+ return Object.values(errors).flat();
8
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { ValidationErrorsTouched } from "@/models/ValidationErrorsTouched";
3
+ import { Computed, LateShared } from "silentium";
4
+ import { ValidationErrors } from "@/models/ValidationErrors";
5
+ import { ValidationItems } from "@/models/ValidationItems";
6
+ import { Integer, Required } from "@/rules";
7
+
8
+ describe("ValidationErrorsTouched.test", () => {
9
+ test("regular", async () => {
10
+ expect(true).toBe(true);
11
+
12
+ const $form = LateShared<any>({
13
+ name: "",
14
+ age: 0,
15
+ });
16
+ const rules = {
17
+ name: [Required],
18
+ age: [Integer],
19
+ };
20
+
21
+ const $errors = ValidationErrors(Computed(ValidationItems, $form, rules));
22
+ const $errorsTouched = ValidationErrorsTouched($form, $errors);
23
+ expect(await $errorsTouched).toStrictEqual({});
24
+
25
+ $form.use({
26
+ age: "not int" as unknown as number,
27
+ });
28
+ expect(await $errorsTouched).toStrictEqual({
29
+ age: ["Must be integer"],
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,21 @@
1
+ import { All, Applied, Chainable, MessageType } from "silentium";
2
+ import { ValidationErrorType } from "@/types";
3
+ import { Dirty, MergeAccumulation } from "silentium-components";
4
+
5
+ /**
6
+ * Validation errors are only those that correspond to changed form fields
7
+ */
8
+ export function ValidationErrorsTouched(
9
+ $form: MessageType<Record<string, unknown>>,
10
+ $errors: MessageType<ValidationErrorType>,
11
+ ) {
12
+ const dirtyForm = Dirty($form);
13
+ Chainable(dirtyForm).chain($form);
14
+ const touchedForm = MergeAccumulation(dirtyForm);
15
+ const errorsTouched = All(Applied(touchedForm, Object.keys), $errors);
16
+ return Applied(errorsTouched, ([touched, errors]) => {
17
+ return Object.fromEntries(
18
+ Object.entries(errors).filter((entry) => touched.includes(entry[0])),
19
+ );
20
+ });
21
+ }
@@ -0,0 +1,94 @@
1
+ import { ValidationErrors } from "@/models/ValidationErrors";
2
+ import { ValidationItems } from "@/models/ValidationItems";
3
+ import { Integer, Required } from "@/rules";
4
+ import { Computed, LateShared } from "silentium";
5
+ import { describe, expect, test } from "vitest";
6
+
7
+ describe("ValidationItems", () => {
8
+ test("should return a MessageRx for form fields", () => {
9
+ const form = { name: "John", age: 30 };
10
+ const rules = {
11
+ name: [Required],
12
+ age: [Integer],
13
+ };
14
+
15
+ const result = ValidationItems(form, rules as any);
16
+
17
+ expect(result).toStrictEqual([
18
+ {
19
+ key: "name",
20
+ rules: [Required],
21
+ value: "John",
22
+ },
23
+ {
24
+ key: "age",
25
+ rules: [Integer],
26
+ value: 30,
27
+ },
28
+ ]);
29
+ });
30
+
31
+ test("reactive variant", async () => {
32
+ const form = LateShared({ name: "John", age: 30 });
33
+ const rules = {
34
+ name: [Required],
35
+ age: [Integer],
36
+ };
37
+
38
+ const $result = Computed(ValidationItems, form, rules);
39
+
40
+ expect(await $result).toStrictEqual([
41
+ {
42
+ key: "name",
43
+ rules: [Required],
44
+ value: "John",
45
+ },
46
+ {
47
+ key: "age",
48
+ rules: [Integer],
49
+ value: 30,
50
+ },
51
+ ]);
52
+
53
+ form.use({
54
+ name: "Happy",
55
+ age: 5,
56
+ });
57
+
58
+ expect(await $result).toStrictEqual([
59
+ {
60
+ key: "name",
61
+ rules: [Required],
62
+ value: "Happy",
63
+ },
64
+ {
65
+ key: "age",
66
+ rules: [Integer],
67
+ value: 5,
68
+ },
69
+ ]);
70
+ });
71
+
72
+ test("should handle empty form", () => {
73
+ const form = {};
74
+ const rules = {};
75
+
76
+ const result = ValidationItems(form, rules);
77
+
78
+ expect(result).toStrictEqual([]);
79
+ });
80
+
81
+ test("Integration with errors component", async () => {
82
+ const form = { name: "John", age: 11.1, norule: "Norule" };
83
+ const rules = {
84
+ name: [Required],
85
+ age: [Integer],
86
+ };
87
+
88
+ const $errors = ValidationErrors(ValidationItems(form, rules as any));
89
+ expect(await $errors).toStrictEqual({
90
+ age: ["Must be integer"],
91
+ name: [],
92
+ });
93
+ });
94
+ });
@@ -0,0 +1,24 @@
1
+ import { ConstructorType } from "silentium";
2
+ import { ValidationRule } from "@/types";
3
+
4
+ export type FormType = Record<string, unknown>;
5
+ export type FormRulesType = Record<
6
+ string,
7
+ ConstructorType<any, ValidationRule>[]
8
+ >;
9
+
10
+ /**
11
+ * Get a set of all validation rules
12
+ * for each form field
13
+ */
14
+ export function ValidationItems(form: FormType, rules: FormRulesType) {
15
+ return Object.keys(form)
16
+ .map((key) => {
17
+ return {
18
+ key,
19
+ value: form[key],
20
+ rules: rules[key],
21
+ };
22
+ })
23
+ .filter((item) => !!item.rules);
24
+ }
package/src/rules.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Validation rule, what requires truthy value
3
+ */
4
+ export const Required = (v: unknown) => !!v || "Field required";
5
+
6
+ /**
7
+ * Validation rule what requires integer value
8
+ */
9
+ export const Integer = (v: unknown) => Number.isInteger(v) || "Must be integer";
package/src/types.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { ConstructorType, MaybeMessage } from "silentium";
2
+
3
+ export type ValidationErrorType = Record<string, string[]>;
4
+
5
+ export type ValidationRule = MaybeMessage<string | boolean>;
6
+
7
+ export interface ValidationItem {
8
+ value: unknown;
9
+ key: string;
10
+ rules: ConstructorType<any, ValidationRule>[];
11
+ }