From f2a43bc22000bf350e165b810aaa882985998c6a Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 17 Oct 2024 17:33:23 +0900 Subject: [PATCH 01/37] Add FabrixFormConstraint type --- packages/graphql-config/src/directive.graphql | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/graphql-config/src/directive.graphql b/packages/graphql-config/src/directive.graphql index 142e6cf4..64b64ddb 100644 --- a/packages/graphql-config/src/directive.graphql +++ b/packages/graphql-config/src/directive.graphql @@ -117,6 +117,28 @@ input FabrixFormField { config: FabrixFormFieldConfig! } +input FabrixFormConstraint { + """ + String + """ + minLength: Int + maxLength: Int + startsWith: String + endsWith: String + contains: String + notContains: String + pattern: String + format: String + + """ + Int/Float + """ + min: Int + max: Int + exclusiveMin: Float + exclusiveMax: Float +} + """ Fabrix directive for form """ From b7a8c6e263b96f250633e9bf0752511e7d4beab1 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 13:13:19 +0900 Subject: [PATCH 02/37] Split GraphQL definition files --- packages/graphql-config/src/config.ts | 10 +- packages/graphql-config/src/directive.graphql | 145 ------------------ .../graphql-config/src/schema/common.graphql | 23 +++ .../src/schema/constraints.graphql | 64 ++++++++ .../graphql-config/src/schema/form.graphql | 55 +++++++ .../graphql-config/src/schema/view.graphql | 43 ++++++ 6 files changed, 193 insertions(+), 147 deletions(-) delete mode 100644 packages/graphql-config/src/directive.graphql create mode 100644 packages/graphql-config/src/schema/common.graphql create mode 100644 packages/graphql-config/src/schema/constraints.graphql create mode 100644 packages/graphql-config/src/schema/form.graphql create mode 100644 packages/graphql-config/src/schema/view.graphql diff --git a/packages/graphql-config/src/config.ts b/packages/graphql-config/src/config.ts index 2ebf32e9..abe2bae8 100644 --- a/packages/graphql-config/src/config.ts +++ b/packages/graphql-config/src/config.ts @@ -1,12 +1,18 @@ import * as os from "node:os"; import * as path from "node:path"; import * as fs from "node:fs"; -import Document from "./directive.graphql"; +import CommonSchema from "./schema/common.graphql"; +import ViewDirectiveSchema from "./schema/view.graphql"; +import FormDirectiveSchema from "./schema/form.graphql"; +import ConstraintSchema from "./schema/constraint.graphql"; export const generateConfig = () => { const tempGQLFile = path.join(os.tmpdir(), "fabrix-graphql-config.graphql"); - fs.writeFileSync(tempGQLFile, Document); + fs.writeFileSync( + tempGQLFile, + CommonSchema + ViewDirectiveSchema + FormDirectiveSchema + ConstraintSchema, + ); return { directiveSchema: tempGQLFile, diff --git a/packages/graphql-config/src/directive.graphql b/packages/graphql-config/src/directive.graphql deleted file mode 100644 index 64b64ddb..00000000 --- a/packages/graphql-config/src/directive.graphql +++ /dev/null @@ -1,145 +0,0 @@ -input FabrixComponentType { - """ - Component name to render the field - """ - name: String! - - """ - Component props - """ - props: [FabrixComponentProps] -} - -input FabrixComponentProps { - """ - The property name for the component - """ - name: String! - - """ - The value for the property - """ - value: String! -} - -input FabrixViewConfig { - """ - The number of grid columns the field (max: 12) - """ - gridCol: Int - - """ - The 0-based index of the field - """ - index: Int - - """ - The label of the field on UI - """ - label: String - - """ - Hide the field on UI - """ - hidden: Boolean - - """ - The component to render for the field - """ - componentType: FabrixComponentType -} - -input FabrixView { - """ - The field name in the schema - """ - field: String! - - """ - The configuration for the field - """ - config: FabrixViewConfig! -} - -""" -Fabrix directive for fields -""" -directive @fabrixView(input: [FabrixView!]) on FIELD - -input FabrixFormFieldConfig { - """ - The number of grid columns the field (max: 12) - """ - gridCol: Int - - """ - The 0-based index of the field - """ - index: Int - - """ - The label of the field on UI - """ - label: String - - """ - Placeholder text for the field - """ - placeholder: String - - """ - Hide the field on UI - """ - hidden: Boolean - - """ - The default value for the field - - The value will automatically be converted to the type of the field - """ - defaultValue: String - - """ - The component to render for the field - """ - componentType: FabrixComponentType -} - -input FabrixFormField { - """ - The field name in the schema - """ - field: String! - - """ - The configuration for the field - """ - config: FabrixFormFieldConfig! -} - -input FabrixFormConstraint { - """ - String - """ - minLength: Int - maxLength: Int - startsWith: String - endsWith: String - contains: String - notContains: String - pattern: String - format: String - - """ - Int/Float - """ - min: Int - max: Int - exclusiveMin: Float - exclusiveMax: Float -} - -""" -Fabrix directive for form -""" -directive @fabrixForm(input: [FabrixFormField!]) on FIELD diff --git a/packages/graphql-config/src/schema/common.graphql b/packages/graphql-config/src/schema/common.graphql new file mode 100644 index 00000000..f799f2b4 --- /dev/null +++ b/packages/graphql-config/src/schema/common.graphql @@ -0,0 +1,23 @@ +input FabrixComponentType { + """ + Component name to render the field + """ + name: String! + + """ + Component props + """ + props: [FabrixComponentProps] +} + +input FabrixComponentProps { + """ + The property name for the component + """ + name: String! + + """ + The value for the property + """ + value: String! +} diff --git a/packages/graphql-config/src/schema/constraints.graphql b/packages/graphql-config/src/schema/constraints.graphql new file mode 100644 index 00000000..303880c1 --- /dev/null +++ b/packages/graphql-config/src/schema/constraints.graphql @@ -0,0 +1,64 @@ +""" +Validation constraints for the form field +""" +input FabrixFormConstraint { + """ + Minimum length of the string + """ + minLength: Int + + """ + Maximum length of the string + """ + maxLength: Int + + """ + Validates if the string value starts with the given value + """ + startsWith: String + + """ + Validates if the string value ends with the given value + """ + endsWith: String + + """ + Validates if the string value contains the given value + """ + contains: String + + """ + Validates if the string value does not contain the given value + """ + notContains: String + + """ + Validates if the string value matches the given pattern + """ + pattern: String + + """ + Validates if the string value matches the custom format given + """ + format: String + + """ + Validates if the number value is greater than the given value + """ + min: Int + + """ + Validates if the number value is less than the given value + """ + max: Int + + """ + Validates if the number value is greater than or equal to the given value + """ + exclusiveMin: Float + + """ + Validates if the number value is less than or equal to the given value + """ + exclusiveMax: Float +} diff --git a/packages/graphql-config/src/schema/form.graphql b/packages/graphql-config/src/schema/form.graphql new file mode 100644 index 00000000..eb0eed01 --- /dev/null +++ b/packages/graphql-config/src/schema/form.graphql @@ -0,0 +1,55 @@ +input FabrixFormFieldConfig { + """ + The number of grid columns the field (max: 12) + """ + gridCol: Int + + """ + The 0-based index of the field + """ + index: Int + + """ + The label of the field on UI + """ + label: String + + """ + Placeholder text for the field + """ + placeholder: String + + """ + Hide the field on UI + """ + hidden: Boolean + + """ + The default value for the field + + The value will automatically be converted to the type of the field + """ + defaultValue: String + + """ + The component to render for the field + """ + componentType: FabrixComponentType +} + +input FabrixFormField { + """ + The field name in the schema + """ + field: String! + + """ + The configuration for the field + """ + config: FabrixFormFieldConfig! +} + +""" +Fabrix directive for form +""" +directive @fabrixForm(input: [FabrixFormField!]) on FIELD diff --git a/packages/graphql-config/src/schema/view.graphql b/packages/graphql-config/src/schema/view.graphql new file mode 100644 index 00000000..da37795a --- /dev/null +++ b/packages/graphql-config/src/schema/view.graphql @@ -0,0 +1,43 @@ +input FabrixViewConfig { + """ + The number of grid columns the field (max: 12) + """ + gridCol: Int + + """ + The 0-based index of the field + """ + index: Int + + """ + The label of the field on UI + """ + label: String + + """ + Hide the field on UI + """ + hidden: Boolean + + """ + The component to render for the field + """ + componentType: FabrixComponentType +} + +input FabrixView { + """ + The field name in the schema + """ + field: String! + + """ + The configuration for the field + """ + config: FabrixViewConfig! +} + +""" +Fabrix directive for fields +""" +directive @fabrixView(input: [FabrixView!]) on FIELD From 885495386a1aa6776fdf11988c7691726fb29a63 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:40:44 +0900 Subject: [PATCH 03/37] Add test for graphql-config --- packages/graphql-config/package.json | 10 +- packages/graphql-config/src/config.test.ts | 25 ++++ packages/graphql-config/src/config.ts | 12 +- packages/graphql-config/src/graphql.d.ts | 6 +- packages/graphql-config/src/schema.ts | 19 ++- ...constraints.graphql => constraint.graphql} | 0 packages/graphql-config/tsconfig.json | 3 +- packages/graphql-config/vitest.config.ts | 9 ++ pnpm-lock.yaml | 119 +++++++++++++++--- 9 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 packages/graphql-config/src/config.test.ts rename packages/graphql-config/src/schema/{constraints.graphql => constraint.graphql} (100%) create mode 100644 packages/graphql-config/vitest.config.ts diff --git a/packages/graphql-config/package.json b/packages/graphql-config/package.json index 87c60357..ad9d3391 100644 --- a/packages/graphql-config/package.json +++ b/packages/graphql-config/package.json @@ -1,6 +1,7 @@ { "name": "@fabrix-framework/graphql-config", "private": false, + "type": "module", "version": "0.1.0", "description": "GraphQL configuration for fabrix", "exports": { @@ -21,19 +22,22 @@ "build": "tsup", "lint": "eslint '**/*.{ts,tsx}' --ignore-pattern 'dist/*' --max-warnings=0", "type-check": "tsc --noEmit --incremental --pretty", - "test": "exit 0" + "test": "vitest run" }, "dependencies": { "graphql": "^16.9.0" }, "devDependencies": { + "@fabrix-framework/eslint-config": "workspace:*", + "@fabrix-framework/prettier-config": "workspace:*", "@types/node": "^22.7.5", "eslint": "^9.6.0", + "memfs": "^4.14.0", "prettier": "^3.3.3", "tsup": "^8.1.0", "typescript": "^5.5.3", - "@fabrix-framework/eslint-config": "workspace:*", - "@fabrix-framework/prettier-config": "workspace:*" + "vite-plugin-graphql-loader": "^4.0.4", + "vitest": "^2.0.3" }, "prettier": "@fabrix-framework/prettier-config" } diff --git a/packages/graphql-config/src/config.test.ts b/packages/graphql-config/src/config.test.ts new file mode 100644 index 00000000..c1398cc6 --- /dev/null +++ b/packages/graphql-config/src/config.test.ts @@ -0,0 +1,25 @@ +import { vol } from "memfs"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { generateConfig } from "./config"; + +vi.mock("node:fs", async () => { + const { fs } = await vi.importActual("memfs"); + return fs; +}); +vi.mock("node:os", async () => ({ + tmpdir: () => "/tmp", +})); + +beforeEach(() => { + vol.fromJSON({ + "/tmp": null, + }); +}); + +describe("generateConfig", () => { + test("should generate a config", () => { + const filePath = generateConfig(); + const dirs = vol.toJSON(); + expect(dirs[filePath.directiveSchema]).not.toHaveLength(0); + }); +}); diff --git a/packages/graphql-config/src/config.ts b/packages/graphql-config/src/config.ts index abe2bae8..9ebfb8be 100644 --- a/packages/graphql-config/src/config.ts +++ b/packages/graphql-config/src/config.ts @@ -1,18 +1,14 @@ import * as os from "node:os"; import * as path from "node:path"; import * as fs from "node:fs"; -import CommonSchema from "./schema/common.graphql"; -import ViewDirectiveSchema from "./schema/view.graphql"; -import FormDirectiveSchema from "./schema/form.graphql"; -import ConstraintSchema from "./schema/constraint.graphql"; +import { print } from "graphql"; +import { schemaDefinition } from "./schema"; export const generateConfig = () => { const tempGQLFile = path.join(os.tmpdir(), "fabrix-graphql-config.graphql"); + const content = schemaDefinition.definitions.map(print).join("\n"); - fs.writeFileSync( - tempGQLFile, - CommonSchema + ViewDirectiveSchema + FormDirectiveSchema + ConstraintSchema, - ); + fs.writeFileSync(tempGQLFile, content); return { directiveSchema: tempGQLFile, diff --git a/packages/graphql-config/src/graphql.d.ts b/packages/graphql-config/src/graphql.d.ts index dc2883a8..0ef97c88 100644 --- a/packages/graphql-config/src/graphql.d.ts +++ b/packages/graphql-config/src/graphql.d.ts @@ -1,4 +1,2 @@ -declare module "*.graphql" { - const Document: string; - export default Document; -} +declare module "*.gql"; +declare module "*.graphql"; diff --git a/packages/graphql-config/src/schema.ts b/packages/graphql-config/src/schema.ts index 80a60984..40014ee4 100644 --- a/packages/graphql-config/src/schema.ts +++ b/packages/graphql-config/src/schema.ts @@ -1,4 +1,17 @@ -import { parse } from "graphql"; -import Document from "./directive.graphql"; +import { DocumentNode, Kind } from "graphql"; +import CommonSchema from "./schema/common.graphql"; +import ViewDirectiveSchema from "./schema/view.graphql"; +import FormDirectiveSchema from "./schema/form.graphql"; +import ConstraintSchema from "./schema/constraint.graphql"; -export const schemaDefinition = parse(Document); +const mergeDocumentNodes = (docs: DocumentNode[]) => ({ + kind: Kind.DOCUMENT, + definitions: docs.flatMap((doc) => doc.definitions), +}); + +export const schemaDefinition = mergeDocumentNodes([ + CommonSchema, + ViewDirectiveSchema, + FormDirectiveSchema, + ConstraintSchema, +]); diff --git a/packages/graphql-config/src/schema/constraints.graphql b/packages/graphql-config/src/schema/constraint.graphql similarity index 100% rename from packages/graphql-config/src/schema/constraints.graphql rename to packages/graphql-config/src/schema/constraint.graphql diff --git a/packages/graphql-config/tsconfig.json b/packages/graphql-config/tsconfig.json index f67c6684..19a8b645 100644 --- a/packages/graphql-config/tsconfig.json +++ b/packages/graphql-config/tsconfig.json @@ -3,5 +3,6 @@ "moduleResolution": "Bundler", "module": "ES2015" }, - "include": ["src", "tsup.config.ts"], + "include": ["."], + "exclude": ["dist", "node_modules"] } diff --git a/packages/graphql-config/vitest.config.ts b/packages/graphql-config/vitest.config.ts new file mode 100644 index 00000000..394fbe94 --- /dev/null +++ b/packages/graphql-config/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; +import graphqlLoader from "vite-plugin-graphql-loader"; + +export default defineConfig({ + plugins: [graphqlLoader()], + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04c7b750..d62cca57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: version: 18.3.1 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.2(vite@5.4.6(@types/node@22.7.5)) + version: 4.3.2(vite@5.4.8(@types/node@22.7.5)) eslint: specifier: ^9.6.0 version: 9.12.0 @@ -256,7 +256,7 @@ importers: version: 5.6.3 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3)(vite@5.4.6(@types/node@22.7.5)) + version: 4.3.2(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)) vitest: specifier: ^2.0.3 version: 2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3)) @@ -279,6 +279,9 @@ importers: eslint: specifier: ^9.6.0 version: 9.12.0 + memfs: + specifier: ^4.14.0 + version: 4.14.0 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -288,6 +291,12 @@ importers: typescript: specifier: ^5.5.3 version: 5.6.3 + vite-plugin-graphql-loader: + specifier: ^4.0.4 + version: 4.0.4 + vitest: + specifier: ^2.0.3 + version: 2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3)) shared/eslint: dependencies: @@ -302,7 +311,7 @@ importers: version: 9.1.0(eslint@9.12.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.31.0(eslint@9.12.0) + version: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0) typescript: specifier: ^5 version: 5.6.3 @@ -1053,6 +1062,24 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.1.0': + resolution: {integrity: sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.5.0': + resolution: {integrity: sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@mswjs/interceptors@0.35.9': resolution: {integrity: sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==} engines: {node: '>=18'} @@ -2333,6 +2360,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2607,6 +2638,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memfs@4.14.0: + resolution: {integrity: sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==} + engines: {node: '>= 4.0.0'} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -3251,6 +3286,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3298,6 +3339,12 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-dump@1.0.2: + resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3516,6 +3563,9 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-graphql-loader@4.0.4: + resolution: {integrity: sha512-lYnpQ2luV2fcuXmOJADljuktfMbDW00Y+6QS+Ek8Jz1Vdzlj/51LSGJwZqyjJ24a5YQ+o29Hr6el/5+nlZetvg==} + vite-tsconfig-paths@4.3.2: resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: @@ -4442,6 +4492,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + + '@jsonjoy.com/json-pack@1.1.0(tslib@2.7.0)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.7.0) + '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.7.0) + tslib: 2.7.0 + + '@jsonjoy.com/util@1.5.0(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + '@mswjs/interceptors@0.35.9': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -4854,17 +4920,6 @@ snapshots: transitivePeerDependencies: - graphql - '@vitejs/plugin-react@4.3.2(vite@5.4.6(@types/node@22.7.5))': - dependencies: - '@babel/core': 7.25.2 - '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) - '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 5.4.6(@types/node@22.7.5) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@22.7.5))': dependencies: '@babel/core': 7.25.2 @@ -5525,16 +5580,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(eslint@9.12.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5545,7 +5601,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -5556,6 +5612,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5924,6 +5982,8 @@ snapshots: human-signals@2.1.0: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -6161,6 +6221,13 @@ snapshots: media-typer@0.3.0: {} + memfs@4.14.0: + dependencies: + '@jsonjoy.com/json-pack': 1.1.0(tslib@2.7.0) + '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + tree-dump: 1.0.2(tslib@2.7.0) + tslib: 2.7.0 + memoize-one@6.0.0: {} merge-descriptors@1.0.3: {} @@ -6826,6 +6893,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thingies@1.21.0(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + tinybench@2.9.0: {} tinyexec@0.3.0: {} @@ -6864,6 +6935,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.0.2(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + tree-kill@1.2.2: {} ts-api-utils@1.3.0(typescript@5.6.3): @@ -7087,13 +7162,19 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.6(@types/node@22.7.5)): + vite-plugin-graphql-loader@4.0.4: + dependencies: + graphql: 16.9.0 + graphql-tag: 2.12.6(graphql@16.9.0) + magic-string: 0.30.11 + + vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.3(typescript@5.6.3) optionalDependencies: - vite: 5.4.6(@types/node@22.7.5) + vite: 5.4.8(@types/node@22.7.5) transitivePeerDependencies: - supports-color - typescript From fe70e65261e40e35353a6856976722ad976a5800 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:44:09 +0900 Subject: [PATCH 04/37] Fix lint errors --- packages/graphql-config/src/config.test.ts | 2 +- packages/graphql-config/src/graphql.d.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/graphql-config/src/config.test.ts b/packages/graphql-config/src/config.test.ts index c1398cc6..e5dd1cda 100644 --- a/packages/graphql-config/src/config.test.ts +++ b/packages/graphql-config/src/config.test.ts @@ -6,7 +6,7 @@ vi.mock("node:fs", async () => { const { fs } = await vi.importActual("memfs"); return fs; }); -vi.mock("node:os", async () => ({ +vi.mock("node:os", () => ({ tmpdir: () => "/tmp", })); diff --git a/packages/graphql-config/src/graphql.d.ts b/packages/graphql-config/src/graphql.d.ts index 0ef97c88..50b008e1 100644 --- a/packages/graphql-config/src/graphql.d.ts +++ b/packages/graphql-config/src/graphql.d.ts @@ -1,2 +1,4 @@ -declare module "*.gql"; -declare module "*.graphql"; +declare module "*.graphql" { + const Document: import("graphql").DocumentNode; + export default Document; +} From 5f63fb45bd64ff04d0a82da2adb3099fb35eba2d Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:46:35 +0900 Subject: [PATCH 05/37] Specified target --- packages/graphql-config/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql-config/tsconfig.json b/packages/graphql-config/tsconfig.json index 19a8b645..0bd58694 100644 --- a/packages/graphql-config/tsconfig.json +++ b/packages/graphql-config/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "ES6", "moduleResolution": "Bundler", "module": "ES2015" }, From 45072078d215d3d2d091e2f17d293eeb8fc1f265 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:50:05 +0900 Subject: [PATCH 06/37] Remove constraint.graphql --- packages/graphql-config/src/schema.ts | 2 - .../src/schema/constraint.graphql | 64 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 packages/graphql-config/src/schema/constraint.graphql diff --git a/packages/graphql-config/src/schema.ts b/packages/graphql-config/src/schema.ts index 40014ee4..05eb2284 100644 --- a/packages/graphql-config/src/schema.ts +++ b/packages/graphql-config/src/schema.ts @@ -2,7 +2,6 @@ import { DocumentNode, Kind } from "graphql"; import CommonSchema from "./schema/common.graphql"; import ViewDirectiveSchema from "./schema/view.graphql"; import FormDirectiveSchema from "./schema/form.graphql"; -import ConstraintSchema from "./schema/constraint.graphql"; const mergeDocumentNodes = (docs: DocumentNode[]) => ({ kind: Kind.DOCUMENT, @@ -13,5 +12,4 @@ export const schemaDefinition = mergeDocumentNodes([ CommonSchema, ViewDirectiveSchema, FormDirectiveSchema, - ConstraintSchema, ]); diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql deleted file mode 100644 index 303880c1..00000000 --- a/packages/graphql-config/src/schema/constraint.graphql +++ /dev/null @@ -1,64 +0,0 @@ -""" -Validation constraints for the form field -""" -input FabrixFormConstraint { - """ - Minimum length of the string - """ - minLength: Int - - """ - Maximum length of the string - """ - maxLength: Int - - """ - Validates if the string value starts with the given value - """ - startsWith: String - - """ - Validates if the string value ends with the given value - """ - endsWith: String - - """ - Validates if the string value contains the given value - """ - contains: String - - """ - Validates if the string value does not contain the given value - """ - notContains: String - - """ - Validates if the string value matches the given pattern - """ - pattern: String - - """ - Validates if the string value matches the custom format given - """ - format: String - - """ - Validates if the number value is greater than the given value - """ - min: Int - - """ - Validates if the number value is less than the given value - """ - max: Int - - """ - Validates if the number value is greater than or equal to the given value - """ - exclusiveMin: Float - - """ - Validates if the number value is less than or equal to the given value - """ - exclusiveMax: Float -} From de54976a594e7e58e8d645665b3c510002317e44 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 17:23:42 +0900 Subject: [PATCH 07/37] pnpm dedupe --- pnpm-lock.yaml | 192 ++++++++----------------------------------------- 1 file changed, 28 insertions(+), 164 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04c7b750..3936f23d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -416,10 +416,6 @@ packages: resolution: {integrity: sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA==} engines: {node: '>=14'} - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} - engines: {node: '>=6.9.0'} - '@babel/code-frame@7.25.7': resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} engines: {node: '>=6.9.0'} @@ -462,10 +458,6 @@ packages: resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} - engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.25.7': resolution: {integrity: sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==} engines: {node: '>=6.9.0'} @@ -478,10 +470,6 @@ packages: resolution: {integrity: sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - '@babel/highlight@7.25.7': resolution: {integrity: sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==} engines: {node: '>=6.9.0'} @@ -503,10 +491,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.25.6': - resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.25.7': resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} engines: {node: '>=6.9.0'} @@ -585,9 +569,6 @@ packages: '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} - '@emotion/is-prop-valid@1.3.0': - resolution: {integrity: sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==} - '@emotion/is-prop-valid@1.3.1': resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} @@ -921,10 +902,6 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint-community/regexpp@4.11.1': resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -1845,15 +1822,6 @@ packages: supports-color: optional: true - debug@4.3.6: - resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -2216,9 +2184,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} @@ -2580,9 +2545,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.1: - resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} - loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} @@ -2666,9 +2628,6 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2863,10 +2822,6 @@ packages: yaml: optional: true - postcss@8.4.45: - resolution: {integrity: sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.4.47: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} @@ -3138,10 +3093,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3524,37 +3475,6 @@ packages: vite: optional: true - vite@5.4.6: - resolution: {integrity: sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vite@5.4.8: resolution: {integrity: sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3829,11 +3749,6 @@ snapshots: '@apollo/utils.withrequired@2.0.1': {} - '@babel/code-frame@7.24.7': - dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.1.0 - '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 @@ -3844,7 +3759,7 @@ snapshots: '@babel/core@7.25.2': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.25.7 '@babel/generator': 7.25.6 '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) @@ -3854,7 +3769,7 @@ snapshots: '@babel/traverse': 7.25.6 '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -3888,7 +3803,7 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.24.7 '@babel/helper-simple-access': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 + '@babel/helper-validator-identifier': 7.25.7 '@babel/traverse': 7.25.6 transitivePeerDependencies: - supports-color @@ -3904,8 +3819,6 @@ snapshots: '@babel/helper-string-parser@7.24.8': {} - '@babel/helper-validator-identifier@7.24.7': {} - '@babel/helper-validator-identifier@7.25.7': {} '@babel/helper-validator-option@7.24.8': {} @@ -3915,13 +3828,6 @@ snapshots: '@babel/template': 7.25.0 '@babel/types': 7.25.6 - '@babel/highlight@7.24.7': - dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.1.0 - '@babel/highlight@7.25.7': dependencies: '@babel/helper-validator-identifier': 7.25.7 @@ -3943,28 +3849,24 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.8 - '@babel/runtime@7.25.6': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.25.7': dependencies: regenerator-runtime: 0.14.1 '@babel/template@7.25.0': dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.25.7 '@babel/parser': 7.25.6 '@babel/types': 7.25.6 '@babel/traverse@7.25.6': dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.25.7 '@babel/generator': 7.25.6 '@babel/parser': 7.25.6 '@babel/template': 7.25.0 '@babel/types': 7.25.6 - debug: 4.3.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -3972,7 +3874,7 @@ snapshots: '@babel/types@7.25.6': dependencies: '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 + '@babel/helper-validator-identifier': 7.25.7 to-fast-properties: 2.0.0 '@bundled-es-modules/cookie@2.0.0': @@ -4057,7 +3959,7 @@ snapshots: '@emotion/babel-plugin@11.12.0': dependencies: '@babel/helper-module-imports': 7.24.7 - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.1 @@ -4090,20 +3992,15 @@ snapshots: '@emotion/hash@0.9.2': {} - '@emotion/is-prop-valid@1.3.0': - dependencies: - '@emotion/memoize': 0.9.0 - '@emotion/is-prop-valid@1.3.1': dependencies: '@emotion/memoize': 0.9.0 - optional: true '@emotion/memoize@0.9.0': {} '@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@emotion/babel-plugin': 11.12.0 '@emotion/cache': 11.13.1 '@emotion/serialize': 1.3.1 @@ -4129,9 +4026,9 @@ snapshots: '@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@emotion/babel-plugin': 11.12.0 - '@emotion/is-prop-valid': 1.3.0 + '@emotion/is-prop-valid': 1.3.1 '@emotion/react': 11.13.3(@types/react@18.3.11)(react@18.3.1) '@emotion/serialize': 1.3.1 '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) @@ -4298,8 +4195,6 @@ snapshots: eslint: 9.12.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.0': {} - '@eslint-community/regexpp@4.11.1': {} '@eslint/compat@1.2.0(eslint@9.12.0)': @@ -4309,7 +4204,7 @@ snapshots: '@eslint/config-array@0.18.0': dependencies: '@eslint/object-schema': 2.1.4 - debug: 4.3.6 + debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -4319,7 +4214,7 @@ snapshots: '@eslint/eslintrc@3.1.0': dependencies: ajv: 6.12.6 - debug: 4.3.6 + debug: 4.3.7 espree: 10.2.0 globals: 14.0.0 ignore: 5.3.2 @@ -4633,7 +4528,7 @@ snapshots: '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.25.7 '@testing-library/dom': 10.4.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -4883,14 +4778,14 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.6(@types/node@22.7.5))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.8(@types/node@22.7.5))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: msw: 2.4.9(typescript@5.6.3) - vite: 5.4.6(@types/node@22.7.5) + vite: 5.4.8(@types/node@22.7.5) '@vitest/pretty-format@2.1.2': dependencies: @@ -5132,7 +5027,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.1 + loupe: 3.1.2 pathval: 2.0.0 chakra-react-select@5.0.1(@chakra-ui/react@2.10.2(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(framer-motion@11.11.8(@emotion/is-prop-valid@1.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.13.3(@types/react@18.3.11)(react@18.3.1))(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -5291,10 +5186,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.6: - dependencies: - ms: 2.1.2 - debug@4.3.7: dependencies: ms: 2.1.3 @@ -5595,7 +5486,7 @@ snapshots: eslint@9.12.0: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) - '@eslint-community/regexpp': 4.11.0 + '@eslint-community/regexpp': 4.11.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.6.0 '@eslint/eslintrc': 3.1.0 @@ -5609,7 +5500,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.1.0 eslint-visitor-keys: 4.1.0 @@ -5813,8 +5704,6 @@ snapshots: get-caller-file@2.0.5: {} - get-func-name@2.0.2: {} - get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -6139,10 +6028,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.1: - dependencies: - get-func-name: 2.0.2 - loupe@3.1.2: {} lru-cache@10.4.3: {} @@ -6202,8 +6087,6 @@ snapshots: ms@2.0.0: {} - ms@2.1.2: {} - ms@2.1.3: {} msw@2.4.9(typescript@5.6.3): @@ -6329,7 +6212,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.25.7 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -6374,18 +6257,11 @@ snapshots: postcss: 8.4.47 tsx: 4.19.1 - postcss@8.4.45: - dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 - source-map-js: 1.2.0 - postcss@8.4.47: dependencies: nanoid: 3.3.7 picocolors: 1.1.0 source-map-js: 1.2.1 - optional: true prelude-ls@1.2.1: {} @@ -6702,10 +6578,7 @@ snapshots: signal-exit@4.1.0: {} - source-map-js@1.2.0: {} - - source-map-js@1.2.1: - optional: true + source-map-js@1.2.1: {} source-map@0.5.7: {} @@ -6893,7 +6766,7 @@ snapshots: cac: 6.7.14 chokidar: 3.6.0 consola: 3.2.3 - debug: 4.3.6 + debug: 4.3.7 esbuild: 0.23.1 execa: 5.1.1 joycon: 3.1.1 @@ -7073,9 +6946,9 @@ snapshots: vite-node@2.1.2(@types/node@22.7.5): dependencies: cac: 6.7.14 - debug: 4.3.6 + debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.6(@types/node@22.7.5) + vite: 5.4.8(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - less @@ -7098,19 +6971,10 @@ snapshots: - supports-color - typescript - vite@5.4.6(@types/node@22.7.5): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.45 - rollup: 4.21.2 - optionalDependencies: - '@types/node': 22.7.5 - fsevents: 2.3.3 - vite@5.4.8(@types/node@22.7.5): dependencies: esbuild: 0.21.5 - postcss: 8.4.45 + postcss: 8.4.47 rollup: 4.21.2 optionalDependencies: '@types/node': 22.7.5 @@ -7119,14 +6983,14 @@ snapshots: vitest@2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3)): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.6(@types/node@22.7.5)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(msw@2.4.9(typescript@5.6.3))(vite@5.4.8(@types/node@22.7.5)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 '@vitest/spy': 2.1.2 '@vitest/utils': 2.1.2 chai: 5.1.1 - debug: 4.3.6 + debug: 4.3.7 magic-string: 0.30.11 pathe: 1.1.2 std-env: 3.7.0 @@ -7134,7 +6998,7 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.6(@types/node@22.7.5) + vite: 5.4.8(@types/node@22.7.5) vite-node: 2.1.2(@types/node@22.7.5) why-is-node-running: 2.3.0 optionalDependencies: From 85f2115716383b75cf34b393fedcabe8a37076ac Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 17 Oct 2024 17:33:23 +0900 Subject: [PATCH 08/37] Add FabrixFormConstraint type --- packages/graphql-config/src/directive.graphql | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/graphql-config/src/directive.graphql b/packages/graphql-config/src/directive.graphql index 142e6cf4..64b64ddb 100644 --- a/packages/graphql-config/src/directive.graphql +++ b/packages/graphql-config/src/directive.graphql @@ -117,6 +117,28 @@ input FabrixFormField { config: FabrixFormFieldConfig! } +input FabrixFormConstraint { + """ + String + """ + minLength: Int + maxLength: Int + startsWith: String + endsWith: String + contains: String + notContains: String + pattern: String + format: String + + """ + Int/Float + """ + min: Int + max: Int + exclusiveMin: Float + exclusiveMax: Float +} + """ Fabrix directive for form """ From bd8ce818f67688d2c4e86984467a9438bb699e2a Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 13:13:19 +0900 Subject: [PATCH 09/37] Split GraphQL definition files --- packages/graphql-config/src/config.ts | 10 +- packages/graphql-config/src/directive.graphql | 145 ------------------ .../graphql-config/src/schema/common.graphql | 23 +++ .../src/schema/constraints.graphql | 64 ++++++++ .../graphql-config/src/schema/form.graphql | 55 +++++++ .../graphql-config/src/schema/view.graphql | 43 ++++++ 6 files changed, 193 insertions(+), 147 deletions(-) delete mode 100644 packages/graphql-config/src/directive.graphql create mode 100644 packages/graphql-config/src/schema/common.graphql create mode 100644 packages/graphql-config/src/schema/constraints.graphql create mode 100644 packages/graphql-config/src/schema/form.graphql create mode 100644 packages/graphql-config/src/schema/view.graphql diff --git a/packages/graphql-config/src/config.ts b/packages/graphql-config/src/config.ts index 2ebf32e9..abe2bae8 100644 --- a/packages/graphql-config/src/config.ts +++ b/packages/graphql-config/src/config.ts @@ -1,12 +1,18 @@ import * as os from "node:os"; import * as path from "node:path"; import * as fs from "node:fs"; -import Document from "./directive.graphql"; +import CommonSchema from "./schema/common.graphql"; +import ViewDirectiveSchema from "./schema/view.graphql"; +import FormDirectiveSchema from "./schema/form.graphql"; +import ConstraintSchema from "./schema/constraint.graphql"; export const generateConfig = () => { const tempGQLFile = path.join(os.tmpdir(), "fabrix-graphql-config.graphql"); - fs.writeFileSync(tempGQLFile, Document); + fs.writeFileSync( + tempGQLFile, + CommonSchema + ViewDirectiveSchema + FormDirectiveSchema + ConstraintSchema, + ); return { directiveSchema: tempGQLFile, diff --git a/packages/graphql-config/src/directive.graphql b/packages/graphql-config/src/directive.graphql deleted file mode 100644 index 64b64ddb..00000000 --- a/packages/graphql-config/src/directive.graphql +++ /dev/null @@ -1,145 +0,0 @@ -input FabrixComponentType { - """ - Component name to render the field - """ - name: String! - - """ - Component props - """ - props: [FabrixComponentProps] -} - -input FabrixComponentProps { - """ - The property name for the component - """ - name: String! - - """ - The value for the property - """ - value: String! -} - -input FabrixViewConfig { - """ - The number of grid columns the field (max: 12) - """ - gridCol: Int - - """ - The 0-based index of the field - """ - index: Int - - """ - The label of the field on UI - """ - label: String - - """ - Hide the field on UI - """ - hidden: Boolean - - """ - The component to render for the field - """ - componentType: FabrixComponentType -} - -input FabrixView { - """ - The field name in the schema - """ - field: String! - - """ - The configuration for the field - """ - config: FabrixViewConfig! -} - -""" -Fabrix directive for fields -""" -directive @fabrixView(input: [FabrixView!]) on FIELD - -input FabrixFormFieldConfig { - """ - The number of grid columns the field (max: 12) - """ - gridCol: Int - - """ - The 0-based index of the field - """ - index: Int - - """ - The label of the field on UI - """ - label: String - - """ - Placeholder text for the field - """ - placeholder: String - - """ - Hide the field on UI - """ - hidden: Boolean - - """ - The default value for the field - - The value will automatically be converted to the type of the field - """ - defaultValue: String - - """ - The component to render for the field - """ - componentType: FabrixComponentType -} - -input FabrixFormField { - """ - The field name in the schema - """ - field: String! - - """ - The configuration for the field - """ - config: FabrixFormFieldConfig! -} - -input FabrixFormConstraint { - """ - String - """ - minLength: Int - maxLength: Int - startsWith: String - endsWith: String - contains: String - notContains: String - pattern: String - format: String - - """ - Int/Float - """ - min: Int - max: Int - exclusiveMin: Float - exclusiveMax: Float -} - -""" -Fabrix directive for form -""" -directive @fabrixForm(input: [FabrixFormField!]) on FIELD diff --git a/packages/graphql-config/src/schema/common.graphql b/packages/graphql-config/src/schema/common.graphql new file mode 100644 index 00000000..f799f2b4 --- /dev/null +++ b/packages/graphql-config/src/schema/common.graphql @@ -0,0 +1,23 @@ +input FabrixComponentType { + """ + Component name to render the field + """ + name: String! + + """ + Component props + """ + props: [FabrixComponentProps] +} + +input FabrixComponentProps { + """ + The property name for the component + """ + name: String! + + """ + The value for the property + """ + value: String! +} diff --git a/packages/graphql-config/src/schema/constraints.graphql b/packages/graphql-config/src/schema/constraints.graphql new file mode 100644 index 00000000..303880c1 --- /dev/null +++ b/packages/graphql-config/src/schema/constraints.graphql @@ -0,0 +1,64 @@ +""" +Validation constraints for the form field +""" +input FabrixFormConstraint { + """ + Minimum length of the string + """ + minLength: Int + + """ + Maximum length of the string + """ + maxLength: Int + + """ + Validates if the string value starts with the given value + """ + startsWith: String + + """ + Validates if the string value ends with the given value + """ + endsWith: String + + """ + Validates if the string value contains the given value + """ + contains: String + + """ + Validates if the string value does not contain the given value + """ + notContains: String + + """ + Validates if the string value matches the given pattern + """ + pattern: String + + """ + Validates if the string value matches the custom format given + """ + format: String + + """ + Validates if the number value is greater than the given value + """ + min: Int + + """ + Validates if the number value is less than the given value + """ + max: Int + + """ + Validates if the number value is greater than or equal to the given value + """ + exclusiveMin: Float + + """ + Validates if the number value is less than or equal to the given value + """ + exclusiveMax: Float +} diff --git a/packages/graphql-config/src/schema/form.graphql b/packages/graphql-config/src/schema/form.graphql new file mode 100644 index 00000000..eb0eed01 --- /dev/null +++ b/packages/graphql-config/src/schema/form.graphql @@ -0,0 +1,55 @@ +input FabrixFormFieldConfig { + """ + The number of grid columns the field (max: 12) + """ + gridCol: Int + + """ + The 0-based index of the field + """ + index: Int + + """ + The label of the field on UI + """ + label: String + + """ + Placeholder text for the field + """ + placeholder: String + + """ + Hide the field on UI + """ + hidden: Boolean + + """ + The default value for the field + + The value will automatically be converted to the type of the field + """ + defaultValue: String + + """ + The component to render for the field + """ + componentType: FabrixComponentType +} + +input FabrixFormField { + """ + The field name in the schema + """ + field: String! + + """ + The configuration for the field + """ + config: FabrixFormFieldConfig! +} + +""" +Fabrix directive for form +""" +directive @fabrixForm(input: [FabrixFormField!]) on FIELD diff --git a/packages/graphql-config/src/schema/view.graphql b/packages/graphql-config/src/schema/view.graphql new file mode 100644 index 00000000..da37795a --- /dev/null +++ b/packages/graphql-config/src/schema/view.graphql @@ -0,0 +1,43 @@ +input FabrixViewConfig { + """ + The number of grid columns the field (max: 12) + """ + gridCol: Int + + """ + The 0-based index of the field + """ + index: Int + + """ + The label of the field on UI + """ + label: String + + """ + Hide the field on UI + """ + hidden: Boolean + + """ + The component to render for the field + """ + componentType: FabrixComponentType +} + +input FabrixView { + """ + The field name in the schema + """ + field: String! + + """ + The configuration for the field + """ + config: FabrixViewConfig! +} + +""" +Fabrix directive for fields +""" +directive @fabrixView(input: [FabrixView!]) on FIELD From e3b8b381375369ab9d8eb43dae7281070f451ce9 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:40:44 +0900 Subject: [PATCH 10/37] Add test for graphql-config --- packages/graphql-config/package.json | 10 +- packages/graphql-config/src/config.test.ts | 25 ++++ packages/graphql-config/src/config.ts | 12 +- packages/graphql-config/src/graphql.d.ts | 6 +- packages/graphql-config/src/schema.ts | 19 ++- ...constraints.graphql => constraint.graphql} | 0 packages/graphql-config/tsconfig.json | 3 +- packages/graphql-config/vitest.config.ts | 9 ++ pnpm-lock.yaml | 119 +++++++++++++++--- 9 files changed, 165 insertions(+), 38 deletions(-) create mode 100644 packages/graphql-config/src/config.test.ts rename packages/graphql-config/src/schema/{constraints.graphql => constraint.graphql} (100%) create mode 100644 packages/graphql-config/vitest.config.ts diff --git a/packages/graphql-config/package.json b/packages/graphql-config/package.json index 87c60357..ad9d3391 100644 --- a/packages/graphql-config/package.json +++ b/packages/graphql-config/package.json @@ -1,6 +1,7 @@ { "name": "@fabrix-framework/graphql-config", "private": false, + "type": "module", "version": "0.1.0", "description": "GraphQL configuration for fabrix", "exports": { @@ -21,19 +22,22 @@ "build": "tsup", "lint": "eslint '**/*.{ts,tsx}' --ignore-pattern 'dist/*' --max-warnings=0", "type-check": "tsc --noEmit --incremental --pretty", - "test": "exit 0" + "test": "vitest run" }, "dependencies": { "graphql": "^16.9.0" }, "devDependencies": { + "@fabrix-framework/eslint-config": "workspace:*", + "@fabrix-framework/prettier-config": "workspace:*", "@types/node": "^22.7.5", "eslint": "^9.6.0", + "memfs": "^4.14.0", "prettier": "^3.3.3", "tsup": "^8.1.0", "typescript": "^5.5.3", - "@fabrix-framework/eslint-config": "workspace:*", - "@fabrix-framework/prettier-config": "workspace:*" + "vite-plugin-graphql-loader": "^4.0.4", + "vitest": "^2.0.3" }, "prettier": "@fabrix-framework/prettier-config" } diff --git a/packages/graphql-config/src/config.test.ts b/packages/graphql-config/src/config.test.ts new file mode 100644 index 00000000..c1398cc6 --- /dev/null +++ b/packages/graphql-config/src/config.test.ts @@ -0,0 +1,25 @@ +import { vol } from "memfs"; +import { beforeEach, describe, expect, test, vi } from "vitest"; +import { generateConfig } from "./config"; + +vi.mock("node:fs", async () => { + const { fs } = await vi.importActual("memfs"); + return fs; +}); +vi.mock("node:os", async () => ({ + tmpdir: () => "/tmp", +})); + +beforeEach(() => { + vol.fromJSON({ + "/tmp": null, + }); +}); + +describe("generateConfig", () => { + test("should generate a config", () => { + const filePath = generateConfig(); + const dirs = vol.toJSON(); + expect(dirs[filePath.directiveSchema]).not.toHaveLength(0); + }); +}); diff --git a/packages/graphql-config/src/config.ts b/packages/graphql-config/src/config.ts index abe2bae8..9ebfb8be 100644 --- a/packages/graphql-config/src/config.ts +++ b/packages/graphql-config/src/config.ts @@ -1,18 +1,14 @@ import * as os from "node:os"; import * as path from "node:path"; import * as fs from "node:fs"; -import CommonSchema from "./schema/common.graphql"; -import ViewDirectiveSchema from "./schema/view.graphql"; -import FormDirectiveSchema from "./schema/form.graphql"; -import ConstraintSchema from "./schema/constraint.graphql"; +import { print } from "graphql"; +import { schemaDefinition } from "./schema"; export const generateConfig = () => { const tempGQLFile = path.join(os.tmpdir(), "fabrix-graphql-config.graphql"); + const content = schemaDefinition.definitions.map(print).join("\n"); - fs.writeFileSync( - tempGQLFile, - CommonSchema + ViewDirectiveSchema + FormDirectiveSchema + ConstraintSchema, - ); + fs.writeFileSync(tempGQLFile, content); return { directiveSchema: tempGQLFile, diff --git a/packages/graphql-config/src/graphql.d.ts b/packages/graphql-config/src/graphql.d.ts index dc2883a8..0ef97c88 100644 --- a/packages/graphql-config/src/graphql.d.ts +++ b/packages/graphql-config/src/graphql.d.ts @@ -1,4 +1,2 @@ -declare module "*.graphql" { - const Document: string; - export default Document; -} +declare module "*.gql"; +declare module "*.graphql"; diff --git a/packages/graphql-config/src/schema.ts b/packages/graphql-config/src/schema.ts index 80a60984..40014ee4 100644 --- a/packages/graphql-config/src/schema.ts +++ b/packages/graphql-config/src/schema.ts @@ -1,4 +1,17 @@ -import { parse } from "graphql"; -import Document from "./directive.graphql"; +import { DocumentNode, Kind } from "graphql"; +import CommonSchema from "./schema/common.graphql"; +import ViewDirectiveSchema from "./schema/view.graphql"; +import FormDirectiveSchema from "./schema/form.graphql"; +import ConstraintSchema from "./schema/constraint.graphql"; -export const schemaDefinition = parse(Document); +const mergeDocumentNodes = (docs: DocumentNode[]) => ({ + kind: Kind.DOCUMENT, + definitions: docs.flatMap((doc) => doc.definitions), +}); + +export const schemaDefinition = mergeDocumentNodes([ + CommonSchema, + ViewDirectiveSchema, + FormDirectiveSchema, + ConstraintSchema, +]); diff --git a/packages/graphql-config/src/schema/constraints.graphql b/packages/graphql-config/src/schema/constraint.graphql similarity index 100% rename from packages/graphql-config/src/schema/constraints.graphql rename to packages/graphql-config/src/schema/constraint.graphql diff --git a/packages/graphql-config/tsconfig.json b/packages/graphql-config/tsconfig.json index f67c6684..19a8b645 100644 --- a/packages/graphql-config/tsconfig.json +++ b/packages/graphql-config/tsconfig.json @@ -3,5 +3,6 @@ "moduleResolution": "Bundler", "module": "ES2015" }, - "include": ["src", "tsup.config.ts"], + "include": ["."], + "exclude": ["dist", "node_modules"] } diff --git a/packages/graphql-config/vitest.config.ts b/packages/graphql-config/vitest.config.ts new file mode 100644 index 00000000..394fbe94 --- /dev/null +++ b/packages/graphql-config/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; +import graphqlLoader from "vite-plugin-graphql-loader"; + +export default defineConfig({ + plugins: [graphqlLoader()], + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3936f23d..6e12b728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: version: 18.3.1 '@vitejs/plugin-react': specifier: ^4.3.1 - version: 4.3.2(vite@5.4.6(@types/node@22.7.5)) + version: 4.3.2(vite@5.4.8(@types/node@22.7.5)) eslint: specifier: ^9.6.0 version: 9.12.0 @@ -256,7 +256,7 @@ importers: version: 5.6.3 vite-tsconfig-paths: specifier: ^4.3.2 - version: 4.3.2(typescript@5.6.3)(vite@5.4.6(@types/node@22.7.5)) + version: 4.3.2(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)) vitest: specifier: ^2.0.3 version: 2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3)) @@ -279,6 +279,9 @@ importers: eslint: specifier: ^9.6.0 version: 9.12.0 + memfs: + specifier: ^4.14.0 + version: 4.14.0 prettier: specifier: ^3.3.3 version: 3.3.3 @@ -288,6 +291,12 @@ importers: typescript: specifier: ^5.5.3 version: 5.6.3 + vite-plugin-graphql-loader: + specifier: ^4.0.4 + version: 4.0.4 + vitest: + specifier: ^2.0.3 + version: 2.1.2(@types/node@22.7.5)(happy-dom@15.7.4)(msw@2.4.9(typescript@5.6.3)) shared/eslint: dependencies: @@ -302,7 +311,7 @@ importers: version: 9.1.0(eslint@9.12.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.31.0(eslint@9.12.0) + version: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0) typescript: specifier: ^5 version: 5.6.3 @@ -1030,6 +1039,24 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.1.0': + resolution: {integrity: sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.5.0': + resolution: {integrity: sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@mswjs/interceptors@0.35.9': resolution: {integrity: sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==} engines: {node: '>=18'} @@ -2298,6 +2325,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2569,6 +2600,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memfs@4.14.0: + resolution: {integrity: sha512-JUeY0F/fQZgIod31Ja1eJgiSxLn7BfQlCnqhwXFBzFHEw63OdLK7VJUJ7bnzNsWgCyoUP5tEp1VRY8rDaYzqOA==} + engines: {node: '>= 4.0.0'} + memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} @@ -3202,6 +3237,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3249,6 +3290,12 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-dump@1.0.2: + resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -3467,6 +3514,9 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-plugin-graphql-loader@4.0.4: + resolution: {integrity: sha512-lYnpQ2luV2fcuXmOJADljuktfMbDW00Y+6QS+Ek8Jz1Vdzlj/51LSGJwZqyjJ24a5YQ+o29Hr6el/5+nlZetvg==} + vite-tsconfig-paths@4.3.2: resolution: {integrity: sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==} peerDependencies: @@ -4337,6 +4387,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + + '@jsonjoy.com/json-pack@1.1.0(tslib@2.7.0)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.7.0) + '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.7.0) + tslib: 2.7.0 + + '@jsonjoy.com/util@1.5.0(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + '@mswjs/interceptors@0.35.9': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -4749,17 +4815,6 @@ snapshots: transitivePeerDependencies: - graphql - '@vitejs/plugin-react@4.3.2(vite@5.4.6(@types/node@22.7.5))': - dependencies: - '@babel/core': 7.25.2 - '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) - '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 5.4.6(@types/node@22.7.5) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.3.2(vite@5.4.8(@types/node@22.7.5))': dependencies: '@babel/core': 7.25.2 @@ -5416,16 +5471,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(eslint@9.12.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5436,7 +5492,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -5447,6 +5503,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5813,6 +5871,8 @@ snapshots: human-signals@2.1.0: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -6046,6 +6106,13 @@ snapshots: media-typer@0.3.0: {} + memfs@4.14.0: + dependencies: + '@jsonjoy.com/json-pack': 1.1.0(tslib@2.7.0) + '@jsonjoy.com/util': 1.5.0(tslib@2.7.0) + tree-dump: 1.0.2(tslib@2.7.0) + tslib: 2.7.0 + memoize-one@6.0.0: {} merge-descriptors@1.0.3: {} @@ -6699,6 +6766,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thingies@1.21.0(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + tinybench@2.9.0: {} tinyexec@0.3.0: {} @@ -6737,6 +6808,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.0.2(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + tree-kill@1.2.2: {} ts-api-utils@1.3.0(typescript@5.6.3): @@ -6960,13 +7035,19 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.6(@types/node@22.7.5)): + vite-plugin-graphql-loader@4.0.4: + dependencies: + graphql: 16.9.0 + graphql-tag: 2.12.6(graphql@16.9.0) + magic-string: 0.30.11 + + vite-tsconfig-paths@4.3.2(typescript@5.6.3)(vite@5.4.8(@types/node@22.7.5)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.3(typescript@5.6.3) optionalDependencies: - vite: 5.4.6(@types/node@22.7.5) + vite: 5.4.8(@types/node@22.7.5) transitivePeerDependencies: - supports-color - typescript From a12e7f76fdece7aea15bdc66f51dfe23654ab668 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:44:09 +0900 Subject: [PATCH 11/37] Fix lint errors --- packages/graphql-config/src/config.test.ts | 2 +- packages/graphql-config/src/graphql.d.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/graphql-config/src/config.test.ts b/packages/graphql-config/src/config.test.ts index c1398cc6..e5dd1cda 100644 --- a/packages/graphql-config/src/config.test.ts +++ b/packages/graphql-config/src/config.test.ts @@ -6,7 +6,7 @@ vi.mock("node:fs", async () => { const { fs } = await vi.importActual("memfs"); return fs; }); -vi.mock("node:os", async () => ({ +vi.mock("node:os", () => ({ tmpdir: () => "/tmp", })); diff --git a/packages/graphql-config/src/graphql.d.ts b/packages/graphql-config/src/graphql.d.ts index 0ef97c88..50b008e1 100644 --- a/packages/graphql-config/src/graphql.d.ts +++ b/packages/graphql-config/src/graphql.d.ts @@ -1,2 +1,4 @@ -declare module "*.gql"; -declare module "*.graphql"; +declare module "*.graphql" { + const Document: import("graphql").DocumentNode; + export default Document; +} From 212f79ddcb8528723262c0bdcc8125c96c9e4a12 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:46:35 +0900 Subject: [PATCH 12/37] Specified target --- packages/graphql-config/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql-config/tsconfig.json b/packages/graphql-config/tsconfig.json index 19a8b645..0bd58694 100644 --- a/packages/graphql-config/tsconfig.json +++ b/packages/graphql-config/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "target": "ES6", "moduleResolution": "Bundler", "module": "ES2015" }, From ef42f22600b9a414f7d38e6f30036daa64aa2c57 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:50:05 +0900 Subject: [PATCH 13/37] Remove constraint.graphql --- packages/graphql-config/src/schema.ts | 2 - .../src/schema/constraint.graphql | 64 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 packages/graphql-config/src/schema/constraint.graphql diff --git a/packages/graphql-config/src/schema.ts b/packages/graphql-config/src/schema.ts index 40014ee4..05eb2284 100644 --- a/packages/graphql-config/src/schema.ts +++ b/packages/graphql-config/src/schema.ts @@ -2,7 +2,6 @@ import { DocumentNode, Kind } from "graphql"; import CommonSchema from "./schema/common.graphql"; import ViewDirectiveSchema from "./schema/view.graphql"; import FormDirectiveSchema from "./schema/form.graphql"; -import ConstraintSchema from "./schema/constraint.graphql"; const mergeDocumentNodes = (docs: DocumentNode[]) => ({ kind: Kind.DOCUMENT, @@ -13,5 +12,4 @@ export const schemaDefinition = mergeDocumentNodes([ CommonSchema, ViewDirectiveSchema, FormDirectiveSchema, - ConstraintSchema, ]); diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql deleted file mode 100644 index 303880c1..00000000 --- a/packages/graphql-config/src/schema/constraint.graphql +++ /dev/null @@ -1,64 +0,0 @@ -""" -Validation constraints for the form field -""" -input FabrixFormConstraint { - """ - Minimum length of the string - """ - minLength: Int - - """ - Maximum length of the string - """ - maxLength: Int - - """ - Validates if the string value starts with the given value - """ - startsWith: String - - """ - Validates if the string value ends with the given value - """ - endsWith: String - - """ - Validates if the string value contains the given value - """ - contains: String - - """ - Validates if the string value does not contain the given value - """ - notContains: String - - """ - Validates if the string value matches the given pattern - """ - pattern: String - - """ - Validates if the string value matches the custom format given - """ - format: String - - """ - Validates if the number value is greater than the given value - """ - min: Int - - """ - Validates if the number value is less than the given value - """ - max: Int - - """ - Validates if the number value is greater than or equal to the given value - """ - exclusiveMin: Float - - """ - Validates if the number value is less than or equal to the given value - """ - exclusiveMax: Float -} From fc9e623bab1f352dde158262cd8afb8bc5b210bd Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 14:58:57 +0900 Subject: [PATCH 14/37] Add input type for constraint --- packages/graphql-config/src/schema.ts | 2 ++ .../src/schema/constraint.graphql | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 packages/graphql-config/src/schema/constraint.graphql diff --git a/packages/graphql-config/src/schema.ts b/packages/graphql-config/src/schema.ts index 05eb2284..40014ee4 100644 --- a/packages/graphql-config/src/schema.ts +++ b/packages/graphql-config/src/schema.ts @@ -2,6 +2,7 @@ import { DocumentNode, Kind } from "graphql"; import CommonSchema from "./schema/common.graphql"; import ViewDirectiveSchema from "./schema/view.graphql"; import FormDirectiveSchema from "./schema/form.graphql"; +import ConstraintSchema from "./schema/constraint.graphql"; const mergeDocumentNodes = (docs: DocumentNode[]) => ({ kind: Kind.DOCUMENT, @@ -12,4 +13,5 @@ export const schemaDefinition = mergeDocumentNodes([ CommonSchema, ViewDirectiveSchema, FormDirectiveSchema, + ConstraintSchema, ]); diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql new file mode 100644 index 00000000..2b45aa88 --- /dev/null +++ b/packages/graphql-config/src/schema/constraint.graphql @@ -0,0 +1,21 @@ +input FabrixFormConstraint { + """ + String + """ + minLength: Int + maxLength: Int + startsWith: String + endsWith: String + contains: String + notContains: String + pattern: String + format: String + + """ + Int/Float + """ + min: Int + max: Int + exclusiveMin: Float + exclusiveMax: Float +} From 22d20d38a84016e39fe76edd02d6b4b166ab8fa2 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 15:24:33 +0900 Subject: [PATCH 15/37] Added zod schema for constraint fields --- packages/fabrix/src/directive/schema.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index 23fcc00a..224e96d4 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -35,6 +35,24 @@ export const formFieldSchema = baseFieldSchema.merge( }), ); +const formFieldConstraintSchema = z.object({ + // String constraints + minLength: z.number().nullish(), + maxLength: z.number().nullish(), + startsWith: z.string().nullish(), + endsWith: z.string().nullish(), + contains: z.string().nullish(), + notContais: z.string().nullish(), + pattern: z.string().nullish(), + format: z.string().nullish(), + + // Number constraints + min: z.number().nullish(), + max: z.number().nullish(), + exclusiveMin: z.number().nullish(), + exclusiveMax: z.number().nullish(), +}); + export const viewFieldSchema = baseFieldSchema.merge( z.object({ gridCol: z @@ -89,6 +107,7 @@ export const directiveSchemaMap = { hidden: defaultValues.hidden, }), ), + constraint: formFieldConstraintSchema.nullish(), }), ), }), From d5a745ce6e1835a6551efa1d0d5cfc6e3d3de6bc Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 15:25:06 +0900 Subject: [PATCH 16/37] Use constraint in example --- examples/todoapp/src/App.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/todoapp/src/App.tsx b/examples/todoapp/src/App.tsx index 1ad1f60b..ece8d658 100644 --- a/examples/todoapp/src/App.tsx +++ b/examples/todoapp/src/App.tsx @@ -21,7 +21,11 @@ function App() { input: [ { field: "id", config: { hidden: true } } { field: "hasDone", config: { hidden: true } } - { field: "name", config: { gridCol: 9 } } + { + field: "name" + config: { gridCol: 9 } + constraint: { minLength: 0, maxLength: 50 } + } { field: "priority", config: { gridCol: 3 } } ] ) { From 1e4b59132b6f5ddf18acaeb412204e3102d65435 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 16:12:19 +0900 Subject: [PATCH 17/37] Fallback to empty record --- packages/fabrix/src/directive/schema.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index 224e96d4..533ab3d3 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -107,7 +107,9 @@ export const directiveSchemaMap = { hidden: defaultValues.hidden, }), ), - constraint: formFieldConstraintSchema.nullish(), + constraint: formFieldConstraintSchema + .nullish() + .transform(fallbackDefault({})), }), ), }), From cb46394be4089c106540ec6988ca07366995eb9a Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 16:15:04 +0900 Subject: [PATCH 18/37] Curved out merger --- packages/fabrix/src/inferer.ts | 44 +++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/fabrix/src/inferer.ts b/packages/fabrix/src/inferer.ts index 37bc6261..05a6eced 100644 --- a/packages/fabrix/src/inferer.ts +++ b/packages/fabrix/src/inferer.ts @@ -16,10 +16,31 @@ export type FieldWithDirective< type DirectiveInput< C extends Record = Record, -> = Array<{ +> = { field: Path; config: C; -}>; +}; + +const merger = < + C extends Record, + M extends Record, +>( + fieldValue: FieldWithDirective | undefined, + directiveValue: DirectiveInput | undefined, +) => { + if (fieldValue && directiveValue) { + return { + config: deepmerge<[C, C]>(fieldValue.config, directiveValue.config), + meta: fieldValue.meta, + }; + } else if (fieldValue) { + return { config: fieldValue.config, meta: fieldValue.meta }; + } else if (directiveValue) { + return { config: directiveValue.config, meta: {} }; + } else { + return null; + } +}; /* * Merge the default field configs with the input field configs @@ -29,7 +50,7 @@ export const mergeFieldConfigs = < M extends Record, >( fieldConfigs: Array>, - directiveInput: DirectiveInput, + directiveInput: Array>, ) => { const allFieldKeys = new Set([ ...fieldConfigs.map((f) => f.field.asKey()), @@ -44,22 +65,7 @@ export const mergeFieldConfigs = < return []; } - const mergeConfig = () => { - if (fieldValue && directiveValue) { - return { - config: deepmerge<[C, C]>(fieldValue.config, directiveValue.config), - meta: fieldValue.meta, - }; - } else if (fieldValue) { - return { config: fieldValue.config, meta: fieldValue.meta }; - } else if (directiveValue) { - return { config: directiveValue.config, meta: {} }; - } else { - return null; - } - }; - - const mergedValue = mergeConfig(); + const mergedValue = merger(fieldValue, directiveValue); if (!mergedValue) { return []; } From 52c150cd21fc00377e3ce8d76f975546662e833e Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 16:16:13 +0900 Subject: [PATCH 19/37] Spreading --- packages/fabrix/src/inferer.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/fabrix/src/inferer.ts b/packages/fabrix/src/inferer.ts index 05a6eced..619c8559 100644 --- a/packages/fabrix/src/inferer.ts +++ b/packages/fabrix/src/inferer.ts @@ -72,8 +72,7 @@ export const mergeFieldConfigs = < return { field, - config: mergedValue.config, - meta: mergedValue.meta, + ...mergedValue, }; }); }; From bbf655f80b2dfd653772db5ebbe393940a1b9d20 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 17:20:27 +0900 Subject: [PATCH 20/37] Implement pluggable merger --- packages/fabrix/src/directive/schema.ts | 4 +- packages/fabrix/src/inferer.ts | 150 ----------------------- packages/fabrix/src/readers/field.ts | 49 ++++++++ packages/fabrix/src/readers/form.ts | 91 ++++++++++++++ packages/fabrix/src/readers/shared.ts | 64 ++++++++++ packages/fabrix/src/renderer.tsx | 10 +- packages/fabrix/src/renderers/fields.tsx | 2 +- packages/fabrix/src/renderers/form.tsx | 7 +- packages/fabrix/src/renderers/shared.tsx | 51 ++++---- 9 files changed, 244 insertions(+), 184 deletions(-) delete mode 100644 packages/fabrix/src/inferer.ts create mode 100644 packages/fabrix/src/readers/field.ts create mode 100644 packages/fabrix/src/readers/form.ts create mode 100644 packages/fabrix/src/readers/shared.ts diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index 533ab3d3..224e96d4 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -107,9 +107,7 @@ export const directiveSchemaMap = { hidden: defaultValues.hidden, }), ), - constraint: formFieldConstraintSchema - .nullish() - .transform(fallbackDefault({})), + constraint: formFieldConstraintSchema.nullish(), }), ), }), diff --git a/packages/fabrix/src/inferer.ts b/packages/fabrix/src/inferer.ts deleted file mode 100644 index 619c8559..00000000 --- a/packages/fabrix/src/inferer.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { FabrixContextType } from "@context"; -import { formFieldSchema, viewFieldSchema } from "@directive/schema"; -import { resolveFieldType } from "@renderers/shared"; -import { Fields, FieldVariables, Path } from "@visitor"; -import { deepmerge } from "deepmerge-ts"; -import { GraphQLInputObjectType, GraphQLNonNull } from "graphql"; - -export type FieldWithDirective< - C extends Record = Record, - M extends Record = Record, -> = { - field: Path; - config: C; - meta: M; -}; - -type DirectiveInput< - C extends Record = Record, -> = { - field: Path; - config: C; -}; - -const merger = < - C extends Record, - M extends Record, ->( - fieldValue: FieldWithDirective | undefined, - directiveValue: DirectiveInput | undefined, -) => { - if (fieldValue && directiveValue) { - return { - config: deepmerge<[C, C]>(fieldValue.config, directiveValue.config), - meta: fieldValue.meta, - }; - } else if (fieldValue) { - return { config: fieldValue.config, meta: fieldValue.meta }; - } else if (directiveValue) { - return { config: directiveValue.config, meta: {} }; - } else { - return null; - } -}; - -/* - * Merge the default field configs with the input field configs - */ -export const mergeFieldConfigs = < - C extends Record, - M extends Record, ->( - fieldConfigs: Array>, - directiveInput: Array>, -) => { - const allFieldKeys = new Set([ - ...fieldConfigs.map((f) => f.field.asKey()), - ...directiveInput.map((f) => f.field.asKey()), - ]); - - return Array.from(allFieldKeys).flatMap((key) => { - const directiveValue = directiveInput.find((f) => f.field.asKey() === key); - const fieldValue = fieldConfigs.find((f) => f.field.asKey() === key); - const field = directiveValue?.field || fieldValue?.field; - if (!field) { - return []; - } - - const mergedValue = merger(fieldValue, directiveValue); - if (!mergedValue) { - return []; - } - - return { - field, - ...mergedValue, - }; - }); -}; - -/** - * Infer the field configuration from the fields - */ -export const buildDefaultViewFieldConfigs = (fields: Fields) => - fields.unwrap().flatMap((field) => { - const config = viewFieldSchema.parse({ - index: 0, - label: field.getName(), - }); - - // This field configs are used to be merged into the field configs on directives - // So we need to skip the root path here. - const path = field.value.path.rootOffset(1); - if (!path) { - return []; - } - - return { - field: path, - config, - meta: {}, - }; - }); - -/** - * Infer the field configuration from the input object type for the form - */ -export const buildDefaultFormFieldConfigs = ( - context: FabrixContextType, - fieldVariables: FieldVariables, -) => { - if (!("input" in fieldVariables)) { - return []; - } - - if (context.schemaLoader.status === "loading") { - return []; - } - - const inputType = context.schemaLoader.schemaSet.serverSchema.getType( - fieldVariables.input.type, - ); - if (!inputType) { - return []; - } - - // Only support object type for "input" argument - if (!(inputType instanceof GraphQLInputObjectType)) { - return []; - } - - const fields = inputType.getFields(); - return Object.keys(fields).map((key, index) => { - const field = fields[key]; - const path = new Path(key.split(".")); - - return { - field: path, - meta: { - fieldType: resolveFieldType( - field.type instanceof GraphQLNonNull ? field.type.ofType : field.type, - ), - isRequired: field.type instanceof GraphQLNonNull, - }, - config: formFieldSchema.parse({ - index, - label: field.name, - }), - }; - }); -}; diff --git a/packages/fabrix/src/readers/field.ts b/packages/fabrix/src/readers/field.ts new file mode 100644 index 00000000..5bf35b2a --- /dev/null +++ b/packages/fabrix/src/readers/field.ts @@ -0,0 +1,49 @@ +import { viewFieldSchema } from "@directive/schema"; +import { FieldWithDirective, DirectiveInput } from "@readers/shared"; +import { Fields } from "@visitor"; +import { deepmerge } from "deepmerge-ts"; + +/** + * Infer the field configuration from the fields + */ +export const buildDefaultViewFieldConfigs = (fields: Fields) => + fields.unwrap().flatMap((field) => { + const config = viewFieldSchema.parse({ + index: 0, + label: field.getName(), + }); + + // This field configs are used to be merged into the field configs on directives + // So we need to skip the root path here. + const path = field.value.path.rootOffset(1); + if (!path) { + return []; + } + + return { + field: path, + config, + meta: {}, + }; + }); + +export const viewFieldMerger = < + C extends Record, + M extends Record, +>( + fieldValue: FieldWithDirective | undefined, + directiveValue: DirectiveInput | undefined, +) => { + if (fieldValue && directiveValue) { + return { + config: deepmerge(fieldValue.config, directiveValue.config), + meta: fieldValue.meta, + }; + } else if (fieldValue) { + return { config: fieldValue.config, meta: fieldValue.meta }; + } else if (directiveValue) { + return { config: directiveValue.config, meta: null }; + } else { + return null; + } +}; diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts new file mode 100644 index 00000000..0a8eeee4 --- /dev/null +++ b/packages/fabrix/src/readers/form.ts @@ -0,0 +1,91 @@ +import { FabrixContextType } from "@context"; +import { formFieldSchema } from "@directive/schema"; +import { DirectiveInput, FieldWithDirective } from "@readers/shared"; +import { resolveFieldType } from "@renderers/shared"; +import { FieldVariables, Path } from "@visitor"; +import { deepmerge } from "deepmerge-ts"; +import { GraphQLInputObjectType, GraphQLNonNull } from "graphql"; + +/** + * Infer the field configuration from the input object type for the form + */ +export const buildDefaultFormFieldConfigs = ( + context: FabrixContextType, + fieldVariables: FieldVariables, +) => { + if (!("input" in fieldVariables)) { + return []; + } + + if (context.schemaLoader.status === "loading") { + return []; + } + + const inputType = context.schemaLoader.schemaSet.serverSchema.getType( + fieldVariables.input.type, + ); + if (!inputType) { + return []; + } + + // Only support object type for "input" argument + if (!(inputType instanceof GraphQLInputObjectType)) { + return []; + } + + const fields = inputType.getFields(); + return Object.keys(fields).map((key, index) => { + const field = fields[key]; + const path = new Path(key.split(".")); + + return { + field: path, + meta: { + fieldType: resolveFieldType( + field.type instanceof GraphQLNonNull ? field.type.ofType : field.type, + ), + isRequired: field.type instanceof GraphQLNonNull, + }, + config: formFieldSchema.parse({ + index, + label: field.name, + }), + }; + }); +}; + +export const formFieldMerger = < + C extends Record, + M extends Record, +>( + fieldValue: FieldWithDirective | undefined, + directiveValue: + | DirectiveInput< + C, + { + constraint?: Record | null; + } + > + | undefined, +) => { + if (fieldValue && directiveValue) { + return { + config: deepmerge(fieldValue.config, directiveValue.config), + meta: fieldValue.meta, + constraint: directiveValue.constraint, + }; + } else if (fieldValue) { + return { + config: fieldValue.config, + meta: fieldValue.meta, + }; + } else if (directiveValue) { + return { + config: directiveValue.config, + meta: null, + constraint: directiveValue.constraint, + }; + } else { + return null; + } +}; diff --git a/packages/fabrix/src/readers/shared.ts b/packages/fabrix/src/readers/shared.ts new file mode 100644 index 00000000..65200c5a --- /dev/null +++ b/packages/fabrix/src/readers/shared.ts @@ -0,0 +1,64 @@ +import { Path } from "@visitor"; + +export type FieldWithDirective< + C extends Record = Record, + M extends Record = Record, +> = { + field: Path; + config: C; + meta: M | null; +}; + +export type DirectiveInput< + C extends Record = Record, + E extends Record = Record, +> = { + field: Path; + config: C; +} & E; + +type Merger< + C extends Record, + M extends Record, + E extends Record, +> = ( + fieldValue: FieldWithDirective | undefined, + directiveValue: DirectiveInput | undefined, +) => Omit & E, "field"> | null; + +/* + * Merge the default field configs with the input field configs + */ +export const mergeFieldConfigs = < + C extends Record, + M extends Record, + E extends Record, +>( + fieldConfigs: Array>, + directiveInput: Array>, + merger: Merger, +) => { + const allFieldKeys = new Set([ + ...fieldConfigs.map((f) => f.field.asKey()), + ...directiveInput.map((f) => f.field.asKey()), + ]); + + return Array.from(allFieldKeys).flatMap((key) => { + const directiveValue = directiveInput.find((f) => f.field.asKey() === key); + const fieldValue = fieldConfigs.find((f) => f.field.asKey() === key); + const field = directiveValue?.field || fieldValue?.field; + if (!field) { + return []; + } + + const mergedValue = merger(fieldValue, directiveValue); + if (!mergedValue) { + return []; + } + + return { + field, + ...mergedValue, + }; + }); +}; diff --git a/packages/fabrix/src/renderer.tsx b/packages/fabrix/src/renderer.tsx index 837e8926..3e5ee2b9 100644 --- a/packages/fabrix/src/renderer.tsx +++ b/packages/fabrix/src/renderer.tsx @@ -10,13 +10,11 @@ import { CommonFabrixComponentRendererProps, } from "@renderers/shared"; import { directiveSchemaMap } from "@directive/schema"; -import { - buildDefaultViewFieldConfigs, - buildDefaultFormFieldConfigs, - mergeFieldConfigs, -} from "@inferer"; +import { mergeFieldConfigs } from "@readers/shared"; import { buildRootDocument, Field, Fields, FieldVariables } from "@/visitor"; import { FabrixComponentData } from "@/fetcher"; +import { buildDefaultViewFieldConfigs, viewFieldMerger } from "@readers/field"; +import { buildDefaultFormFieldConfigs, formFieldMerger } from "@readers/form"; const decideStrategy = ( directiveNodes: readonly DirectiveNode[], @@ -73,6 +71,7 @@ const getFieldConfig = ( fields: mergeFieldConfigs( buildDefaultViewFieldConfigs(childFields), directive.input, + viewFieldMerger, ), }, }; @@ -100,6 +99,7 @@ const getFieldConfig = ( fields: mergeFieldConfigs( buildDefaultFormFieldConfigs(context, fieldVariables), directive.input, + formFieldMerger, ), }, }; diff --git a/packages/fabrix/src/renderers/fields.tsx b/packages/fabrix/src/renderers/fields.tsx index 8f19f103..97286cf7 100644 --- a/packages/fabrix/src/renderers/fields.tsx +++ b/packages/fabrix/src/renderers/fields.tsx @@ -1,6 +1,6 @@ import { createElement, useCallback, useMemo } from "react"; import { ViewFieldSchema } from "@directive/schema"; -import { FieldWithDirective } from "@inferer"; +import { FieldWithDirective } from "@readers/shared"; import { FabrixContextType } from "@context"; import { useDataFetch, Value } from "../fetcher"; import { RendererCommonProps } from "../renderer"; diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 4c40a749..7d4814ba 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -2,11 +2,12 @@ import { createElement, useCallback } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; -import { FieldWithDirective } from "@inferer"; +import { FieldWithDirective } from "@readers/shared"; import { FabrixContextType } from "../context"; import { buildClassName, CommonFabrixComponentRendererProps, + defaultFieldType, FieldType, getFieldConfigByKey, Loader, @@ -141,9 +142,9 @@ const renderField = (props: { return createElement(component, { key: indexKey, value: fieldConfig.defaultValue, - type: field.meta.fieldType, + type: field.meta?.fieldType ?? defaultFieldType, name: field.field.asKey(), - isRequired: field.meta.isRequired, + isRequired: field.meta?.isRequired ?? false, attributes: { className, label: fieldConfig.label, diff --git a/packages/fabrix/src/renderers/shared.tsx b/packages/fabrix/src/renderers/shared.tsx index 98d8bbaf..b4bff69d 100644 --- a/packages/fabrix/src/renderers/shared.tsx +++ b/packages/fabrix/src/renderers/shared.tsx @@ -10,7 +10,7 @@ import { } from "graphql"; import { DirectiveAttributes } from "@registry"; import { FabrixContextType } from "@context"; -import { FieldWithDirective } from "@inferer"; +import { FieldWithDirective } from "@readers/shared"; import { FabrixComponentData } from "../fetcher"; type FabrixComponentFieldsRendererExtraProps = Partial & { @@ -152,27 +152,34 @@ export const resolveFieldTypesFromTypename = ( }, {}); }; -export type FieldType = - | { - type: "Scalar"; - name: string; - } - | { - type: "Enum"; - name: string; - meta: { - values: string[]; - }; - } - | { - type: "Object"; - name: string; - } - | { - type: "List"; - innerType: NonNullable; - } - | null; +type ScalarType = { + type: "Scalar"; + name: string; +}; + +type EnumType = { + type: "Enum"; + name: string; + meta: { + values: string[]; + }; +}; + +type ObjectType = { + type: "Object"; + name: string; +}; + +type ListType = { + type: "List"; + innerType: NonNullable; +}; + +export type FieldType = ScalarType | EnumType | ObjectType | ListType | null; +export const defaultFieldType = { + type: "Scalar" as const, + name: "String", +}; export const resolveFieldType = ( field: GraphQLOutputType | GraphQLNullableType, From 0460ad39387b604c64498728f8f7a71a92ec734e Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Fri, 18 Oct 2024 17:26:46 +0900 Subject: [PATCH 21/37] Fix lint errors --- packages/fabrix/src/renderer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fabrix/src/renderer.tsx b/packages/fabrix/src/renderer.tsx index 3e5ee2b9..2a64e064 100644 --- a/packages/fabrix/src/renderer.tsx +++ b/packages/fabrix/src/renderer.tsx @@ -11,10 +11,10 @@ import { } from "@renderers/shared"; import { directiveSchemaMap } from "@directive/schema"; import { mergeFieldConfigs } from "@readers/shared"; -import { buildRootDocument, Field, Fields, FieldVariables } from "@/visitor"; -import { FabrixComponentData } from "@/fetcher"; import { buildDefaultViewFieldConfigs, viewFieldMerger } from "@readers/field"; import { buildDefaultFormFieldConfigs, formFieldMerger } from "@readers/form"; +import { buildRootDocument, Field, Fields, FieldVariables } from "@/visitor"; +import { FabrixComponentData } from "@/fetcher"; const decideStrategy = ( directiveNodes: readonly DirectiveNode[], From 8c6d76c66c6de3981d78f108a491757f3f4745e3 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 11:48:05 +0900 Subject: [PATCH 22/37] Fix lockfile --- pnpm-lock.yaml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 425d655e..33366c48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,7 +235,7 @@ importers: version: 18.3.1 '@vitejs/plugin-react': specifier: ^4.3.3 - version: 4.3.3(vite@5.4.6(@types/node@22.7.5)) + version: 4.3.3(vite@5.4.8(@types/node@22.7.5)) eslint: specifier: ^9.6.0 version: 9.12.0 @@ -311,7 +311,7 @@ importers: version: 9.1.0(eslint@9.12.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0) + version: 2.31.0(eslint@9.12.0) typescript: specifier: ^5 version: 5.6.3 @@ -4815,17 +4815,6 @@ snapshots: transitivePeerDependencies: - graphql - '@vitejs/plugin-react@4.3.3(vite@5.4.6(@types/node@22.7.5))': - dependencies: - '@babel/core': 7.25.2 - '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) - '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) - '@types/babel__core': 7.20.5 - react-refresh: 0.14.2 - vite: 5.4.6(@types/node@22.7.5) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.3.3(vite@5.4.8(@types/node@22.7.5))': dependencies: '@babel/core': 7.25.2 @@ -5482,17 +5471,16 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): + eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0): + eslint-plugin-import@2.31.0(eslint@9.12.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5503,7 +5491,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) + eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -5514,8 +5502,6 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack From c262456bc1a81cac9a5beaae7a57db2e8906f511 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 15:51:49 +0900 Subject: [PATCH 23/37] Export constraint schema --- packages/fabrix/src/directive/schema.ts | 36 +++++++++++++------------ 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index 224e96d4..79214294 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -35,23 +35,25 @@ export const formFieldSchema = baseFieldSchema.merge( }), ); -const formFieldConstraintSchema = z.object({ - // String constraints - minLength: z.number().nullish(), - maxLength: z.number().nullish(), - startsWith: z.string().nullish(), - endsWith: z.string().nullish(), - contains: z.string().nullish(), - notContais: z.string().nullish(), - pattern: z.string().nullish(), - format: z.string().nullish(), +export const formFieldConstraintSchema = z + .object({ + // String constraints + minLength: z.number().nullish(), + maxLength: z.number().nullish(), + startsWith: z.string().nullish(), + endsWith: z.string().nullish(), + contains: z.string().nullish(), + notContais: z.string().nullish(), + pattern: z.string().nullish(), + format: z.string().nullish(), - // Number constraints - min: z.number().nullish(), - max: z.number().nullish(), - exclusiveMin: z.number().nullish(), - exclusiveMax: z.number().nullish(), -}); + // Number constraints + min: z.number().nullish(), + max: z.number().nullish(), + exclusiveMin: z.number().nullish(), + exclusiveMax: z.number().nullish(), + }) + .nullish(); export const viewFieldSchema = baseFieldSchema.merge( z.object({ @@ -107,7 +109,7 @@ export const directiveSchemaMap = { hidden: defaultValues.hidden, }), ), - constraint: formFieldConstraintSchema.nullish(), + constraint: formFieldConstraintSchema, }), ), }), From 73167011276408ddcb5bf4e72f65d7ba0e39251b Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 15:53:37 +0900 Subject: [PATCH 24/37] Remove an unused function --- packages/fabrix/src/directive.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/packages/fabrix/src/directive.ts b/packages/fabrix/src/directive.ts index de53bb24..9ecbae98 100644 --- a/packages/fabrix/src/directive.ts +++ b/packages/fabrix/src/directive.ts @@ -14,35 +14,6 @@ export const parseDirectiveArguments = ( return parsedValue.data; }; -type FieldConfig = { - config: C; - field?: string | null; -}; - -/** - * A helper function to build a record from object array that has `field` key - */ -export const buildRecordByFieldName = < - C extends Record, - V extends FieldConfig = FieldConfig, - T extends Array = Array, ->( - input: T, -) => { - return input.reduce>( - (acc, value) => - value.field - ? { - ...acc, - [value.field]: { - config: value.config, - }, - } - : acc, - {}, - ); -}; - export const findDirective = ( directives: ReadonlyArray | undefined, ) => { From 5815e1433877d0ea14738eb3bb03607ff47125ac Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 16:23:21 +0900 Subject: [PATCH 25/37] Unabstract meta field --- packages/fabrix/src/readers/field.ts | 9 +++------ packages/fabrix/src/readers/form.ts | 7 ++----- packages/fabrix/src/readers/shared.ts | 19 +++++++++++-------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/packages/fabrix/src/readers/field.ts b/packages/fabrix/src/readers/field.ts index 5bf35b2a..3b717ac1 100644 --- a/packages/fabrix/src/readers/field.ts +++ b/packages/fabrix/src/readers/field.ts @@ -23,15 +23,12 @@ export const buildDefaultViewFieldConfigs = (fields: Fields) => return { field: path, config, - meta: {}, + meta: null, }; }); -export const viewFieldMerger = < - C extends Record, - M extends Record, ->( - fieldValue: FieldWithDirective | undefined, +export const viewFieldMerger = >( + fieldValue: FieldWithDirective | undefined, directiveValue: DirectiveInput | undefined, ) => { if (fieldValue && directiveValue) { diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts index 0a8eeee4..5567879e 100644 --- a/packages/fabrix/src/readers/form.ts +++ b/packages/fabrix/src/readers/form.ts @@ -54,11 +54,8 @@ export const buildDefaultFormFieldConfigs = ( }); }; -export const formFieldMerger = < - C extends Record, - M extends Record, ->( - fieldValue: FieldWithDirective | undefined, +export const formFieldMerger = >( + fieldValue: FieldWithDirective | undefined, directiveValue: | DirectiveInput< C, diff --git a/packages/fabrix/src/readers/shared.ts b/packages/fabrix/src/readers/shared.ts index 65200c5a..9fad377f 100644 --- a/packages/fabrix/src/readers/shared.ts +++ b/packages/fabrix/src/readers/shared.ts @@ -1,12 +1,17 @@ +import { FieldType } from "@renderers/shared"; import { Path } from "@visitor"; +type FieldMeta = { + fieldType: FieldType; + isRequired: boolean; +} | null; + export type FieldWithDirective< C extends Record = Record, - M extends Record = Record, > = { field: Path; config: C; - meta: M | null; + meta: FieldMeta; }; export type DirectiveInput< @@ -19,24 +24,22 @@ export type DirectiveInput< type Merger< C extends Record, - M extends Record, E extends Record, > = ( - fieldValue: FieldWithDirective | undefined, + fieldValue: FieldWithDirective | undefined, directiveValue: DirectiveInput | undefined, -) => Omit & E, "field"> | null; +) => Omit & E, "field"> | null; /* * Merge the default field configs with the input field configs */ export const mergeFieldConfigs = < C extends Record, - M extends Record, E extends Record, >( - fieldConfigs: Array>, + fieldConfigs: Array>, directiveInput: Array>, - merger: Merger, + merger: Merger, ) => { const allFieldKeys = new Set([ ...fieldConfigs.map((f) => f.field.asKey()), From 88f7c95021c16959b5c56e0bdeb433903c515f3f Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 16:33:45 +0900 Subject: [PATCH 26/37] Add buildFieldMeta function --- packages/fabrix/src/readers/form.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts index 5567879e..dd631c50 100644 --- a/packages/fabrix/src/readers/form.ts +++ b/packages/fabrix/src/readers/form.ts @@ -1,10 +1,23 @@ import { FabrixContextType } from "@context"; import { formFieldSchema } from "@directive/schema"; -import { DirectiveInput, FieldWithDirective } from "@readers/shared"; +import { FieldConfig, FieldConfigWithMeta } from "@readers/shared"; import { resolveFieldType } from "@renderers/shared"; import { FieldVariables, Path } from "@visitor"; import { deepmerge } from "deepmerge-ts"; -import { GraphQLInputObjectType, GraphQLNonNull } from "graphql"; +import { + GraphQLInputObjectType, + GraphQLInputType, + GraphQLNonNull, +} from "graphql"; + +const buildFieldMeta = (type: GraphQLInputType) => ({ + fieldType: resolveFieldType( + type instanceof GraphQLNonNull ? type.ofType : type, + ), + isRequired: type instanceof GraphQLNonNull, +}); + +export type FieldMeta = ReturnType | null; /** * Infer the field configuration from the input object type for the form @@ -40,12 +53,7 @@ export const buildDefaultFormFieldConfigs = ( return { field: path, - meta: { - fieldType: resolveFieldType( - field.type instanceof GraphQLNonNull ? field.type.ofType : field.type, - ), - isRequired: field.type instanceof GraphQLNonNull, - }, + meta: buildFieldMeta(field.type), config: formFieldSchema.parse({ index, label: field.name, From 73b9b72243302d1dd7a315d24e2271053e67de5c Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 16:36:30 +0900 Subject: [PATCH 27/37] Fix type errors --- packages/fabrix/src/readers/form.ts | 2 +- packages/fabrix/src/renderers/shared.tsx | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts index dd631c50..d927e72f 100644 --- a/packages/fabrix/src/readers/form.ts +++ b/packages/fabrix/src/readers/form.ts @@ -1,6 +1,5 @@ import { FabrixContextType } from "@context"; import { formFieldSchema } from "@directive/schema"; -import { FieldConfig, FieldConfigWithMeta } from "@readers/shared"; import { resolveFieldType } from "@renderers/shared"; import { FieldVariables, Path } from "@visitor"; import { deepmerge } from "deepmerge-ts"; @@ -9,6 +8,7 @@ import { GraphQLInputType, GraphQLNonNull, } from "graphql"; +import { FieldWithDirective, DirectiveInput } from "./shared"; const buildFieldMeta = (type: GraphQLInputType) => ({ fieldType: resolveFieldType( diff --git a/packages/fabrix/src/renderers/shared.tsx b/packages/fabrix/src/renderers/shared.tsx index b4bff69d..3f81aa71 100644 --- a/packages/fabrix/src/renderers/shared.tsx +++ b/packages/fabrix/src/renderers/shared.tsx @@ -93,11 +93,8 @@ export const assertObjectValue: ( export type FieldTypes = ReturnType; -export const getFieldConfigByKey = < - C extends Record = Record, - M extends Record = Record, ->( - fields: Array>, +export const getFieldConfigByKey = >( + fields: Array>, name: string, ) => fields.find((f) => f.field.asKey() == name); From bfb074a5068bcda505284c36dacb06271e56525b Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 16:38:52 +0900 Subject: [PATCH 28/37] Rename types --- packages/fabrix/src/readers/field.ts | 6 ++--- packages/fabrix/src/readers/form.ts | 6 ++--- packages/fabrix/src/readers/shared.ts | 30 ++++++++++++------------ packages/fabrix/src/renderers/fields.tsx | 4 ++-- packages/fabrix/src/renderers/form.tsx | 12 ++-------- packages/fabrix/src/renderers/shared.tsx | 4 ++-- 6 files changed, 27 insertions(+), 35 deletions(-) diff --git a/packages/fabrix/src/readers/field.ts b/packages/fabrix/src/readers/field.ts index 3b717ac1..794526f2 100644 --- a/packages/fabrix/src/readers/field.ts +++ b/packages/fabrix/src/readers/field.ts @@ -1,5 +1,5 @@ import { viewFieldSchema } from "@directive/schema"; -import { FieldWithDirective, DirectiveInput } from "@readers/shared"; +import { FieldConfigWithMeta, FieldConfig } from "@readers/shared"; import { Fields } from "@visitor"; import { deepmerge } from "deepmerge-ts"; @@ -28,8 +28,8 @@ export const buildDefaultViewFieldConfigs = (fields: Fields) => }); export const viewFieldMerger = >( - fieldValue: FieldWithDirective | undefined, - directiveValue: DirectiveInput | undefined, + fieldValue: FieldConfigWithMeta | undefined, + directiveValue: FieldConfig | undefined, ) => { if (fieldValue && directiveValue) { return { diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts index d927e72f..a0b05d3a 100644 --- a/packages/fabrix/src/readers/form.ts +++ b/packages/fabrix/src/readers/form.ts @@ -8,7 +8,7 @@ import { GraphQLInputType, GraphQLNonNull, } from "graphql"; -import { FieldWithDirective, DirectiveInput } from "./shared"; +import { FieldConfigWithMeta, FieldConfig } from "./shared"; const buildFieldMeta = (type: GraphQLInputType) => ({ fieldType: resolveFieldType( @@ -63,9 +63,9 @@ export const buildDefaultFormFieldConfigs = ( }; export const formFieldMerger = >( - fieldValue: FieldWithDirective | undefined, + fieldValue: FieldConfigWithMeta | undefined, directiveValue: - | DirectiveInput< + | FieldConfig< C, { constraint?: Record | null; diff --git a/packages/fabrix/src/readers/shared.ts b/packages/fabrix/src/readers/shared.ts index 9fad377f..6a8a17e1 100644 --- a/packages/fabrix/src/readers/shared.ts +++ b/packages/fabrix/src/readers/shared.ts @@ -6,29 +6,29 @@ type FieldMeta = { isRequired: boolean; } | null; -export type FieldWithDirective< - C extends Record = Record, -> = { - field: Path; - config: C; - meta: FieldMeta; -}; - -export type DirectiveInput< - C extends Record = Record, +export type FieldConfig< + C extends Record, E extends Record = Record, > = { field: Path; config: C; } & E; +export type FieldConfigWithMeta> = + FieldConfig< + C, + { + meta: FieldMeta; + } + >; + type Merger< C extends Record, E extends Record, > = ( - fieldValue: FieldWithDirective | undefined, - directiveValue: DirectiveInput | undefined, -) => Omit & E, "field"> | null; + fieldValue: FieldConfigWithMeta | undefined, + directiveValue: FieldConfig | undefined, +) => Omit & E, "field"> | null; /* * Merge the default field configs with the input field configs @@ -37,8 +37,8 @@ export const mergeFieldConfigs = < C extends Record, E extends Record, >( - fieldConfigs: Array>, - directiveInput: Array>, + fieldConfigs: Array>, + directiveInput: Array>, merger: Merger, ) => { const allFieldKeys = new Set([ diff --git a/packages/fabrix/src/renderers/fields.tsx b/packages/fabrix/src/renderers/fields.tsx index 97286cf7..c9924bff 100644 --- a/packages/fabrix/src/renderers/fields.tsx +++ b/packages/fabrix/src/renderers/fields.tsx @@ -1,6 +1,6 @@ import { createElement, useCallback, useMemo } from "react"; import { ViewFieldSchema } from "@directive/schema"; -import { FieldWithDirective } from "@readers/shared"; +import { FieldConfigWithMeta } from "@readers/shared"; import { FabrixContextType } from "@context"; import { useDataFetch, Value } from "../fetcher"; import { RendererCommonProps } from "../renderer"; @@ -13,7 +13,7 @@ import { resolveFieldTypesFromTypename, } from "./shared"; -type ViewField = FieldWithDirective; +type ViewField = FieldConfigWithMeta; type Fields = Array; export const ViewRenderer = ( diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 7d4814ba..d547bc64 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -2,13 +2,12 @@ import { createElement, useCallback } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; -import { FieldWithDirective } from "@readers/shared"; +import { FieldConfigWithMeta } from "@readers/shared"; import { FabrixContextType } from "../context"; import { buildClassName, CommonFabrixComponentRendererProps, defaultFieldType, - FieldType, getFieldConfigByKey, Loader, } from "./shared"; @@ -21,14 +20,7 @@ const getClearedValue = (values: Record) => }; }, {}); -export type FormFieldMeta = - | { - fieldType: FieldType; - isRequired: boolean; - } - | Record; - -export type FormField = FieldWithDirective; +export type FormField = FieldConfigWithMeta; export const FormRenderer = ( props: CommonFabrixComponentRendererProps<{ diff --git a/packages/fabrix/src/renderers/shared.tsx b/packages/fabrix/src/renderers/shared.tsx index 3f81aa71..fe87a3fe 100644 --- a/packages/fabrix/src/renderers/shared.tsx +++ b/packages/fabrix/src/renderers/shared.tsx @@ -10,7 +10,7 @@ import { } from "graphql"; import { DirectiveAttributes } from "@registry"; import { FabrixContextType } from "@context"; -import { FieldWithDirective } from "@readers/shared"; +import { FieldConfigWithMeta } from "@readers/shared"; import { FabrixComponentData } from "../fetcher"; type FabrixComponentFieldsRendererExtraProps = Partial & { @@ -94,7 +94,7 @@ export const assertObjectValue: ( export type FieldTypes = ReturnType; export const getFieldConfigByKey = >( - fields: Array>, + fields: Array>, name: string, ) => fields.find((f) => f.field.asKey() == name); From b15f3eef1f4a5e32d3586aef5824ebf59585e398 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 17:48:09 +0900 Subject: [PATCH 29/37] Fix types --- packages/fabrix/src/readers/form.ts | 19 ++++++++----------- packages/fabrix/src/renderers/form.tsx | 3 ++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts index a0b05d3a..ab03d26b 100644 --- a/packages/fabrix/src/readers/form.ts +++ b/packages/fabrix/src/readers/form.ts @@ -1,5 +1,5 @@ import { FabrixContextType } from "@context"; -import { formFieldSchema } from "@directive/schema"; +import { FormFieldSchema, formFieldSchema } from "@directive/schema"; import { resolveFieldType } from "@renderers/shared"; import { FieldVariables, Path } from "@visitor"; import { deepmerge } from "deepmerge-ts"; @@ -62,16 +62,13 @@ export const buildDefaultFormFieldConfigs = ( }); }; -export const formFieldMerger = >( - fieldValue: FieldConfigWithMeta | undefined, - directiveValue: - | FieldConfig< - C, - { - constraint?: Record | null; - } - > - | undefined, +export type FormFieldExtra = { + constraint?: Record | null; +}; + +export const formFieldMerger = ( + fieldValue: FieldConfigWithMeta | undefined, + directiveValue: FieldConfig | undefined, ) => { if (fieldValue && directiveValue) { return { diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index d547bc64..41ce8cd1 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -3,6 +3,7 @@ import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; import { FieldConfigWithMeta } from "@readers/shared"; +import { FormFieldExtra } from "@readers/form"; import { FabrixContextType } from "../context"; import { buildClassName, @@ -20,7 +21,7 @@ const getClearedValue = (values: Record) => }; }, {}); -export type FormField = FieldConfigWithMeta; +export type FormField = FieldConfigWithMeta & FormFieldExtra; export const FormRenderer = ( props: CommonFabrixComponentRendererProps<{ From 67970ff9dc32175a004dab2c0bc695b250f207fd Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 19:51:01 +0900 Subject: [PATCH 30/37] Add a function to build ajv schema --- examples/todoapp/src/App.tsx | 2 +- packages/fabrix/package.json | 3 + packages/fabrix/src/directive/schema.ts | 4 -- packages/fabrix/src/readers/form.ts | 9 ++- packages/fabrix/src/renderers/form.tsx | 6 +- .../fabrix/src/renderers/form/validation.ts | 53 +++++++++++++++ .../src/schema/constraint.graphql | 4 -- pnpm-lock.yaml | 64 +++++++++++++++++-- 8 files changed, 129 insertions(+), 16 deletions(-) create mode 100644 packages/fabrix/src/renderers/form/validation.ts diff --git a/examples/todoapp/src/App.tsx b/examples/todoapp/src/App.tsx index ece8d658..ec34caca 100644 --- a/examples/todoapp/src/App.tsx +++ b/examples/todoapp/src/App.tsx @@ -24,7 +24,7 @@ function App() { { field: "name" config: { gridCol: 9 } - constraint: { minLength: 0, maxLength: 50 } + constraint: { maxLength: 5 } } { field: "priority", config: { gridCol: 3 } } ] diff --git a/packages/fabrix/package.json b/packages/fabrix/package.json index 59fb23c0..026026fd 100644 --- a/packages/fabrix/package.json +++ b/packages/fabrix/package.json @@ -27,6 +27,9 @@ "author": "", "license": "MIT", "dependencies": { + "@hookform/resolvers": "^3.9.0", + "ajv": "^8.17.1", + "ajv-errors": "^3.0.0", "deepmerge-ts": "^7.1.0", "graphql-tag": "^2.12.6", "react-hook-form": "^7.53.0", diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index 79214294..a64c7a1d 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -40,10 +40,6 @@ export const formFieldConstraintSchema = z // String constraints minLength: z.number().nullish(), maxLength: z.number().nullish(), - startsWith: z.string().nullish(), - endsWith: z.string().nullish(), - contains: z.string().nullish(), - notContais: z.string().nullish(), pattern: z.string().nullish(), format: z.string().nullish(), diff --git a/packages/fabrix/src/readers/form.ts b/packages/fabrix/src/readers/form.ts index ab03d26b..763ddbc8 100644 --- a/packages/fabrix/src/readers/form.ts +++ b/packages/fabrix/src/readers/form.ts @@ -1,5 +1,9 @@ import { FabrixContextType } from "@context"; -import { FormFieldSchema, formFieldSchema } from "@directive/schema"; +import { + formFieldConstraintSchema, + FormFieldSchema, + formFieldSchema, +} from "@directive/schema"; import { resolveFieldType } from "@renderers/shared"; import { FieldVariables, Path } from "@visitor"; import { deepmerge } from "deepmerge-ts"; @@ -8,6 +12,7 @@ import { GraphQLInputType, GraphQLNonNull, } from "graphql"; +import { z } from "zod"; import { FieldConfigWithMeta, FieldConfig } from "./shared"; const buildFieldMeta = (type: GraphQLInputType) => ({ @@ -63,7 +68,7 @@ export const buildDefaultFormFieldConfigs = ( }; export type FormFieldExtra = { - constraint?: Record | null; + constraint?: z.infer; }; export const formFieldMerger = ( diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 41ce8cd1..2a586f42 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -4,6 +4,7 @@ import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; import { FieldConfigWithMeta } from "@readers/shared"; import { FormFieldExtra } from "@readers/form"; +import { ajvResolver } from "@hookform/resolvers/ajv"; import { FabrixContextType } from "../context"; import { buildClassName, @@ -12,6 +13,7 @@ import { getFieldConfigByKey, Loader, } from "./shared"; +import { buildAjvSchema } from "./form/validation"; const getClearedValue = (values: Record) => Object.keys(values).reduce((acc, key) => { @@ -29,7 +31,9 @@ export const FormRenderer = ( }>, ) => { const { context, fieldConfigs, query, componentFieldsRenderer } = props; - const formContext = useForm(); + const formContext = useForm({ + resolver: ajvResolver(buildAjvSchema(fieldConfigs.fields)), + }); const [mutationResult, runMutation] = useMutation(query.documentResolver()); const runSubmit = useCallback(() => { diff --git a/packages/fabrix/src/renderers/form/validation.ts b/packages/fabrix/src/renderers/form/validation.ts new file mode 100644 index 00000000..8df91d53 --- /dev/null +++ b/packages/fabrix/src/renderers/form/validation.ts @@ -0,0 +1,53 @@ +import { FormField } from "@renderers/form"; + +const convertToAjvProperty = (field: FormField) => { + switch (field.meta?.fieldType?.type) { + case "Scalar": + switch (field.meta.fieldType.name) { + case "Int": + case "Float": + return { + type: "number", + maximum: field.constraint?.max, + minimum: field.constraint?.min, + exclusiveMaximum: field.constraint?.exclusiveMax, + exclusiveMinimum: field.constraint?.exclusiveMin, + } as const; + case "String": + default: + return { + type: "string", + maxLength: field.constraint?.maxLength, + minLength: field.constraint?.minLength, + format: field.constraint?.format, + pattern: field.constraint?.pattern, + } as const; + case "Boolean": + return { + type: "boolean", + } as const; + } + default: + // TODO: handle other types (e.g. object, array) + return null; + } +}; + +export const buildAjvSchema = (fields: Array) => { + const visibleFields = fields.filter((field) => !field.config.hidden); + const requiredFields = visibleFields.filter( + (field) => field.meta?.isRequired, + ); + + return { + type: "object", + properties: visibleFields.reduce((acc, field) => { + const property = convertToAjvProperty(field); + return property === null + ? acc + : { ...acc, [field.field.asKey()]: property }; + }, {}), + required: requiredFields.map((field) => field.field.asKey()), + additionalProperties: false, + } as const; +}; diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql index 2b45aa88..360698a8 100644 --- a/packages/graphql-config/src/schema/constraint.graphql +++ b/packages/graphql-config/src/schema/constraint.graphql @@ -4,10 +4,6 @@ input FabrixFormConstraint { """ minLength: Int maxLength: Int - startsWith: String - endsWith: String - contains: String - notContains: String pattern: String format: String diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33366c48..88435d4c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -175,6 +175,15 @@ importers: packages/fabrix: dependencies: + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.9.0(react-hook-form@7.53.0(react@18.3.1)) + ajv: + specifier: ^8.17.1 + version: 8.17.1 + ajv-errors: + specifier: ^3.0.0 + version: 3.0.0(ajv@8.17.1) deepmerge-ts: specifier: ^7.1.0 version: 7.1.3 @@ -311,7 +320,7 @@ importers: version: 9.1.0(eslint@9.12.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.31.0(eslint@9.12.0) + version: 2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0) typescript: specifier: ^5 version: 5.6.3 @@ -981,6 +990,11 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hookform/resolvers@3.9.0': + resolution: {integrity: sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==} + peerDependencies: + react-hook-form: ^7.0.0 + '@humanfs/core@0.19.0': resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} engines: {node: '>=18.18.0'} @@ -1541,9 +1555,17 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-errors@3.0.0: + resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} + peerDependencies: + ajv: ^8.0.1 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -2111,6 +2133,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -2515,6 +2540,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3013,6 +3041,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -4320,6 +4352,10 @@ snapshots: dependencies: graphql: 16.9.0 + '@hookform/resolvers@3.9.0(react-hook-form@7.53.0(react@18.3.1))': + dependencies: + react-hook-form: 7.53.0(react@18.3.1) + '@humanfs/core@0.19.0': {} '@humanfs/node@0.16.5': @@ -4886,6 +4922,10 @@ snapshots: acorn@8.12.1: {} + ajv-errors@3.0.0(ajv@8.17.1): + dependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4893,6 +4933,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -5471,16 +5518,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(eslint@9.12.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -5491,7 +5539,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.12.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint@9.12.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -5502,6 +5550,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -5664,6 +5714,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.0.3: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -6035,6 +6087,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -6492,6 +6546,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + requires-port@1.0.0: {} resolve-from@4.0.0: {} From 499000611a0c34bb7477d842efd2f6117d67a63b Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 20:22:33 +0900 Subject: [PATCH 31/37] Show errors --- examples/todoapp/src/App.tsx | 2 +- .../chakra-ui/src/components/default/form.tsx | 21 ++++++++++++++++- packages/fabrix/src/renderers/form.tsx | 23 ++++++++----------- .../fabrix/src/renderers/form/validation.ts | 2 +- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/examples/todoapp/src/App.tsx b/examples/todoapp/src/App.tsx index ec34caca..b8722cb6 100644 --- a/examples/todoapp/src/App.tsx +++ b/examples/todoapp/src/App.tsx @@ -24,7 +24,7 @@ function App() { { field: "name" config: { gridCol: 9 } - constraint: { maxLength: 5 } + constraint: { maxLength: 15 } } { field: "priority", config: { gridCol: 3 } } ] diff --git a/packages/chakra-ui/src/components/default/form.tsx b/packages/chakra-ui/src/components/default/form.tsx index 72e77bb1..16511ccf 100644 --- a/packages/chakra-ui/src/components/default/form.tsx +++ b/packages/chakra-ui/src/components/default/form.tsx @@ -3,7 +3,7 @@ import { FormComponentProps, FieldType, } from "@fabrix-framework/fabrix"; -import { Switch, Input, Stack, Button, Box } from "@chakra-ui/react"; +import { Text, Switch, Input, Stack, Button, Box } from "@chakra-ui/react"; import { Select } from "chakra-react-select"; import { useController } from "@fabrix-framework/fabrix/rhf"; import { LabelledHeading } from "./shared"; @@ -55,6 +55,21 @@ export const ChakraFormField = (props: FormFieldComponentProps) => { } }; +const ErrorField = (props: FormFieldComponentProps) => { + const { formState } = useController({ + name: props.name, + }); + const error = formState.errors[props.name]; + + return ( + error && ( + + {error?.message?.toString()} + + ) + ); +}; + type EnumFieldType = Extract; const MultiSelectFormField = ( @@ -106,6 +121,7 @@ const SelectFormField = ( onBlur={field.onBlur} onChange={(e) => e && field.onChange(e.value)} /> + ); }; @@ -120,6 +136,7 @@ const TextFormField = (props: FormFieldComponentProps) => { + ); }; @@ -134,6 +151,7 @@ const NumberFormField = (props: FormFieldComponentProps) => { + ); }; @@ -148,6 +166,7 @@ const BooleanFormField = (props: FormFieldComponentProps) => { + ); }; diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 2a586f42..8b09877b 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -1,4 +1,4 @@ -import { createElement, useCallback } from "react"; +import { createElement, useCallback, useMemo } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; @@ -19,7 +19,7 @@ const getClearedValue = (values: Record) => Object.keys(values).reduce((acc, key) => { return { ...acc, - [key]: null, + [key]: undefined, }; }, {}); @@ -35,19 +35,14 @@ export const FormRenderer = ( resolver: ajvResolver(buildAjvSchema(fieldConfigs.fields)), }); const [mutationResult, runMutation] = useMutation(query.documentResolver()); + const runSubmit = formContext.handleSubmit(async (input) => { + // TODO: sending values should be specifiable by the user through something like `path` + await runMutation({ + input, + }); - const runSubmit = useCallback(() => { - runMutation({ - // TODO: here should be specifiable by the user through `path` - input: formContext.getValues(), - }) - .then(() => { - formContext.reset(getClearedValue(formContext.getValues())); - }) - .catch((error) => { - throw error; - }); - }, [formContext, runMutation]); + formContext.reset(getClearedValue(formContext.getValues())); + }); const renderFields = useCallback(() => { if (componentFieldsRenderer) { diff --git a/packages/fabrix/src/renderers/form/validation.ts b/packages/fabrix/src/renderers/form/validation.ts index 8df91d53..2c358a0b 100644 --- a/packages/fabrix/src/renderers/form/validation.ts +++ b/packages/fabrix/src/renderers/form/validation.ts @@ -48,6 +48,6 @@ export const buildAjvSchema = (fields: Array) => { : { ...acc, [field.field.asKey()]: property }; }, {}), required: requiredFields.map((field) => field.field.asKey()), - additionalProperties: false, + additionalProperties: true, } as const; }; From 50697199bc1ef842d0e5ee3447fa51ce4ca15225 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 20:28:59 +0900 Subject: [PATCH 32/37] Removed unnecessary function --- packages/fabrix/src/renderers/form.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index 8b09877b..b1914325 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -1,4 +1,4 @@ -import { createElement, useCallback, useMemo } from "react"; +import { createElement, useCallback } from "react"; import { FormProvider, useForm } from "react-hook-form"; import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; @@ -15,14 +15,6 @@ import { } from "./shared"; import { buildAjvSchema } from "./form/validation"; -const getClearedValue = (values: Record) => - Object.keys(values).reduce((acc, key) => { - return { - ...acc, - [key]: undefined, - }; - }, {}); - export type FormField = FieldConfigWithMeta & FormFieldExtra; export const FormRenderer = ( @@ -41,7 +33,7 @@ export const FormRenderer = ( input, }); - formContext.reset(getClearedValue(formContext.getValues())); + formContext.reset(); }); const renderFields = useCallback(() => { From a283ab345e25b7ff7e3ce1532d40b6ad0571622c Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 20:44:58 +0900 Subject: [PATCH 33/37] Add multipleOf support --- packages/fabrix/src/directive/schema.ts | 1 + packages/fabrix/src/renderers/form/validation.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index a64c7a1d..b1a7a633 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -48,6 +48,7 @@ export const formFieldConstraintSchema = z max: z.number().nullish(), exclusiveMin: z.number().nullish(), exclusiveMax: z.number().nullish(), + multipleOf: z.number().nullish(), }) .nullish(); diff --git a/packages/fabrix/src/renderers/form/validation.ts b/packages/fabrix/src/renderers/form/validation.ts index 2c358a0b..7d20c90f 100644 --- a/packages/fabrix/src/renderers/form/validation.ts +++ b/packages/fabrix/src/renderers/form/validation.ts @@ -12,6 +12,7 @@ const convertToAjvProperty = (field: FormField) => { minimum: field.constraint?.min, exclusiveMaximum: field.constraint?.exclusiveMax, exclusiveMinimum: field.constraint?.exclusiveMin, + multipleOf: field.constraint?.multipleOf, } as const; case "String": default: From d34fcaaf122361e12c2a14f72104a48a4e8fc3c5 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Mon, 21 Oct 2024 20:45:13 +0900 Subject: [PATCH 34/37] Add schema for Float/Int --- examples/todoapp/src/App.tsx | 2 +- .../graphql-config/src/schema/constraint.graphql | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/todoapp/src/App.tsx b/examples/todoapp/src/App.tsx index b8722cb6..ec34caca 100644 --- a/examples/todoapp/src/App.tsx +++ b/examples/todoapp/src/App.tsx @@ -24,7 +24,7 @@ function App() { { field: "name" config: { gridCol: 9 } - constraint: { maxLength: 15 } + constraint: { maxLength: 5 } } { field: "priority", config: { gridCol: 3 } } ] diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql index 360698a8..df380a76 100644 --- a/packages/graphql-config/src/schema/constraint.graphql +++ b/packages/graphql-config/src/schema/constraint.graphql @@ -8,10 +8,20 @@ input FabrixFormConstraint { format: String """ - Int/Float + Int """ min: Int max: Int + exclusiveMin: Int + exclusiveMax: Int + multipleOf: Int + + """ + Float + """ + min: Float + max: Float exclusiveMin: Float exclusiveMax: Float + multipleOf: Float } From a08c29e6d1527784d1c8e2dd59b5b26b40116fe8 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Tue, 22 Oct 2024 14:31:25 +0900 Subject: [PATCH 35/37] Support oneOf --- examples/todoapp/src/App.tsx | 2 +- packages/fabrix/src/directive/schema.ts | 3 +++ packages/fabrix/src/renderers/form/validation.ts | 5 ++++- packages/graphql-config/src/schema/constraint.graphql | 3 +++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/todoapp/src/App.tsx b/examples/todoapp/src/App.tsx index ec34caca..e332036f 100644 --- a/examples/todoapp/src/App.tsx +++ b/examples/todoapp/src/App.tsx @@ -24,7 +24,7 @@ function App() { { field: "name" config: { gridCol: 9 } - constraint: { maxLength: 5 } + constraint: { maxLength: 50 } } { field: "priority", config: { gridCol: 3 } } ] diff --git a/packages/fabrix/src/directive/schema.ts b/packages/fabrix/src/directive/schema.ts index b1a7a633..7b3498f1 100644 --- a/packages/fabrix/src/directive/schema.ts +++ b/packages/fabrix/src/directive/schema.ts @@ -49,6 +49,9 @@ export const formFieldConstraintSchema = z exclusiveMin: z.number().nullish(), exclusiveMax: z.number().nullish(), multipleOf: z.number().nullish(), + + // String/Number constraints + oneOf: z.array(z.string().or(z.number())).nullish(), }) .nullish(); diff --git a/packages/fabrix/src/renderers/form/validation.ts b/packages/fabrix/src/renderers/form/validation.ts index 7d20c90f..0eba121e 100644 --- a/packages/fabrix/src/renderers/form/validation.ts +++ b/packages/fabrix/src/renderers/form/validation.ts @@ -13,16 +13,19 @@ const convertToAjvProperty = (field: FormField) => { exclusiveMaximum: field.constraint?.exclusiveMax, exclusiveMinimum: field.constraint?.exclusiveMin, multipleOf: field.constraint?.multipleOf, + enum: field.constraint?.oneOf, } as const; case "String": - default: + default: { return { type: "string", maxLength: field.constraint?.maxLength, minLength: field.constraint?.minLength, format: field.constraint?.format, pattern: field.constraint?.pattern, + enum: field.constraint?.oneOf, } as const; + } case "Boolean": return { type: "boolean", diff --git a/packages/graphql-config/src/schema/constraint.graphql b/packages/graphql-config/src/schema/constraint.graphql index df380a76..8a36731a 100644 --- a/packages/graphql-config/src/schema/constraint.graphql +++ b/packages/graphql-config/src/schema/constraint.graphql @@ -6,6 +6,7 @@ input FabrixFormConstraint { maxLength: Int pattern: String format: String + oneOf: [String] """ Int @@ -15,6 +16,7 @@ input FabrixFormConstraint { exclusiveMin: Int exclusiveMax: Int multipleOf: Int + oneOf: [Int] """ Float @@ -24,4 +26,5 @@ input FabrixFormConstraint { exclusiveMin: Float exclusiveMax: Float multipleOf: Float + oneOf: [Float] } From b9b9920088da5a42b570af4f7d140a98dd1d10e4 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 24 Oct 2024 10:06:52 +0900 Subject: [PATCH 36/37] Use ajv-formats --- packages/fabrix/package.json | 1 + packages/fabrix/src/renderers/form.tsx | 2 +- .../fabrix/src/renderers/form/ajvResolver.ts | 112 ++++++++++++++++++ pnpm-lock.yaml | 15 +++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 packages/fabrix/src/renderers/form/ajvResolver.ts diff --git a/packages/fabrix/package.json b/packages/fabrix/package.json index 026026fd..65f04b32 100644 --- a/packages/fabrix/package.json +++ b/packages/fabrix/package.json @@ -30,6 +30,7 @@ "@hookform/resolvers": "^3.9.0", "ajv": "^8.17.1", "ajv-errors": "^3.0.0", + "ajv-formats": "^3.0.1", "deepmerge-ts": "^7.1.0", "graphql-tag": "^2.12.6", "react-hook-form": "^7.53.0", diff --git a/packages/fabrix/src/renderers/form.tsx b/packages/fabrix/src/renderers/form.tsx index b1914325..7da6e68f 100644 --- a/packages/fabrix/src/renderers/form.tsx +++ b/packages/fabrix/src/renderers/form.tsx @@ -4,7 +4,6 @@ import { useMutation } from "urql"; import { FormFieldSchema } from "@directive/schema"; import { FieldConfigWithMeta } from "@readers/shared"; import { FormFieldExtra } from "@readers/form"; -import { ajvResolver } from "@hookform/resolvers/ajv"; import { FabrixContextType } from "../context"; import { buildClassName, @@ -14,6 +13,7 @@ import { Loader, } from "./shared"; import { buildAjvSchema } from "./form/validation"; +import { ajvResolver } from "./form/ajvResolver"; export type FormField = FieldConfigWithMeta & FormFieldExtra; diff --git a/packages/fabrix/src/renderers/form/ajvResolver.ts b/packages/fabrix/src/renderers/form/ajvResolver.ts new file mode 100644 index 00000000..ced58a38 --- /dev/null +++ b/packages/fabrix/src/renderers/form/ajvResolver.ts @@ -0,0 +1,112 @@ +// Implementation copy-pasted from the original (https://github.com/react-hook-form/resolvers/blob/master/ajv/src/ajv.ts) +// we would like to extends Ajv to use ajv-formats for our requirements, but built-in RHF resolvers are not extendable. + +import { toNestErrors, validateFieldsNatively } from "@hookform/resolvers"; +import Ajv, { DefinedError, JSONSchemaType, Options } from "ajv"; +import ajvErrors from "ajv-errors"; +import ajvFormats from "ajv-formats"; +import { + FieldError, + FieldValues, + ResolverOptions, + ResolverResult, + appendErrors, +} from "react-hook-form"; + +type Resolver = ( + schema: JSONSchemaType, + schemaOptions?: Options, + factoryOptions?: { mode?: "async" | "sync" }, +) => ( + values: TFieldValues, + context: TContext | undefined, + options: ResolverOptions, +) => Promise>; + +const parseErrorSchema = ( + ajvErrors: DefinedError[], + validateAllFieldCriteria: boolean, +) => { + // Ajv will return empty instancePath when require error + ajvErrors.forEach((error) => { + if (error.keyword === "required") { + error.instancePath += "/" + error.params.missingProperty; + } + }); + + return ajvErrors.reduce>((previous, error) => { + // `/deepObject/data` -> `deepObject.data` + const path = error.instancePath.substring(1).replace(/\//g, "."); + + if (!previous[path]) { + previous[path] = { + message: error.message, + type: error.keyword, + }; + } + + if (validateAllFieldCriteria) { + const types = previous[path].types; + const messages = types && types[error.keyword]; + + previous[path] = appendErrors( + path, + validateAllFieldCriteria, + previous, + error.keyword, + messages + ? ([] as string[]).concat(messages as string[], error.message || "") + : error.message, + ) as FieldError; + } + + return previous; + }, {}); +}; + +export const ajvResolver: Resolver = + (schema, schemaOptions, resolverOptions = {}) => + async (values, _, options) => { + const ajv = new Ajv( + Object.assign( + {}, + { + allErrors: true, + validateSchema: true, + }, + schemaOptions, + ), + ); + + ajvErrors(ajv); + ajvFormats(ajv); + + const validate = ajv.compile( + Object.assign( + { $async: resolverOptions && resolverOptions.mode === "async" }, + schema, + ), + ); + + const valid = validate(values); + + if (options.shouldUseNativeValidation) { + validateFieldsNatively({}, options); + } + + return Promise.resolve( + valid + ? { values, errors: {} } + : { + values: {}, + errors: toNestErrors( + parseErrorSchema( + validate.errors as DefinedError[], + !options.shouldUseNativeValidation && + options.criteriaMode === "all", + ), + options, + ), + }, + ); + }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 88435d4c..a51ed07d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: ajv-errors: specifier: ^3.0.0 version: 3.0.0(ajv@8.17.1) + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.17.1) deepmerge-ts: specifier: ^7.1.0 version: 7.1.3 @@ -1560,6 +1563,14 @@ packages: peerDependencies: ajv: ^8.0.1 + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -4926,6 +4937,10 @@ snapshots: dependencies: ajv: 8.17.1 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 From 8d5909b5ccff0d227979391331985223d1ae4711 Mon Sep 17 00:00:00 2001 From: IzumiSy Date: Thu, 24 Oct 2024 12:38:42 +0900 Subject: [PATCH 37/37] Change dir structure for tests and added tests for validation --- .../fabrix/{tests => __tests__}/mocks/data.ts | 4 +- .../{tests => __tests__}/mocks/handlers.ts | 6 +- .../{tests => __tests__}/mocks/server.ts | 0 packages/fabrix/__tests__/mutation.test.tsx | 51 ++++ .../__tests__/mutationWithValidation.test.tsx | 268 ++++++++++++++++++ .../query.test.tsx} | 59 +--- packages/fabrix/{tests => __tests__}/setup.ts | 0 .../supports}/components.tsx | 32 ++- .../{tests => __tests__/supports}/render.tsx | 0 packages/fabrix/__tests__/supports/utils.ts | 25 ++ packages/fabrix/vitest.config.ts | 4 +- 11 files changed, 382 insertions(+), 67 deletions(-) rename packages/fabrix/{tests => __tests__}/mocks/data.ts (73%) rename packages/fabrix/{tests => __tests__}/mocks/handlers.ts (94%) rename packages/fabrix/{tests => __tests__}/mocks/server.ts (100%) create mode 100644 packages/fabrix/__tests__/mutation.test.tsx create mode 100644 packages/fabrix/__tests__/mutationWithValidation.test.tsx rename packages/fabrix/{src/renders.test.tsx => __tests__/query.test.tsx} (68%) rename packages/fabrix/{tests => __tests__}/setup.ts (100%) rename packages/fabrix/{tests => __tests__/supports}/components.tsx (61%) rename packages/fabrix/{tests => __tests__/supports}/render.tsx (100%) create mode 100644 packages/fabrix/__tests__/supports/utils.ts diff --git a/packages/fabrix/tests/mocks/data.ts b/packages/fabrix/__tests__/mocks/data.ts similarity index 73% rename from packages/fabrix/tests/mocks/data.ts rename to packages/fabrix/__tests__/mocks/data.ts index df853cf9..41a4c433 100644 --- a/packages/fabrix/tests/mocks/data.ts +++ b/packages/fabrix/__tests__/mocks/data.ts @@ -4,11 +4,11 @@ export const users = [ { id: faker.string.uuid(), name: "first user", - code: "u001", + email: faker.internet.email(), }, { id: faker.string.uuid(), name: "second user", - code: "u002", + email: faker.internet.email(), }, ]; diff --git a/packages/fabrix/tests/mocks/handlers.ts b/packages/fabrix/__tests__/mocks/handlers.ts similarity index 94% rename from packages/fabrix/tests/mocks/handlers.ts rename to packages/fabrix/__tests__/mocks/handlers.ts index 9fa5836f..963ba65e 100644 --- a/packages/fabrix/tests/mocks/handlers.ts +++ b/packages/fabrix/__tests__/mocks/handlers.ts @@ -7,7 +7,7 @@ const mockSchema = buildSchema(` type User { id: ID! name: String! - code: String! + email: String! } type UsersResult { @@ -26,9 +26,9 @@ enum UserCategory { input CreateUserInput { id: ID name: String! - code: String! + email: String age: Int! - category: UserCategory! + category: UserCategory } type Mutation { diff --git a/packages/fabrix/tests/mocks/server.ts b/packages/fabrix/__tests__/mocks/server.ts similarity index 100% rename from packages/fabrix/tests/mocks/server.ts rename to packages/fabrix/__tests__/mocks/server.ts diff --git a/packages/fabrix/__tests__/mutation.test.tsx b/packages/fabrix/__tests__/mutation.test.tsx new file mode 100644 index 00000000..89b39ff8 --- /dev/null +++ b/packages/fabrix/__tests__/mutation.test.tsx @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { screen, within } from "@testing-library/react"; +import { FabrixComponent } from "@renderer"; +import { testWithUnmount } from "./supports/render"; + +describe("mutation", () => { + it("should render the form", async () => { + await testWithUnmount( + , + async () => { + const form = await screen.findByRole("form"); + expect(form).toBeInTheDocument(); + + const inputs = await within(form).findAllByRole("textbox"); + expect(inputs.length).toBe(5); + }, + ); + }); + + it("should render the form with customized labels", async () => { + await testWithUnmount( + , + async () => { + const form = await screen.findByRole("form"); + expect(form).toBeInTheDocument(); + + expect(within(form).queryByLabelText("id")).not.toBeInTheDocument(); + expect(within(form).getByLabelText("UserName")).toBeInTheDocument(); + }, + ); + }); +}); diff --git a/packages/fabrix/__tests__/mutationWithValidation.test.tsx b/packages/fabrix/__tests__/mutationWithValidation.test.tsx new file mode 100644 index 00000000..0e586b00 --- /dev/null +++ b/packages/fabrix/__tests__/mutationWithValidation.test.tsx @@ -0,0 +1,268 @@ +import { describe, expect, it } from "vitest"; +import { FabrixComponent } from "@renderer"; +import { faker } from "@faker-js/faker"; +import { testWithUnmount } from "./supports/render"; +import { findForm } from "./supports/utils"; + +describe("String", () => { + it("minLength/maxLength", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", faker.string.alpha(4)); + await form.submit(); + expect(form.getAlert("name")).toHaveTextContent( + "must NOT have fewer than 5 characters", + ); + + await form.set("name", faker.string.alpha(5)); + await form.submit(); + expect(form.getAlert("name")).not.toBeInTheDocument(); + + await form.set("name", faker.string.alpha(10)); + await form.submit(); + expect(form.getAlert("name")).not.toBeInTheDocument(); + + await form.set("name", faker.string.alpha(11)); + await form.submit(); + expect(form.getAlert("name")).toHaveTextContent( + "must NOT have more than 10 characters", + ); + }, + ); + }); + + it("pattern", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", "John Doe"); + await form.submit(); + expect(form.getAlert("name")).toHaveTextContent( + 'must match pattern "^[a-z]+$"', + ); + + await form.set("name", "johndoe"); + await form.submit(); + expect(form.getAlert("name")).not.toBeInTheDocument(); + }, + ); + }); + + it("format (email)", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("email", "john.doe"); + await form.submit(); + expect(form.getAlert("email")).toHaveTextContent( + 'must match format "email"', + ); + + await form.set("email", faker.internet.email()); + await form.submit(); + expect(form.getAlert("email")).not.toBeInTheDocument(); + }, + ); + }); + + it("oneOf", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", "EmployeeX"); + await form.submit(); + expect(form.getAlert("name")).toHaveTextContent( + "must be equal to one of the allowed value", + ); + + await form.set("name", "EmployeeA"); + await form.submit(); + expect(form.getAlert("name")).not.toBeInTheDocument(); + }, + ); + }); +}); + +describe("Int/Float", () => { + it("min/max", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", "John Doe"); + await form.set("category", "ADMIN"); + + await form.set("age", "19"); + await form.submit(); + expect(form.getAlert("age")).toHaveTextContent("must be >= 20"); + + await form.set("age", "20"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + + await form.set("age", "30"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + + await form.set("age", "31"); + await form.submit(); + expect(form.getAlert("age")).toHaveTextContent("must be <= 30"); + }, + ); + }); + + it("exclusive min/max", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", "John Doe"); + await form.set("category", "ADMIN"); + + await form.set("age", "20"); + await form.submit(); + expect(form.getAlert("age")).toHaveTextContent("must be > 20"); + + await form.set("age", "21"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + + await form.set("age", "29"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + + await form.set("age", "30"); + await form.submit(); + expect(form.getAlert("age")).toHaveTextContent("must be < 30"); + }, + ); + }); + + it("multipleOf", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", "John Doe"); + + await form.set("age", "19"); + await form.submit(); + expect(form.getAlert("age")).toHaveTextContent("must be multiple of 5"); + + await form.set("age", "20"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + }, + ); + }); + + it("oneOf", async () => { + await testWithUnmount( + , + async () => { + const form = await findForm(); + await form.set("name", "John Doe"); + await form.set("category", "ADMIN"); + + await form.set("age", "19"); + await form.submit(); + expect(form.getAlert("age")).toHaveTextContent( + "must be equal to one of the allowed value", + ); + + await form.set("age", "20"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + + await form.set("age", "30"); + await form.submit(); + expect(form.getAlert("age")).not.toBeInTheDocument(); + }, + ); + }); +}); diff --git a/packages/fabrix/src/renders.test.tsx b/packages/fabrix/__tests__/query.test.tsx similarity index 68% rename from packages/fabrix/src/renders.test.tsx rename to packages/fabrix/__tests__/query.test.tsx index 930e2caf..1b654e04 100644 --- a/packages/fabrix/src/renders.test.tsx +++ b/packages/fabrix/__tests__/query.test.tsx @@ -2,8 +2,8 @@ import { describe, expect, it } from "vitest"; import { screen, within } from "@testing-library/react"; import { FabrixComponent } from "@renderer"; import { ComponentRegistry } from "@registry"; -import { users } from "../tests/mocks/data"; -import { testWithUnmount } from "../tests/render"; +import { users } from "./mocks/data"; +import { testWithUnmount } from "./supports/render"; describe("query", () => { it("should render the table with collection", async () => { @@ -15,7 +15,7 @@ describe("query", () => { collection { id name - code + email } } } @@ -42,7 +42,7 @@ describe("query", () => { collection { id name - code + email } } } @@ -103,7 +103,7 @@ describe("query", () => { collection { id name - code + email } } } @@ -123,52 +123,3 @@ describe("query", () => { ); }); }); - -describe("mutation", () => { - it("should render the form", async () => { - await testWithUnmount( - , - async () => { - const form = await screen.findByRole("form"); - expect(form).toBeInTheDocument(); - - const inputs = await within(form).findAllByRole("textbox"); - expect(inputs.length).toBe(5); - }, - ); - }); - - it("should render the form with customized labels", async () => { - await testWithUnmount( - , - async () => { - const form = await screen.findByRole("form"); - expect(form).toBeInTheDocument(); - - expect(within(form).queryByLabelText("id")).not.toBeInTheDocument(); - expect(within(form).getByLabelText("name")).toHaveTextContent( - "UserName", - ); - }, - ); - }); -}); diff --git a/packages/fabrix/tests/setup.ts b/packages/fabrix/__tests__/setup.ts similarity index 100% rename from packages/fabrix/tests/setup.ts rename to packages/fabrix/__tests__/setup.ts diff --git a/packages/fabrix/tests/components.tsx b/packages/fabrix/__tests__/supports/components.tsx similarity index 61% rename from packages/fabrix/tests/components.tsx rename to packages/fabrix/__tests__/supports/components.tsx index 0707c98a..fcbb71d4 100644 --- a/packages/fabrix/tests/components.tsx +++ b/packages/fabrix/__tests__/supports/components.tsx @@ -6,6 +6,7 @@ import { TableComponentProps, } from "@registry"; import { ReactNode } from "react"; +import { useController } from "react-hook-form"; const fieldView = (props: FieldComponentProps) => { const { value } = props; @@ -48,13 +49,32 @@ const formView = (props: FormComponentProps) => { }; const formFieldView = (props: FormFieldComponentProps) => { + const { field, formState } = useController({ + name: props.name, + defaultValue: "", + }); + const error = formState.errors[props.name]; + const isNumber = + props.type?.type === "Scalar" && + (props.type.name === "Int" || props.type.name === "Float"); + return ( -
- - -
+
+ + { + if (e.target.value && isNumber) { + field.onChange(parseFloat(e.target.value)); + } else { + field.onChange(e.target.value); + } + }} + /> + {error &&
{error?.message?.toString()}
} +
); }; diff --git a/packages/fabrix/tests/render.tsx b/packages/fabrix/__tests__/supports/render.tsx similarity index 100% rename from packages/fabrix/tests/render.tsx rename to packages/fabrix/__tests__/supports/render.tsx diff --git a/packages/fabrix/__tests__/supports/utils.ts b/packages/fabrix/__tests__/supports/utils.ts new file mode 100644 index 00000000..e349980f --- /dev/null +++ b/packages/fabrix/__tests__/supports/utils.ts @@ -0,0 +1,25 @@ +import { screen } from "@testing-library/react"; +import { within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + +export const findForm = async () => { + const form = await screen.findByRole("form"); + const user = userEvent.setup(); + + return { + element: within(form), + set: async (label: string, value: string) => { + const input = within(form).getByLabelText(label); + await user.clear(input); + await user.type(input, value); + }, + submit: async () => { + const submit = within(form).getByRole("button"); + await user.click(submit); + }, + getAlert: (label: string) => { + const field = within(form).getByLabelText(`field:${label}`); + return within(field).queryByRole("alert"); + }, + }; +}; diff --git a/packages/fabrix/vitest.config.ts b/packages/fabrix/vitest.config.ts index a8739ab3..e2cb7e0b 100644 --- a/packages/fabrix/vitest.config.ts +++ b/packages/fabrix/vitest.config.ts @@ -5,8 +5,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [viteReact(), tsconfigPaths()], test: { - include: ["src/**/*.test.tsx"], + include: ["src/**/*.test.tsx", "__tests__/**/*.test.tsx"], environment: "happy-dom", - setupFiles: ["./tests/setup.ts"], + setupFiles: ["./__tests__/setup.ts"], }, });