From 9e2b9ebde30224d6eaf90ac2794717f040054c2a Mon Sep 17 00:00:00 2001 From: Benjamin Hohenwarter Date: Thu, 25 Jan 2024 15:13:57 +0100 Subject: [PATCH] Supported nested fields --- packages/admin/cms-admin/package.json | 2 + .../src/generator/future/generateForm.ts | 10 ++--- .../src/generator/future/generateFormField.ts | 17 +++++--- .../src/generator/future/generator.ts | 3 +- .../src/generator/future/utils/deepKeyOf.ts | 9 ++++ .../future/utils/generateFieldList.ts | 43 +++++++++++++++++++ pnpm-lock.yaml | 15 +++++++ 7 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts create mode 100644 packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts diff --git a/packages/admin/cms-admin/package.json b/packages/admin/cms-admin/package.json index d9ce1baae90..8ba7936365e 100644 --- a/packages/admin/cms-admin/package.json +++ b/packages/admin/cms-admin/package.json @@ -58,6 +58,7 @@ "lodash.isequal": "^4.0.0", "lodash.set": "^4.3.2", "mime-db": "^1.0.0", + "object-path": "^0.11.8", "p-debounce": "^4.0.0", "pluralize": "^8.0.0", "prop-types": "^15.7.2", @@ -107,6 +108,7 @@ "@types/lodash.set": "^4.3.6", "@types/mime-db": "^1.43.1", "@types/node": "^18.0.0", + "@types/object-path": "^0.11.4", "@types/pluralize": "^0.0.29", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", diff --git a/packages/admin/cms-admin/src/generator/future/generateForm.ts b/packages/admin/cms-admin/src/generator/future/generateForm.ts index eb4d4b97dd2..6513e197040 100644 --- a/packages/admin/cms-admin/src/generator/future/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/future/generateForm.ts @@ -3,6 +3,7 @@ import { IntrospectionQuery } from "graphql"; import { generateFormField } from "./generateFormField"; import { FormConfig, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; +import { generateFieldListGqlString } from "./utils/generateFieldList"; import { generateImportsCode, Imports } from "./utils/generateImportsCode"; export function generateForm( @@ -16,13 +17,12 @@ export function generateForm( const gqlQueries: Record = {}; const imports: Imports = []; + const fieldNamesFromConfig: string[] = config.fields.map((field) => field.name); + const fieldList = generateFieldListGqlString(fieldNamesFromConfig); + const fragmentName = config.fragmentName ?? `${gqlType}Form`; gqlQueries[`${instanceGqlType}FormFragment`] = ` - fragment ${fragmentName} on ${gqlType} { - title - slug - description - } + fragment ${fragmentName} on ${gqlType} ${fieldList} `; gqlQueries[`${instanceGqlType}Query`] = ` diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts index 99949aa4889..f27264de5de 100644 --- a/packages/admin/cms-admin/src/generator/future/generateFormField.ts +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -1,7 +1,8 @@ -import { IntrospectionObjectType, IntrospectionQuery } from "graphql"; +import { IntrospectionQuery } from "graphql"; import { FormConfig, FormFieldConfig, GeneratorReturn } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; +import { generateFieldListFromIntrospection } from "./utils/generateFieldList"; import { Imports } from "./utils/generateImportsCode"; export function generateFormField( @@ -17,13 +18,15 @@ export function generateFormField( const name = String(config.name); const label = config.label ?? camelCaseToHumanReadable(name); - const introspectionObject = gqlIntrospection.__schema.types.find((type) => type.kind === "OBJECT" && type.name === gqlType) as - | IntrospectionObjectType - | undefined; - if (!introspectionObject) throw new Error(`didn't find object ${gqlType} in gql introspection`); + const introspectedTypes = gqlIntrospection.__schema.types; + const introspectionObject = introspectedTypes.find((type) => type.kind === "OBJECT" && type.name === gqlType); + if (!introspectionObject || introspectionObject.kind !== "OBJECT") throw new Error(`didn't find object ${gqlType} in gql introspection`); - const introspectionField = introspectionObject.fields.find((field) => field.name === name); - if (!introspectionField) throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`); + const introspectedFields = generateFieldListFromIntrospection(gqlIntrospection, gqlType); + + const introspectionFieldWithPath = introspectedFields.find((field) => field.path === name); + if (!introspectionFieldWithPath) throw new Error(`didn't find field ${name} in gql introspection type ${gqlType}`); + const introspectionField = introspectionFieldWithPath.field; const requiredByIntrospection = introspectionField.type.kind == "NON_NULL"; diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index eb8a1d51f7c..2b79c3a3ebe 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -5,6 +5,7 @@ import { introspectionFromSchema } from "graphql"; import { basename } from "path"; import { generateForm } from "./generateForm"; +import { DeepKeyOf } from "./utils/deepKeyOf"; import { writeGenerated } from "./utils/writeGenerated"; type BlockReference = { @@ -17,7 +18,7 @@ export type FormFieldConfig = ( | { type: "staticSelect"; values?: string[] } | { type: "asyncSelect"; values?: string[] } | { type: "block"; block: BlockReference } -) & { name: keyof T; label?: string; required?: boolean }; +) & { name: DeepKeyOf; label?: string; required?: boolean }; export type FormConfig = { type: "form"; diff --git a/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts b/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts new file mode 100644 index 00000000000..e9b215cb4fd --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/deepKeyOf.ts @@ -0,0 +1,9 @@ +// https://stackoverflow.com/a/58436959 +type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]; +type Join = K extends string | number ? (P extends string | number ? `${K}${"" extends P ? "" : "."}${P}` : never) : never; + +export type DeepKeyOf = [D] extends [never] + ? never + : T extends object + ? { [K in keyof T]-?: Join> }[keyof T] + : ""; diff --git a/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts new file mode 100644 index 00000000000..c6af42f895c --- /dev/null +++ b/packages/admin/cms-admin/src/generator/future/utils/generateFieldList.ts @@ -0,0 +1,43 @@ +import { IntrospectionField, IntrospectionQuery, IntrospectionType } from "graphql"; +import objectPath from "object-path"; + +export function generateFieldListGqlString(fields: string[]) { + const fieldsObject = fields.reduce((acc, fieldName) => { + objectPath.set(acc, fieldName, true); + return acc; + }, {}); + return JSON.stringify(fieldsObject, null, 4) + .replace(/: true,/g, "") + .replace(/: true/g, "") + .replace(/"/g, "") + .replace(/:/g, ""); +} + +function fieldListFromIntrospectionTypeRecursive( + types: readonly IntrospectionType[], + type: string, + parentPath?: string, +): { path: string; field: IntrospectionField }[] { + const typeDef = types.find((typeDef) => typeDef.name === type); + if (!typeDef || typeDef.kind !== "OBJECT") return []; + + return typeDef.fields.reduce<{ path: string; field: IntrospectionField }[]>((acc, field) => { + const path = `${parentPath ? `${parentPath}.` : ""}${field.name}`; + if (field.type.kind === "OBJECT") { + const subFields = fieldListFromIntrospectionTypeRecursive(types, field.type.name, path); + acc.push(...subFields); + } else { + acc.push({ + path: path, + field: field, + }); + } + return acc; + }, []); +} +export function generateFieldListFromIntrospection( + gqlIntrospection: IntrospectionQuery, + type: string, +): { path: string; field: IntrospectionField }[] { + return fieldListFromIntrospectionTypeRecursive(gqlIntrospection.__schema.types, type); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1bb554ce5c..bf77f7cd254 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2087,6 +2087,9 @@ importers: mime-db: specifier: ^1.0.0 version: 1.52.0 + object-path: + specifier: ^0.11.8 + version: 0.11.8 p-debounce: specifier: ^4.0.0 version: 4.0.0 @@ -2226,6 +2229,9 @@ importers: '@types/node': specifier: ^18.0.0 version: 18.15.3 + '@types/object-path': + specifier: ^0.11.4 + version: 0.11.4 '@types/pluralize': specifier: ^0.0.29 version: 0.0.29 @@ -14433,6 +14439,10 @@ packages: resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==} dev: false + /@types/object-path@0.11.4: + resolution: {integrity: sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw==} + dev: true + /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} @@ -25637,6 +25647,11 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + /object-path@0.11.8: + resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} + engines: {node: '>= 10.12.0'} + dev: false + /object-visit@1.0.1: resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} engines: {node: '>=0.10.0'}