From 5a03a7aa41786315368beece69acc38686985dac Mon Sep 17 00:00:00 2001 From: Hugo FOYART <11079152+foyarash@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:33:04 +0200 Subject: [PATCH] remove all occurrences of dmmf --- packages/generator-prisma/package.json | 2 +- packages/generator-prisma/src/dmmf.ts | 49 +- packages/json-schema/src/index.ts | 16 +- packages/next-admin/package.json | 1 + packages/next-admin/src/components/Form.tsx | 4 +- .../next-admin/src/components/FormHeader.tsx | 2 - .../next-admin/src/components/MainLayout.tsx | 4 +- .../next-admin/src/components/NextAdmin.tsx | 4 +- .../src/components/inputs/ArrayField.tsx | 15 +- .../next-admin/src/context/ConfigContext.tsx | 6 +- packages/next-admin/src/handlers/resources.ts | 17 +- .../src/hooks/useSearchPaginatedResource.ts | 14 +- packages/next-admin/src/pageHandler.ts | 1 - packages/next-admin/src/types.ts | 18 +- packages/next-admin/src/utils/jsonSchema.ts | 90 ++- packages/next-admin/src/utils/prisma.ts | 216 ++++--- packages/next-admin/src/utils/props.ts | 11 +- packages/next-admin/src/utils/server.ts | 547 ++++++++++-------- yarn.lock | 5 +- 19 files changed, 554 insertions(+), 468 deletions(-) diff --git a/packages/generator-prisma/package.json b/packages/generator-prisma/package.json index 2d679b6b..7e75e3fe 100644 --- a/packages/generator-prisma/package.json +++ b/packages/generator-prisma/package.json @@ -31,7 +31,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@premieroctet/next-admin-json-schema": "workspace:*", + "@premieroctet/next-admin-json-schema": "workspace:^", "@prisma/generator-helper": "^5.20.0", "@prisma/internals": "^5.20.0", "prisma-json-schema-generator": "5.1.5" diff --git a/packages/generator-prisma/src/dmmf.ts b/packages/generator-prisma/src/dmmf.ts index b5309b4c..558a6e37 100644 --- a/packages/generator-prisma/src/dmmf.ts +++ b/packages/generator-prisma/src/dmmf.ts @@ -1,11 +1,16 @@ import type { DMMF } from "@prisma/generator-helper"; -import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; +import type { JSONSchema7 } from "json-schema"; import { injectIntoJsonSchemaDefinition, + type NextAdminJSONSchema, type NextAdminJsonSchemaDataType, type NextAdminJsonSchemaRelation, } from "@premieroctet/next-admin-json-schema"; +type DeepMutableField = { + -readonly [P in keyof T]: DeepMutableField; +}; + const isFieldDisabled = (field: DMMF.Field) => { const isDisabled = field.isReadOnly || field.isGenerated || field.isUpdatedAt; @@ -83,6 +88,17 @@ export const insertDmmfData = ( const fields = dmmfModel.fields; + if (dmmfModel.primaryKey) { + injectIntoJsonSchemaDefinition(model as NextAdminJSONSchema, { + primaryKeyField: { + name: dmmfModel.primaryKey.fields.join("_"), + fields: dmmfModel.primaryKey.fields as DeepMutableField< + typeof dmmfModel.primaryKey + >["fields"], + }, + }); + } + Object.entries(properties).forEach(([propertyName, property]) => { const dmmfField = fields.find((f) => f.name === propertyName); @@ -90,18 +106,25 @@ export const insertDmmfData = ( return; } - injectIntoJsonSchemaDefinition(model.properties![propertyName], { - primaryKey: dmmfField.isId, - kind: dmmfField.kind, - type: dmmfField.type as NextAdminJsonSchemaDataType, - disabled: isFieldDisabled(dmmfField), - isList: dmmfField.isList, - enum: - dmmfField.kind === "enum" - ? { $ref: `#/definitions/${dmmfField.type}` } - : undefined, - relation: getRelationData(dmmf, dmmfField, dmmfModel.name), - }); + const modelProperty = model.properties![propertyName]; + + if (typeof modelProperty !== "boolean") { + injectIntoJsonSchemaDefinition( + model.properties![propertyName] as NextAdminJSONSchema, + { + primaryKey: dmmfField.isId, + kind: dmmfField.kind, + type: dmmfField.type as NextAdminJsonSchemaDataType, + disabled: isFieldDisabled(dmmfField), + isList: dmmfField.isList, + enum: + dmmfField.kind === "enum" + ? { $ref: `#/definitions/${dmmfField.type}` } + : undefined, + relation: getRelationData(dmmf, dmmfField, dmmfModel.name), + } + ); + } }); }); diff --git a/packages/json-schema/src/index.ts b/packages/json-schema/src/index.ts index 6c18aa9c..f62ab0b2 100644 --- a/packages/json-schema/src/index.ts +++ b/packages/json-schema/src/index.ts @@ -1,4 +1,4 @@ -import type { JSONSchema7Definition } from "json-schema"; +import type { JSONSchema7Definition, JSONSchema7 } from "json-schema"; export type NextAdminJsonSchemaDataType = | "BigInt" @@ -19,7 +19,11 @@ export type NextAdminJsonSchemaRelation = { export type NextAdminJsonSchemaData = { primaryKey?: boolean; - kind: "scalar" | "object" | "enum" | "unsupported"; + primaryKeyField?: { + name: string; + fields?: string[]; + }; + kind?: "scalar" | "object" | "enum" | "unsupported"; type?: NextAdminJsonSchemaDataType; disabled?: boolean; isList?: boolean; @@ -29,14 +33,12 @@ export type NextAdminJsonSchemaData = { relation?: NextAdminJsonSchemaRelation; }; -declare module "json-schema" { - interface JSONSchema7 { - __nextadmin?: NextAdminJsonSchemaData; - } +export interface NextAdminJSONSchema extends JSONSchema7 { + __nextadmin?: NextAdminJsonSchemaData; } export const injectIntoJsonSchemaDefinition = ( - schema: JSONSchema7Definition, + schema: NextAdminJSONSchema, data: NextAdminJsonSchemaData ) => { if (typeof schema !== "object") { diff --git a/packages/next-admin/package.json b/packages/next-admin/package.json index 2f7f2c6d..90725f8e 100644 --- a/packages/next-admin/package.json +++ b/packages/next-admin/package.json @@ -120,6 +120,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@premieroctet/next-admin-json-schema": "workspace:^", "@testing-library/react": "^14.1.2", "@types/body-parser": "^1.19.2", "@types/jest": "^29.5.1", diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index 1b036a26..a39c24fd 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -88,7 +88,6 @@ const widgets: RjsfForm["props"]["widgets"] = { const Form = ({ data, schema, - dmmfSchema, resource, validation: validationProp, customInputs, @@ -109,7 +108,6 @@ const Form = ({ const { edit, id, ...schemas } = getSchemas( data, schema, - dmmfSchema, modelOptions?.edit?.fields as EditFieldsOptions ); const { router } = useRouterInternal(); @@ -549,7 +547,7 @@ const Form = ({ extraErrors={extraErrors} fields={fields} disabled={allDisabled} - formContext={{ isPending, dmmfSchema }} + formContext={{ isPending, schema }} templates={{ ...templates, ButtonTemplates: { SubmitButton: submitButton }, diff --git a/packages/next-admin/src/components/FormHeader.tsx b/packages/next-admin/src/components/FormHeader.tsx index 43673dd3..e67b586a 100644 --- a/packages/next-admin/src/components/FormHeader.tsx +++ b/packages/next-admin/src/components/FormHeader.tsx @@ -23,7 +23,6 @@ export default function FormHeader({ resource, data, schema, - dmmfSchema, }: FormProps) { const { t } = useI18n(); const { basePath, options } = useConfig(); @@ -36,7 +35,6 @@ export default function FormHeader({ const { edit, id } = getSchemas( data, schema, - dmmfSchema, modelOptions?.edit?.fields as EditFieldsOptions ); diff --git a/packages/next-admin/src/components/MainLayout.tsx b/packages/next-admin/src/components/MainLayout.tsx index 9bc3cfb9..aed03f0d 100644 --- a/packages/next-admin/src/components/MainLayout.tsx +++ b/packages/next-admin/src/components/MainLayout.tsx @@ -28,7 +28,7 @@ export const MainLayout = ({ options, apiBasePath, resourcesIdProperty, - dmmfSchema, + schema, }: PropsWithChildren) => { const mergedTranslations = merge({ ...defaultTranslations }, translations); const localePath = locale ? `/${locale}` : ""; @@ -39,9 +39,9 @@ export const MainLayout = ({ basePath={`${localePath}${basePath}`} isAppDir={isAppDir} apiBasePath={apiBasePath} - dmmfSchema={dmmfSchema} resource={resource} resourcesIdProperty={resourcesIdProperty!} + schema={schema} > {renderMainComponent()} diff --git a/packages/next-admin/src/components/inputs/ArrayField.tsx b/packages/next-admin/src/components/inputs/ArrayField.tsx index d00769dd..a75144ba 100644 --- a/packages/next-admin/src/components/inputs/ArrayField.tsx +++ b/packages/next-admin/src/components/inputs/ArrayField.tsx @@ -1,17 +1,20 @@ import { FieldProps } from "@rjsf/utils"; import MultiSelectWidget from "./MultiSelect/MultiSelectWidget"; import ScalarArrayField from "./ScalarArray/ScalarArrayField"; -import type { Enumeration, FormProps } from "../../types"; +import type { Enumeration, FormProps, ModelName } from "../../types"; const ArrayField = (props: FieldProps) => { const { formData, onChange, name, disabled, schema, required, formContext } = props; - const dmmfSchema = formContext.dmmfSchema as FormProps["dmmfSchema"]; + const resourceDefinition: FormProps["schema"] = formContext.schema; - const dmmfField = dmmfSchema.find((field) => field.name === name); + const field = + resourceDefinition.properties[ + name as keyof typeof resourceDefinition.properties + ]; - if (dmmfField?.kind === "scalar" && dmmfField?.isList) { + if (field?.__nextadmin?.kind === "scalar" && field?.__nextadmin?.isList) { return ( { } const options = - dmmfField?.kind === "enum" ? (schema.enum as Enumeration[]) : undefined; + field?.__nextadmin?.kind === "enum" + ? (schema.enum as Enumeration[]) + : undefined; return ( | null; + schema: Schema; }; const ConfigContext = React.createContext( @@ -24,8 +24,8 @@ type ProviderProps = { children: React.ReactNode; isAppDir?: boolean; resource?: ModelName; - dmmfSchema?: readonly Prisma.DMMF.Field[]; resourcesIdProperty: Record | null; + schema: Schema; }; export const ConfigProvider = ({ children, ...props }: ProviderProps) => { diff --git a/packages/next-admin/src/handlers/resources.ts b/packages/next-admin/src/handlers/resources.ts index 38c604e1..9d789727 100644 --- a/packages/next-admin/src/handlers/resources.ts +++ b/packages/next-admin/src/handlers/resources.ts @@ -8,6 +8,7 @@ import { ModelName, NextAdminOptions, Permission, + Schema, SubmitResourceResponse, UploadParameters, } from "../types"; @@ -16,7 +17,6 @@ import { getDataItem } from "../utils/prisma"; import { formattedFormData, getModelIdProperty, - getPrismaModelForResource, parseFormData, } from "../utils/server"; import { uncapitalize } from "../utils/tools"; @@ -51,7 +51,7 @@ type SubmitResourceParams = { body: Record; id?: string | number; options?: NextAdminOptions; - schema: any; + schema: Schema; }; export const submitResource = async ({ @@ -64,8 +64,8 @@ export const submitResource = async ({ }: SubmitResourceParams): Promise => { const { __admin_redirect: redirect, ...formValues } = body; - const dmmfSchema = getPrismaModelForResource(resource); - const parsedFormData = parseFormData(formValues, dmmfSchema?.fields!); + const schemaDefinition = schema.definitions[resource]; + const parsedFormData = parseFormData(formValues, schemaDefinition); const resourceIdField = getModelIdProperty(resource); const fields = options?.model?.[resource]?.edit?.fields as EditFieldsOptions< @@ -76,14 +76,7 @@ export const submitResource = async ({ validate(parsedFormData, fields); const { formattedData, complementaryFormattedData, errors } = - await formattedFormData( - formValues, - dmmfSchema?.fields!, - schema, - resource, - id, - fields - ); + await formattedFormData(formValues, schema, resource, id, fields); if (errors.length) { return { diff --git a/packages/next-admin/src/hooks/useSearchPaginatedResource.ts b/packages/next-admin/src/hooks/useSearchPaginatedResource.ts index 2f827efd..f2e2b9df 100644 --- a/packages/next-admin/src/hooks/useSearchPaginatedResource.ts +++ b/packages/next-admin/src/hooks/useSearchPaginatedResource.ts @@ -12,7 +12,7 @@ const useSearchPaginatedResource = ({ initialOptions, }: UseSearchPaginatedResourceParams) => { const [isPending, setIsPending] = useState(false); - const { apiBasePath, dmmfSchema, resource } = useConfig(); + const { apiBasePath, schema, resource } = useConfig(); const searchPage = useRef(1); const totalSearchedItems = useRef(0); const [allOptions, setAllOptions] = useState( @@ -23,13 +23,19 @@ const useSearchPaginatedResource = ({ const runSearch = async (query: string, resetOptions = true) => { const perPage = 25; - const fieldFromDmmf = dmmfSchema?.find((field) => field.name === fieldName); + const schemaResourceProperties = schema.definitions[resource!].properties; - if (!fieldFromDmmf) { + const schemaFieldEntry = Object.entries(schemaResourceProperties).find( + ([key]) => key === fieldName + ); + + if (!schemaFieldEntry) { return; } - const model = fieldFromDmmf.type; + const [, schemaField] = schemaFieldEntry; + + const model = schemaField.__nextadmin?.type; try { setIsPending(true); diff --git a/packages/next-admin/src/pageHandler.ts b/packages/next-admin/src/pageHandler.ts index 6dcfae52..d02a5bfd 100644 --- a/packages/next-admin/src/pageHandler.ts +++ b/packages/next-admin/src/pageHandler.ts @@ -20,7 +20,6 @@ import { getResources, schema, } from "./utils/server"; -import { getJsonSchema } from "./utils/jsonSchema"; type CreateAppHandlerParams

= { /** diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts index 3bf23bb2..cfc8c2f7 100644 --- a/packages/next-admin/src/types.ts +++ b/packages/next-admin/src/types.ts @@ -1,12 +1,12 @@ import * as OutlineIcons from "@heroicons/react/24/outline"; import { Prisma, PrismaClient } from "@prisma/client"; -import type { JSONSchema7 } from "json-schema"; import { NextApiRequest } from "next"; import { NextRequest, NextResponse } from "next/server"; import type { ChangeEvent, ReactNode } from "react"; import type { PropertyValidationError } from "./exceptions/ValidationError"; +import type { NextAdminJSONSchema } from "@premieroctet/next-admin-json-schema"; -declare type JSONSchema7Definition = JSONSchema7 & { +declare type JSONSchema7Definition = NextAdminJSONSchema & { relation?: ModelName; }; @@ -709,14 +709,14 @@ export type NextAdminOptions = { /** Type for Schema */ export type SchemaProperty = { - [P in Field]?: JSONSchema7 & { + [P in Field]?: NextAdminJSONSchema & { items?: JSONSchema7Definition; relation?: ModelName; }; }; export type SchemaModel = Partial< - Omit + Omit > & { properties: SchemaProperty; }; @@ -725,7 +725,7 @@ export type SchemaDefinitions = { [M in ModelName]: SchemaModel; }; -export type Schema = Partial> & { +export type Schema = Partial> & { definitions: SchemaDefinitions; }; @@ -809,7 +809,7 @@ export type AdminUser = { export type AdminComponentProps = { basePath: string; apiBasePath: string; - schema?: Schema; + schema: Schema; data?: ListData; resource?: ModelName; slug?: string; @@ -824,7 +824,6 @@ export type AdminComponentProps = { validation?: PropertyValidationError[]; resources?: ModelName[]; total?: number; - dmmfSchema?: readonly Prisma.DMMF.Field[]; isAppDir?: boolean; locale?: string; /** @@ -871,7 +870,7 @@ export type MainLayoutProps = Pick< | "externalLinks" | "options" | "resourcesIdProperty" - | "dmmfSchema" + | "schema" >; export type CustomUIProps = { @@ -1074,8 +1073,7 @@ export type CreateAppHandlerParams

= { export type FormProps = { data: any; - schema: any; - dmmfSchema: readonly Prisma.DMMF.Field[]; + schema: SchemaDefinitions[ModelName]; resource: ModelName; slug?: string; validation?: PropertyValidationError[]; diff --git a/packages/next-admin/src/utils/jsonSchema.ts b/packages/next-admin/src/utils/jsonSchema.ts index 0fa760de..26a58009 100644 --- a/packages/next-admin/src/utils/jsonSchema.ts +++ b/packages/next-admin/src/utils/jsonSchema.ts @@ -1,24 +1,17 @@ -import { Prisma } from "@prisma/client"; import { UiSchema } from "@rjsf/utils"; -import { EditFieldsOptions, Field, ModelName, Schema } from "../types"; +import { + EditFieldsOptions, + Field, + ModelName, + Schema, + SchemaDefinitions, +} from "../types"; export type Schemas = { schema: any; uiSchema: UiSchema; }; -export const getJsonSchema = (): Schema => { - try { - const schema = require(".next-admin/schema.json"); - - return schema as Schema; - } catch { - throw new Error( - "Schema not found, make sure you added the generator to your schema.prisma file" - ); - } -}; - function filterProperties(properties: any): Record { const filteredProperties = {}; @@ -52,8 +45,7 @@ export function getSchemaForResource(schema: any, resource: string) { export function getSchemas( data: any, - schema: any, - dmmfSchema: readonly Prisma.DMMF.Field[], + schema: SchemaDefinitions[M], editFieldsOptions?: EditFieldsOptions ): Schemas & { edit: boolean; id?: string | number } { const uiSchema: UiSchema = {}; @@ -75,39 +67,43 @@ export function getSchemas( { requiredFields: [], disabledFields: [] } ); - if (schema && dmmfSchema) { - const idProperty = dmmfSchema.find((property) => property.isId); + const properties = schema.properties!; + const idProperty = Object.keys(properties).find((property) => { + const propertyData = properties[property as keyof typeof properties]; - edit = !!data?.[idProperty?.name ?? "id"]; - id = data?.[idProperty?.name ?? "id"]; - Object.keys(schema.properties).forEach((property) => { - const dmmfProperty = dmmfSchema.find( - (dmmfProperty) => dmmfProperty.name === property - ); + if (typeof propertyData === "boolean") { + return false; + } - if ( - dmmfProperty && - requiredFields?.includes(dmmfProperty.name) && - !schema.require?.includes(dmmfProperty.name) - ) { - schema.required = [...(schema.required ?? []), dmmfProperty.name]; - } + return propertyData?.__nextadmin?.primaryKey; + }); - if ( - dmmfProperty && - (dmmfProperty.isId || - dmmfProperty.name === "createdAt" || - dmmfProperty?.isUpdatedAt || - disabledFields?.includes(dmmfProperty.name)) - ) { - edit - ? (uiSchema[property] = { - ...uiSchema[property], - "ui:disabled": true, - }) - : delete schema.properties[property]; - } - }); - } + edit = !!data?.[idProperty ?? "id"]; + id = data?.[idProperty ?? "id"]; + Object.keys(properties).forEach((property) => { + if ( + requiredFields?.includes(property) && + !schema.required?.includes(property) + ) { + schema.required = [...(schema.required ?? []), property]; + } + + if ( + properties[property as keyof typeof properties]?.__nextadmin?.disabled || + disabledFields?.includes(property) + ) { + edit + ? (uiSchema[property] = { + ...uiSchema[property], + "ui:disabled": true, + }) + : delete properties[property as keyof typeof properties]; + } + }); return { uiSchema, schema, edit, id }; } + +export const getDefinitionFromRef = (schema: Schema, ref: string) => { + const [definition] = ref.split("/").reverse(); + return schema.definitions[definition as keyof typeof schema.definitions]; +}; diff --git a/packages/next-admin/src/utils/prisma.ts b/packages/next-admin/src/utils/prisma.ts index f9ecb028..c4dda925 100644 --- a/packages/next-admin/src/utils/prisma.ts +++ b/packages/next-admin/src/utils/prisma.ts @@ -1,5 +1,9 @@ import { $Enums, Prisma, PrismaClient } from "@prisma/client"; import { cloneDeep } from "lodash"; +import type { + NextAdminJSONSchema, + NextAdminJsonSchemaData, +} from "@premieroctet/next-admin-json-schema"; import { ITEMS_PER_PAGE } from "../config"; import { EditOptions, @@ -13,6 +17,8 @@ import { NextAdminOptions, Order, PrismaListRequest, + SchemaDefinitions, + SchemaProperty, Select, } from "../types"; import { validateQuery } from "./advancedSearch"; @@ -20,15 +26,16 @@ import { enumValueForEnumType, findRelationInData, getModelIdProperty, - getPrismaModelForResource, getToStringForRelations, modelHasIdField, + schema, transformData, } from "./server"; import { capitalize, isScalar, uncapitalize } from "./tools"; +import { getDefinitionFromRef } from "./jsonSchema"; type CreateNestedWherePredicateParams = { - field: Prisma.DMMF.Field; + field: NextAdminJsonSchemaData & { name: string }; options?: NextAdminOptions; search: string; searchOptions?: Field[]; @@ -43,23 +50,26 @@ const createNestedWherePredicate = ( }: CreateNestedWherePredicateParams, acc: Record = {} ) => { - const resource = getPrismaModelForResource(field.type as ModelName); + const resource = schema.definitions[field.type as ModelName]; + const resourceProperties = resource.properties; acc[field.name] = { OR: searchOptions ?.map((searchOption) => { const [_, subFieldName, ...rest] = searchOption.toString().split("."); - const subField = resource?.fields.find( - ({ name }) => name === subFieldName - ); + const subField = + resourceProperties[subFieldName as keyof typeof resourceProperties]; if (!subField) { return null; - } else if (subField.kind === "scalar") { - if (subField.isList) { + } else if (subField.__nextadmin?.kind === "scalar") { + if (subField.__nextadmin?.isList) { let searchTerm: string | number = search; - if (subField.type !== "String" && !isNaN(Number(search))) { + if ( + subField.__nextadmin?.type !== "String" && + !isNaN(Number(search)) + ) { searchTerm = Number(search); } @@ -68,7 +78,7 @@ const createNestedWherePredicate = ( }; } - if (subField.type === "String") { + if (subField.__nextadmin?.type === "String") { // @ts-ignore const mode = Prisma?.QueryMode ? { mode: Prisma.QueryMode.insensitive } @@ -77,20 +87,20 @@ const createNestedWherePredicate = ( [subFieldName]: { contains: search, ...mode }, }; } - if (subField.type === "Int" && !isNaN(Number(search))) { + if (subField.__nextadmin?.type === "Int" && !isNaN(Number(search))) { return { [subFieldName]: Number(search) }; } - } else if (subField.kind === "object") { + } else if (subField.__nextadmin?.kind === "object") { const predicate = createNestedWherePredicate({ - field: subField, + field: { ...subField.__nextadmin, name: subFieldName }, options, search, searchOptions: [[subFieldName, ...rest].join(".")] as Field[], }); - if (subField.isList) { - predicate[subField.name] = { - some: predicate[subField.name], + if (subField.__nextadmin?.isList) { + predicate[subFieldName] = { + some: predicate[subFieldName], }; } @@ -123,67 +133,75 @@ export const createWherePredicate = ({ const searchFilter = search ? { OR: fieldsFiltered - ?.filter((field) => { + ?.filter(([, field]) => { return ( - field.kind === "scalar" || - field.kind === "enum" || - field.kind === "object" + field?.__nextadmin?.kind === "scalar" || + field?.__nextadmin?.kind === "enum" || + field?.__nextadmin?.kind === "object" ); }) - .map((field) => { - if (field.kind === "object") { + .map(([name, field]) => { + const fieldNextAdmin = field?.__nextadmin; + + if (fieldNextAdmin?.kind === "object") { return createNestedWherePredicate({ - field, + field: { ...fieldNextAdmin, name }, options, search, searchOptions: ( options?.model?.[resource]?.list?.search as string[] )?.filter((searchOption) => - searchOption?.toString().startsWith(field.name) + searchOption?.toString().startsWith(name) ) as Field[], }); } - if (field.kind === "enum") { + if (fieldNextAdmin?.kind === "enum" && fieldNextAdmin?.enum) { + const enumDefinition = getDefinitionFromRef( + schema, + fieldNextAdmin.enum.$ref + ); const enumValueForSearchTerm = enumValueForEnumType( - field.type, + enumDefinition, search ); - if (enumValueForSearchTerm) { + if (enumValueForSearchTerm && enumDefinition?.enum) { return { - [field.name]: - // @ts-expect-error - $Enums[field.type as keyof typeof $Enums][ - enumValueForSearchTerm.name - ], + [name]: enumDefinition.enum.find( + (val) => val === enumValueForSearchTerm + ), }; } } - if (field.kind === "scalar" && field.isList) { - if (field.type !== "String" && Number.isNaN(Number(search))) { + if (fieldNextAdmin?.kind === "scalar" && fieldNextAdmin?.isList) { + if ( + fieldNextAdmin?.type !== "String" && + Number.isNaN(Number(search)) + ) { return null; } return { - [field.name]: { - has: field.type === "String" ? search : Number(search), + [name]: { + has: + fieldNextAdmin?.type === "String" ? search : Number(search), }, }; } - if (field.type === "String") { + if (fieldNextAdmin?.type === "String") { // @ts-ignore const mode = Prisma?.QueryMode ? { mode: Prisma.QueryMode.insensitive } : {}; return { - [field.name]: { contains: search, ...mode }, + [name]: { contains: search, ...mode }, }; } - if (field.type === "Int" && !isNaN(Number(search))) { - return { [field.name]: Number(search) }; + if (fieldNextAdmin?.type === "Int" && !isNaN(Number(search))) { + return { [name]: Number(search) }; } return null; }) @@ -207,14 +225,17 @@ export const createWherePredicate = ({ const getFieldsFiltered = ( resource: M, options?: NextAdminOptions -): readonly Prisma.DMMF.Field[] => { - const model = getPrismaModelForResource(resource); +): [string, SchemaProperty[Field]][] => { + const model = schema.definitions[resource] as SchemaDefinitions[ModelName]; + const modelProperties = model.properties; - let fieldsFiltered = model?.fields.filter((field) => field.kind === "scalar"); + let fieldsFiltered = Object.entries(modelProperties).filter( + ([, field]) => field.__nextadmin?.kind === "scalar" + ); const list = options?.model?.[resource]?.list as ListOptions; if (list) { fieldsFiltered = list?.search - ? model?.fields.filter(({ name }) => + ? Object.entries(modelProperties).filter(([name]) => (list.search as string[])?.some((search) => { const searchNameSplit = search?.toString().split("."); @@ -224,7 +245,7 @@ const getFieldsFiltered = ( : fieldsFiltered; } - return fieldsFiltered as readonly Prisma.DMMF.Field[]; + return fieldsFiltered as Array<[string, SchemaProperty[Field]]>; }; const getWherePredicateFromQueryParams = (query: string) => { @@ -237,7 +258,8 @@ const preparePrismaListRequest = ( options?: NextAdminOptions, skipFilters: boolean = false ): PrismaListRequest => { - const model = getPrismaModelForResource(resource); + const model = schema.definitions[resource] as SchemaDefinitions[ModelName]; + const modelProperties = model.properties; const search = searchParams.get("search") || ""; const advancedSearch = searchParams.get("q") || ""; let filtersParams: string[] = []; @@ -270,30 +292,30 @@ const preparePrismaListRequest = ( (searchParams.get("sortDirection") as Prisma.SortOrder) ?? fieldSort?.direction; - const modelFieldSortParam = model?.fields.find( - ({ name }) => name === sortParam - ); + const modelFieldSortParam = + modelProperties[sortParam as keyof typeof modelProperties]; + const modelFieldNextAdminData = modelFieldSortParam?.__nextadmin; if (orderValue in Prisma.SortOrder) { if (sortParam in Prisma[`${capitalize(resource)}ScalarFieldEnum`]) { orderBy[sortParam] = orderValue; - } else if (modelFieldSortParam?.kind === "object") { - if (modelFieldSortParam.isList) { - orderBy[modelFieldSortParam.name as Field] = { + } else if (modelFieldNextAdminData?.kind === "object") { + if (modelFieldNextAdminData.isList) { + orderBy[sortParam as Field] = { _count: orderValue, }; } else { const modelFieldSortProperty = options?.model?.[resource]?.list?.fields?.[ - modelFieldSortParam.name as Field + sortParam as Field // @ts-expect-error ]?.sortBy; const resourceSortByField = modelFieldSortProperty ?? - getModelIdProperty(modelFieldSortParam.type as ModelName); + getModelIdProperty(modelFieldNextAdminData.type as ModelName); - orderBy[modelFieldSortParam.name as Field] = { + orderBy[sortParam as Field] = { [resourceSortByField]: orderValue, }; } @@ -347,13 +369,20 @@ export const optionsFromResource = async ({ ]?.relationshipSearchField; if (relationshipField) { - const targetModel = getPrismaModelForResource(args.resource); - const modelField = targetModel?.fields.find( - (field) => field.name === relationshipField - ); + const targetModel = schema.definitions[args.resource]; + + if (!targetModel) { + throw new Error(`Model ${args.resource} not found in schema`); + } + + const targetModelProperties = targetModel.properties; + const modelField = + targetModelProperties[ + relationshipField as keyof typeof targetModelProperties + ]; - if (modelField && modelField.type !== "scalar") { - args.resource = modelField.type as ModelName; + if (modelField && modelField.__nextadmin?.type !== "scalar") { + args.resource = modelField.__nextadmin?.type as ModelName; } else { console.warn( "Used relationshipSearch on a scalar field, ignoring property" @@ -457,8 +486,7 @@ export const mapDataList = ({ "resource" | "options" | "context" | "appDir" > & { fetchData: any[] }) => { const { resource, options } = args; - const dmmfSchema = getPrismaModelForResource(resource); - const data = findRelationInData(fetchData, dmmfSchema?.fields); + const data = findRelationInData(fetchData, schema.definitions[resource]); const listFields = options?.model?.[resource]?.list?.fields ?? {}; const originalData = cloneDeep(data); data.forEach((item, index) => { @@ -537,37 +565,40 @@ export const selectPayloadForModel = ( options?: EditOptions | ListOptions, level: "scalar" | "object" = "scalar" ) => { - const model = getPrismaModelForResource(resource); + const model = schema.definitions[resource] as SchemaDefinitions[ModelName]; + const properties = model.properties; const idProperty = getModelIdProperty(resource); const displayKeys = options?.display; - let selectedFields = model?.fields.reduce( - (acc, field) => { + let selectedFields = Object.entries(properties).reduce( + (acc, [name, field]) => { + const fieldNextAdmin = field.__nextadmin; + if ( - (displayKeys && displayKeys.includes(field.name as Field)) || + (displayKeys && displayKeys.includes(name as Field)) || !displayKeys ) { - if (field.kind === "object" && level === "object") { - acc[field.name] = { + if (fieldNextAdmin?.kind === "object" && level === "object") { + acc[name] = { select: selectPayloadForModel( - field.type as ModelName, + fieldNextAdmin.type as ModelName, {}, "scalar" ), }; const orderField = (options as EditOptions)?.fields?.[ - field.name as Field + name as Field // @ts-expect-error ]?.orderField; if (orderField) { - acc[field.name].orderBy = { + acc[name].orderBy = { [orderField]: "asc", }; } } else { - acc[field.name] = true; + acc[name] = true; } } return acc; @@ -582,7 +613,6 @@ export const getDataItem = async ({ prisma, resource, options, - resourceId, locale, isAppDir, @@ -592,22 +622,21 @@ export const getDataItem = async ({ isAppDir?: boolean; locale?: string; resource: M; - resourceId: string | number; }) => { - const dmmfSchema = getPrismaModelForResource(resource); const edit = options?.model?.[resource]?.edit as EditOptions; const idProperty = getModelIdProperty(resource); const select = selectPayloadForModel(resource, edit, "object"); + const schemaResourceProperties = schema.definitions[resource].properties; Object.entries(select).forEach(([key, value]) => { - const fieldTypeDmmf = dmmfSchema?.fields.find( - (field) => field.name === key - )?.type; + const fieldType = + schemaResourceProperties[key as keyof typeof schemaResourceProperties] + ?.__nextadmin?.type; - if (fieldTypeDmmf && dmmfSchema) { + if (fieldType) { const relatedResourceOptions = - options?.model?.[fieldTypeDmmf as ModelName]?.list; + options?.model?.[fieldType as ModelName]?.list; if ( // @ts-expect-error @@ -615,13 +644,13 @@ export const getDataItem = async ({ ) { if (!relatedResourceOptions?.display) { throw new Error( - `'table' display mode set for field '${key}', but no list display is setup for model ${fieldTypeDmmf}` + `'table' display mode set for field '${key}', but no list display is setup for model ${fieldType}` ); } select[key] = { select: selectPayloadForModel( - fieldTypeDmmf as ModelName, + fieldType as ModelName, relatedResourceOptions as ListOptions, "object" ), @@ -637,11 +666,11 @@ export const getDataItem = async ({ }); Object.entries(data).forEach(([key, value]) => { if (Array.isArray(value)) { - const fieldTypeDmmf = dmmfSchema?.fields.find( - (field) => field.name === key - )?.type; + const fieldType = + schemaResourceProperties[key as keyof typeof schemaResourceProperties] + ?.__nextadmin?.type; - if (fieldTypeDmmf && dmmfSchema) { + if (fieldType) { if ( // @ts-expect-error edit?.fields?.[key as Field]?.display === "table" @@ -651,7 +680,7 @@ export const getDataItem = async ({ appDir: isAppDir, fetchData: value, options, - resource: fieldTypeDmmf as ModelName, + resource: fieldType as ModelName, }); } } @@ -677,12 +706,13 @@ export const getRawData = async ({ resource: M; resourceIds: Array; }): Promise[]> => { - const modelDMMF = getPrismaModelForResource(resource); + const model = schema.definitions[resource] as SchemaDefinitions[ModelName]; + const modelProperties = model.properties; - const include = modelDMMF?.fields.reduce( - (acc, field) => { - if (field.kind === "object") { - acc[field.name] = true; + const include = Object.entries(modelProperties).reduce( + (acc, [name, field]) => { + if (field.__nextadmin?.kind === "object") { + acc[name] = true; } return acc; }, diff --git a/packages/next-admin/src/utils/props.ts b/packages/next-admin/src/utils/props.ts index c446c660..280e3462 100644 --- a/packages/next-admin/src/utils/props.ts +++ b/packages/next-admin/src/utils/props.ts @@ -17,7 +17,6 @@ import { applyVisiblePropertiesInSchema, getEnableToExecuteActions, getModelIdProperty, - getPrismaModelForResource, getResourceFromParams, getResourceIdFromParam, getResources, @@ -26,7 +25,6 @@ import { transformSchema, } from "./server"; import { extractSerializable } from "./tools"; -import { getJsonSchema } from "./jsonSchema"; enum Page { LIST = 1, @@ -45,7 +43,7 @@ export async function getPropsFromParams({ apiBasePath, }: GetNextAdminPropsParams): Promise< | AdminComponentProps - | Omit + | Omit | Pick< AdminComponentProps, | "pageComponent" @@ -85,6 +83,7 @@ export async function getPropsFromParams({ resourcesIcons, externalLinks, locale, + schema, }; if (!params) return defaultProps; @@ -159,7 +158,6 @@ export async function getPropsFromParams({ case Page.EDIT: { const resourceId = getResourceIdFromParam(params[1], resource); - const dmmfSchema = getPrismaModelForResource(resource); const edit = options?.model?.[resource]?.edit as EditOptions< typeof resource >; @@ -211,7 +209,6 @@ export async function getPropsFromParams({ data, slug, schema: deepCopySchema, - dmmfSchema: dmmfSchema?.fields, customInputs, actions: serializedActions, }; @@ -222,7 +219,6 @@ export async function getPropsFromParams({ ...defaultProps, resource, schema: deepCopySchema, - dmmfSchema: dmmfSchema?.fields, customInputs, }; } @@ -248,7 +244,6 @@ export const getMainLayoutProps = ({ const resources = getResources(options); const resource = getResourceFromParams(params ?? [], resources); - const dmmfSchema = getPrismaModelForResource(resource!); const resourcesIdProperty = resources!.reduce( (acc, resource) => { acc[resource] = getModelIdProperty(resource); @@ -297,7 +292,7 @@ export const getMainLayoutProps = ({ resourcesIcons, externalLinks: options?.externalLinks, options: extractSerializable(options, isAppDir), - dmmfSchema: dmmfSchema?.fields, resourcesIdProperty: resourcesIdProperty, + schema, }; }; diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts index dd221da3..1b0ddb70 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -18,28 +18,39 @@ import { OutputModelAction, ScalarField, Schema, + SchemaDefinitions, + SchemaProperty, UploadParameters, } from "../types"; import { getRawData } from "./prisma"; import { isNativeFunction, isUploadParameters, pipe } from "./tools"; -import { getJsonSchema } from "./jsonSchema"; -export const schema = getJsonSchema(); -export const models: readonly Prisma.DMMF.Model[] = Prisma.dmmf.datamodel - .models as Prisma.DMMF.Model[]; -export const enums = Prisma.dmmf.datamodel.enums; -export const resources = models.map((model) => model.name as ModelName); - -const getEnumValues = (enumName: string) => { - const enumValues = enums.find((en) => en.name === enumName); - return enumValues?.values; -}; +export const getJsonSchema = (): Schema => { + try { + const schema = require(".next-admin/schema.json"); -export const enumValueForEnumType = (enumName: string, value: string) => { - const enumValues = getEnumValues(enumName); + return schema as Schema; + } catch { + throw new Error( + "Schema not found, make sure you added the generator to your schema.prisma file" + ); + } +}; - if (enumValues) { - return enumValues.find((enumValue) => enumValue.name === value); +export const schema = getJsonSchema(); +export const resources = Object.keys(schema.definitions).filter((modelName) => { + // Enums are not resources + return !schema.definitions[modelName as ModelName].enum; +}) as ModelName[]; + +export const enumValueForEnumType = ( + definition: Schema["definitions"][ModelName], + value: string +): string | false | undefined => { + if (definition.enum) { + return definition.enum.find((enumValue) => enumValue === value) as + | string + | undefined; } return false; @@ -90,29 +101,38 @@ export const getEnableToExecuteActions = async ( } }; -export const getPrismaModelForResource = ( - resource: ModelName -): Prisma.DMMF.Model | undefined => - models.find((datamodel) => datamodel.name === resource); - export const getModelIdProperty = (model: ModelName) => { - const prismaModel = models.find((m) => m.name === model); - const idField = prismaModel?.fields.find((f) => f.isId); - return idField?.name ?? "id"; + const schemaModel = schema.definitions[model]; + const schemaModelProperty = schemaModel.properties; + + return ( + Object.keys(schemaModelProperty).find( + (property) => + schemaModelProperty[property as keyof typeof schemaModelProperty] + ?.__nextadmin?.primaryKey + ) ?? "id" + ); }; const getDeepRelationModel = ( model: M, property: Field -): Prisma.DMMF.Field | undefined => { - const prismaModel = getPrismaModelForResource(model); - const relationField = prismaModel?.fields.find((f) => f.name === property); +) => { + const schemaModel = schema.definitions[model] as SchemaDefinitions[ModelName]; + const schemaModelProperty = schemaModel.properties; + + const relationField = + schemaModelProperty[property as keyof typeof schemaModelProperty]; return relationField; }; export const modelHasIdField = (model: ModelName) => { - const prismaModel = models.find((m) => m.name === model); - return !!prismaModel?.fields.some((f) => f.isId); + const schemaModel = schema.definitions[model]; + const schemaModelProperty = schemaModel.properties; + + return Object.entries(schemaModelProperty).some( + ([, value]) => value.__nextadmin?.primaryKey + ); }; export const getResources = ( @@ -175,8 +195,7 @@ export const getToStringForModel = ( const orderSchema = (resource: ModelName, options?: NextAdminOptions) => (schema: Schema) => { const modelName = resource; - const model = models.find((model) => model.name === modelName); - if (!model) return schema; + if (!schema.definitions[resource]) return schema; const edit = options?.model?.[modelName]?.edit as EditOptions< typeof modelName >; @@ -215,28 +234,34 @@ export const fillRelationInSchema = (resource: ModelName, options?: NextAdminOptions) => async (schema: Schema) => { const modelName = resource; - const model = models.find((model) => model.name === modelName); + const modelSchema = schema.definitions[ + modelName + ] as SchemaDefinitions[ModelName]; + const modelProperties = modelSchema?.properties; const display = options?.model?.[modelName]?.edit?.display; let fields; - if (model?.fields && display) { - fields = model.fields?.filter((field) => display.includes(field.name)); + if (modelProperties && display) { + fields = Object.entries(modelProperties).filter(([field]) => + display.includes(field) + ); } else { - fields = model?.fields; + fields = Object.entries(modelProperties); } - if (!model || !fields) return schema; + if (!modelSchema || !fields) return schema; await Promise.all( - fields.map(async (field) => { - const fieldName = field.name as Field; - const fieldType = field.type; - const fieldKind = field.kind; - const relationToFields = field.relationToFields; - const relationFromFields = field.relationFromFields; + fields.map(async ([name, value]) => { + const fieldName = name as Field; + const fieldNextAdmin = value.__nextadmin; + const fieldType = fieldNextAdmin?.type; + const fieldKind = fieldNextAdmin?.kind; + const relationToField = fieldNextAdmin?.relation?.toField; + const relationFromField = fieldNextAdmin?.relation?.fromField; if (fieldKind === "enum") { const fieldValue = schema.definitions[modelName].properties[ - field.name as Field + name as Field ]; if (fieldValue) { fieldValue.enum = fieldValue.enum?.map((item) => @@ -254,18 +279,19 @@ export const fillRelationInSchema = if (fieldKind === "object") { const modelNameRelation = fieldType as ModelName; - if (relationToFields!.length > 0) { + if (relationToField) { //Relation One-to-Many, Many side const enumeration: Enumeration[] = []; schema.definitions[modelName].properties[fieldName] = { type: "string", relation: modelNameRelation, enum: enumeration, + __nextadmin: fieldNextAdmin, }; const required = schema.definitions[modelName].required; - const relationFromFieldsRequired = relationFromFields?.every( - (field) => required?.includes(field) + const relationFromFieldsRequired = required?.includes( + relationFromField! ); if (relationFromFieldsRequired) { @@ -275,7 +301,7 @@ export const fillRelationInSchema = } else { const fieldValue = schema.definitions[modelName].properties[ - field.name as Field + name as Field ]; if (fieldValue) { const enumeration: Enumeration[] = []; @@ -312,12 +338,14 @@ export const transformData = ( options?: NextAdminOptions ) => { const modelName = resource; - const model = models.find((model) => model.name === modelName); + const model = schema.definitions[modelName] as SchemaDefinitions[ModelName]; if (!model) return data; + const schemaProperties = model.properties; + return Object.keys(data).reduce((acc, key) => { - const field = model.fields?.find((field) => field.name === key); - const fieldKind = field?.kind; + const field = schemaProperties[key as keyof typeof schemaProperties]; + const fieldKind = field?.__nextadmin?.kind; const get = editOptions?.fields?.[key as Field]?.handler?.get; const explicitManyToManyRelationField = // @ts-expect-error @@ -335,9 +363,11 @@ export const transformData = ( acc[key] = value ? { label: value, value } : null; } } else if (fieldKind === "object") { - const modelRelation = field!.type as ModelName; + const modelRelation = field?.__nextadmin?.type as ModelName; const modelRelationIdField = getModelIdProperty(modelRelation); - let deepRelationModel: Prisma.DMMF.Field | undefined; + let deepRelationModel: + | SchemaProperty[Field] + | undefined; let deepModelRelationIdField: string; if (explicitManyToManyRelationField) { @@ -346,7 +376,7 @@ export const transformData = ( explicitManyToManyRelationField ); deepModelRelationIdField = getModelIdProperty( - deepRelationModel?.type as ModelName + deepRelationModel?.__nextadmin?.type as ModelName ); } @@ -395,10 +425,13 @@ export const transformData = ( } : null; } - } else if (field?.isList && field.kind === "scalar") { + } else if ( + field?.__nextadmin?.isList && + field.__nextadmin?.kind === "scalar" + ) { acc[key] = data[key]; } else { - const fieldTypes = field?.type; + const fieldTypes = field?.__nextadmin?.type; if (fieldTypes === "DateTime") { acc[key] = data[key] ? data[key].toISOString() : null; } else if (fieldTypes === "Json") { @@ -419,41 +452,37 @@ export const transformData = ( * Fill fields in data with the their values and url for the related model * * @param data - * @param dmmfSchema + * @param schema * * @returns data * */ export const findRelationInData = ( data: any[], - dmmfSchema?: readonly Prisma.DMMF.Field[] + schema: SchemaDefinitions[ModelName] ) => { - dmmfSchema?.forEach((dmmfProperty) => { - const dmmfPropertyName = dmmfProperty.name; - const dmmfPropertyType = dmmfProperty.type; - const dmmfPropertyKind = dmmfProperty.kind; - const dmmfPropertyRelationFromFields = dmmfProperty.relationFromFields; - const dmmfPropertyRelationToFields = dmmfProperty.relationToFields; - - if (dmmfPropertyKind === "object") { + Object.entries(schema.properties).forEach(([property, value]) => { + const propertyType = value.__nextadmin?.type; + const propertyKind = value.__nextadmin?.kind; + const propertyRelationFrom = value.__nextadmin?.relation?.fromField; + const propertyRelationToField = value.__nextadmin?.relation?.toField; + const isList = value.__nextadmin?.isList; + + if (propertyKind === "object") { /** * Handle one-to-one relation * Make sure that we are in a relation that is not a list * because one side of a one-to-one relation will not have relationFromFields */ - if ( - (dmmfPropertyRelationFromFields!.length > 0 && - dmmfPropertyRelationToFields!.length > 0) || - !dmmfProperty.isList - ) { - const idProperty = getModelIdProperty(dmmfProperty.type as ModelName); + if ((propertyRelationFrom && propertyRelationToField) || !isList) { + const idProperty = getModelIdProperty(propertyType as ModelName); data.forEach((item) => { - if (item[dmmfPropertyName]) { - item[dmmfPropertyName] = { + if (item[property]) { + item[property] = { type: "link", value: { - label: item[dmmfPropertyName], - url: `${dmmfProperty.type as ModelName}/${ - item[dmmfPropertyName][idProperty] + label: item[property], + url: `${propertyType as ModelName}/${ + item[property][idProperty] }`, }, }; @@ -462,10 +491,10 @@ export const findRelationInData = ( }); } else { data.forEach((item) => { - if (item[dmmfPropertyName]) { - item[dmmfPropertyName] = { + if (item[property]) { + item[property] = { type: "count", - value: item[dmmfPropertyName].length, + value: item[property].length, }; } return item; @@ -473,12 +502,12 @@ export const findRelationInData = ( } } - if (["scalar", "enum"].includes(dmmfPropertyKind) && dmmfProperty.isList) { + if (["scalar", "enum"].includes(propertyKind ?? "") && isList) { data.forEach((item) => { - if (item[dmmfPropertyName]) { - item[dmmfPropertyName] = { + if (item[property]) { + item[property] = { type: "count", - value: item[dmmfPropertyName].length, + value: item[property].length, }; } return item; @@ -486,23 +515,21 @@ export const findRelationInData = ( } if ( - dmmfPropertyType === "DateTime" || - dmmfPropertyType === "Decimal" || - dmmfPropertyType === "BigInt" + propertyType === "DateTime" || + propertyType === "Decimal" || + propertyType === "BigInt" ) { data.forEach((item) => { - if (item[dmmfProperty.name]) { - if (dmmfPropertyType === "DateTime") { - item[dmmfProperty.name] = { + if (item[property]) { + if (propertyType === "DateTime") { + item[property] = { type: "date", - value: item[dmmfProperty.name].toISOString(), + value: item[property].toISOString(), }; - } else if (dmmfPropertyType === "Decimal") { - item[dmmfProperty.name] = Number(item[dmmfProperty.name]); - } else if (dmmfPropertyType === "BigInt") { - item[dmmfProperty.name] = BigInt( - item[dmmfProperty.name] - ).toString(); + } else if (propertyType === "Decimal") { + item[property] = Number(item[property]); + } else if (propertyType === "BigInt") { + item[property] = BigInt(item[property]).toString(); } } else { return item; @@ -515,39 +542,43 @@ export const findRelationInData = ( export const parseFormData = ( formData: AdminFormData, - dmmfSchema: readonly Prisma.DMMF.Field[] + schemaResource: SchemaDefinitions[ModelName] ): Partial> => { const parsedData: Partial> = {}; - dmmfSchema.forEach((dmmfProperty) => { - if (dmmfProperty.name in formData) { - const dmmfPropertyName = dmmfProperty.name as keyof ScalarField; - const dmmfPropertyType = dmmfProperty.type; - const dmmfPropertyKind = dmmfProperty.kind; - if (dmmfPropertyKind === "object") { - if (formData[dmmfPropertyName]) { - parsedData[dmmfPropertyName] = formData[ - dmmfPropertyName - ] as unknown as ModelWithoutRelationships[typeof dmmfPropertyName]; + Object.entries(schemaResource.properties).forEach(([property, value]) => { + if (property in formData) { + const formPropertyName = property as keyof ScalarField; + const propertyNextAdminData = value.__nextadmin; + const propertyType = propertyNextAdminData?.type; + const propertyKind = propertyNextAdminData?.kind; + if (propertyKind === "object") { + if (formData[formPropertyName]) { + parsedData[formPropertyName] = formData[ + formPropertyName + ] as unknown as ModelWithoutRelationships[typeof formPropertyName]; } else { - parsedData[dmmfPropertyName] = - null as ModelWithoutRelationships[typeof dmmfPropertyName]; + parsedData[formPropertyName] = + null as ModelWithoutRelationships[typeof formPropertyName]; } - } else if (dmmfProperty.isList && dmmfProperty.kind === "scalar") { - parsedData[dmmfPropertyName] = JSON.parse( - formData[dmmfPropertyName]! - ) as unknown as ModelWithoutRelationships[typeof dmmfPropertyName]; - } else if (dmmfPropertyType === "Int") { - const value = Number(formData[dmmfPropertyName]) as number; - parsedData[dmmfPropertyName] = isNaN(value) + } else if ( + propertyNextAdminData?.isList && + propertyNextAdminData.kind === "scalar" + ) { + parsedData[formPropertyName] = JSON.parse( + formData[formPropertyName]! + ) as unknown as ModelWithoutRelationships[typeof formPropertyName]; + } else if (propertyType === "Int") { + const value = Number(formData[formPropertyName]) as number; + parsedData[formPropertyName] = isNaN(value) ? undefined - : (value as ModelWithoutRelationships[typeof dmmfPropertyName]); - } else if (dmmfPropertyType === "Boolean") { - parsedData[dmmfPropertyName] = (formData[dmmfPropertyName] === - "on") as unknown as ModelWithoutRelationships[typeof dmmfPropertyName]; + : (value as ModelWithoutRelationships[typeof formPropertyName]); + } else if (propertyType === "Boolean") { + parsedData[formPropertyName] = (formData[formPropertyName] === + "on") as unknown as ModelWithoutRelationships[typeof formPropertyName]; } else { - parsedData[dmmfPropertyName] = formData[ - dmmfPropertyName - ] as unknown as ModelWithoutRelationships[typeof dmmfPropertyName]; + parsedData[formPropertyName] = formData[ + formPropertyName + ] as unknown as ModelWithoutRelationships[typeof formPropertyName]; } } }); @@ -555,11 +586,13 @@ export const parseFormData = ( }; export const formatId = (resource: ModelName, id: string) => { - const model = models.find((model) => model.name === resource); + const model = schema.definitions[resource] as SchemaDefinitions[ModelName]; + const modelProperties = model.properties; const idProperty = getModelIdProperty(resource); - return model?.fields?.find((field) => field.name === idProperty)?.type === - "Int" + return Object.entries(modelProperties).find( + ([name]) => name === idProperty + )?.[1].__nextadmin?.type === "Int" ? Number(id) : id; }; @@ -567,9 +600,13 @@ export const formatId = (resource: ModelName, id: string) => { const getExplicitManyToManyTableFields = ( manyToManyResource: M ) => { - const model = getPrismaModelForResource(manyToManyResource); - const relationFields = model?.fields.filter( - (field) => field.kind === "object" + const model = schema.definitions[ + manyToManyResource + ] as SchemaDefinitions[ModelName]; + const modelProperties = model.properties; + + const relationFields = Object.entries(modelProperties).filter( + ([, value]) => value.__nextadmin?.kind === "object" ); return relationFields; @@ -578,11 +615,11 @@ const getExplicitManyToManyTableFields = ( const getExplicitManyToManyTablePrimaryKey = ( resource: M ) => { - const model = getPrismaModelForResource(resource); + const model = schema.definitions[resource] as SchemaDefinitions[ModelName]; return { - name: model?.primaryKey?.fields.join("_"), - fields: model?.primaryKey?.fields, + name: model?.__nextadmin?.primaryKeyField?.name, + fields: model?.__nextadmin?.primaryKeyField?.fields, }; }; @@ -590,12 +627,13 @@ const getExplicitManyToManyTablePrimaryKey = ( * Convert the form data to the format expected by Prisma * * @param formData - * @param dmmfSchema - * + * @param schema + * @param resource + * @param resourceId + * @param editOptions */ export const formattedFormData = async ( formData: AdminFormData, - dmmfSchema: readonly Prisma.DMMF.Field[], schema: Schema, resource: M, resourceId: string | number | undefined, @@ -606,25 +644,28 @@ export const formattedFormData = async ( const modelName = resource; const errors: Array<{ field: string; message: string }> = []; const creating = resourceId === undefined; + const resourceSchema = schema.definitions[ + modelName + ] as SchemaDefinitions[ModelName]; const results = await Promise.allSettled( - dmmfSchema.map(async (dmmfProperty) => { - if (dmmfProperty.name in formData) { - const dmmfPropertyType = dmmfProperty.type; - const dmmfPropertyKind = dmmfProperty.kind; - if (dmmfPropertyKind === "object") { - const dmmfPropertyName = dmmfProperty.name as keyof ObjectField; - const dmmfPropertyTypeTyped = dmmfPropertyType as Prisma.ModelName; + Object.entries(resourceSchema.properties).map(async ([property, value]) => { + if (property in formData) { + const propertyNextAdminData = value.__nextadmin; + const propertyType = propertyNextAdminData?.type; + const propertyKind = propertyNextAdminData?.kind; + const isList = propertyNextAdminData?.isList; + if (propertyKind === "object") { + const propertyName = property as keyof ObjectField; + const propertyTypeTyped = propertyType as Prisma.ModelName; const fieldValue = schema.definitions[modelName].properties[ - dmmfPropertyName as Field + propertyName as Field ]; if (fieldValue?.type === "array") { - formData[dmmfPropertyName] = JSON.parse( - formData[dmmfPropertyName]! - ); + formData[propertyName] = JSON.parse(formData[propertyName]!); - const fieldOptions = editOptions?.[dmmfPropertyName]; + const fieldOptions = editOptions?.[propertyName]; const orderField = fieldOptions && @@ -636,29 +677,27 @@ export const formattedFormData = async ( "relationshipSearchField" in fieldOptions && fieldOptions?.relationshipSearchField ) { - const relationFields = getExplicitManyToManyTableFields( - dmmfPropertyTypeTyped - )!; + const relationFields = + getExplicitManyToManyTableFields(propertyTypeTyped)!; const currentResourceField = relationFields.filter( - (field) => field.type === resource + ([, field]) => field.__nextadmin?.type === resource )[0]; const externalResourceField = relationFields.filter( - (field) => field.type !== resource + ([, field]) => field.__nextadmin?.type !== resource )[0]; if (creating) { - formattedData[dmmfPropertyName] = { + formattedData[propertyName] = { create: ( - formData[ - dmmfPropertyName - ] as unknown as Enumeration["value"][] + formData[propertyName] as unknown as Enumeration["value"][] ).map((item, index) => { const data: Record = { - [externalResourceField.name]: { + [externalResourceField[0]]: { connect: { id: formatId( - externalResourceField.type as ModelName, + externalResourceField[1].__nextadmin + ?.type as ModelName, item ), }, @@ -673,33 +712,33 @@ export const formattedFormData = async ( }), }; } else { - const resourcePrimaryKey = getExplicitManyToManyTablePrimaryKey( - dmmfPropertyTypeTyped - )!; + const resourcePrimaryKey = + getExplicitManyToManyTablePrimaryKey(propertyTypeTyped)!; const resourcePrimaryKeyCurrentResourceField = resourcePrimaryKey.fields!.find( (field) => - field === currentResourceField.relationFromFields?.[0] + field === + currentResourceField[1].__nextadmin?.relation?.fromField )!; const resourcePrimaryKeyExternalResourceField = resourcePrimaryKey.fields!.find( (field) => - field === externalResourceField.relationFromFields?.[0] + field === + externalResourceField[1].__nextadmin?.relation?.fromField )!; - formattedData[dmmfPropertyName] = { + formattedData[propertyName] = { upsert: ( - formData[ - dmmfPropertyName - ] as unknown as Enumeration["value"][] + formData[propertyName] as unknown as Enumeration["value"][] ).map((item, index) => { const formattedItem: Record = { create: { - [externalResourceField.name]: { + [externalResourceField[0]]: { connect: { id: formatId( - externalResourceField.type as ModelName, + externalResourceField[1].__nextadmin + ?.type as ModelName, item ), }, @@ -712,16 +751,18 @@ export const formattedFormData = async ( resourceId.toString() ), [resourcePrimaryKeyExternalResourceField]: formatId( - externalResourceField.type as ModelName, + externalResourceField[1]?.__nextadmin + ?.type as ModelName, item ), }, }, update: { - [externalResourceField.name]: { + [externalResourceField[0]]: { connect: { id: formatId( - externalResourceField.type as ModelName, + externalResourceField[1]?.__nextadmin + ?.type as ModelName, item ), }, @@ -744,10 +785,14 @@ export const formattedFormData = async ( [resourcePrimaryKeyExternalResourceField]: { notIn: ( formData[ - dmmfPropertyName + propertyName ] as unknown as Enumeration["value"][] ).map((item) => - formatId(externalResourceField.type as ModelName, item) + formatId( + externalResourceField[1]?.__nextadmin + ?.type as ModelName, + item + ) ), }, }, @@ -757,12 +802,12 @@ export const formattedFormData = async ( const updateRelatedField = { ...(orderField && { update: formData[ - dmmfPropertyName + propertyName // @ts-expect-error ]?.map((item: any, index: number) => { return { where: { - id: formatId(dmmfPropertyType as ModelName, item), + id: formatId(propertyType as ModelName, item), }, data: { ...(orderField && { @@ -774,49 +819,48 @@ export const formattedFormData = async ( }), }; - formattedData[dmmfPropertyName] = { + formattedData[propertyName] = { // @ts-expect-error - [creating ? "connect" : "set"]: formData[dmmfPropertyName].map( + [creating ? "connect" : "set"]: formData[propertyName].map( (item: any) => ({ - id: formatId(dmmfPropertyType as ModelName, item), + id: formatId(propertyType as ModelName, item), }) ), ...(!creating && updateRelatedField), }; if (creating) { - complementaryFormattedData[dmmfPropertyName] = - updateRelatedField; + complementaryFormattedData[propertyName] = updateRelatedField; } } } else { - const connect = Boolean(formData[dmmfPropertyName]); + const connect = Boolean(formData[propertyName]); if (connect) { - formattedData[dmmfPropertyName] = { + formattedData[propertyName] = { connect: { id: formatId( - dmmfPropertyType as ModelName, - formData[dmmfPropertyName]! + propertyType as ModelName, + formData[propertyName]! ), }, }; } else if (!creating) { - formattedData[dmmfPropertyName] = { disconnect: true }; + formattedData[propertyName] = { disconnect: true }; } } - } else if (dmmfPropertyKind === "scalar" && dmmfProperty.isList) { - const dmmfPropertyName = dmmfProperty.name as keyof ScalarField; + } else if (propertyKind === "scalar" && isList) { + const propertyName = property as keyof ScalarField; - const formDataValue = JSON.parse(formData[dmmfPropertyName]!) as + const formDataValue = JSON.parse(formData[propertyName]!) as | string[] | number[]; if ( - dmmfPropertyType === "Int" || - dmmfPropertyType === "Float" || - dmmfPropertyType === "Decimal" + propertyType === "Int" || + propertyType === "Float" || + propertyType === "Decimal" ) { - formattedData[dmmfPropertyName] = { + formattedData[propertyName] = { set: formDataValue .map((item) => !isNaN(Number(item)) ? Number(item) : undefined @@ -824,61 +868,57 @@ export const formattedFormData = async ( .filter(Boolean), }; } else { - formattedData[dmmfPropertyName] = { + formattedData[propertyName] = { set: formDataValue, }; } - } else if (dmmfPropertyKind === "enum" && dmmfProperty.isList) { - const dmmfPropertyName = dmmfProperty.name as keyof ScalarField; + } else if (propertyKind === "enum" && isList) { + const propertyName = property as keyof ScalarField; - const data = JSON.parse(formData[dmmfPropertyName] ?? "[]"); - formattedData[dmmfPropertyName] = { + const data = JSON.parse(formData[propertyName] ?? "[]"); + formattedData[propertyName] = { set: data, }; } else { - const dmmfPropertyName = dmmfProperty.name as keyof ScalarField; - if (formData[dmmfPropertyName] === "") { - formattedData[dmmfPropertyName] = null; + const propertyName = property as keyof ScalarField; + if (formData[propertyName] === "") { + formattedData[propertyName] = null; } else if ( - dmmfPropertyType === "Int" || - dmmfPropertyType === "Float" || - dmmfPropertyType === "Decimal" + propertyType === "Int" || + propertyType === "Float" || + propertyType === "Decimal" ) { - formattedData[dmmfPropertyName] = !isNaN( - Number(formData[dmmfPropertyName]) - ) - ? Number(formData[dmmfPropertyName]) + formattedData[propertyName] = !isNaN(Number(formData[propertyName])) + ? Number(formData[propertyName]) : undefined; - } else if (dmmfPropertyType === "Boolean") { - formattedData[dmmfPropertyName] = - formData[dmmfPropertyName] === "on"; - } else if (dmmfPropertyType === "DateTime") { - formattedData[dmmfPropertyName] = formData[dmmfPropertyName] - ? new Date(formData[dmmfPropertyName]!) + } else if (propertyType === "Boolean") { + formattedData[propertyName] = formData[propertyName] === "on"; + } else if (propertyType === "DateTime") { + formattedData[propertyName] = formData[propertyName] + ? new Date(formData[propertyName]!) : null; - } else if (dmmfPropertyType === "Json") { + } else if (propertyType === "Json") { try { - formattedData[dmmfPropertyName] = formData[dmmfPropertyName] - ? JSON.parse(formData[dmmfPropertyName]!) + formattedData[propertyName] = formData[propertyName] + ? JSON.parse(formData[propertyName]!) : null; } catch { // no-op } - } else if (dmmfPropertyType === "BigInt") { - formattedData[dmmfPropertyName] = formData[dmmfPropertyName] - ? BigInt(formData[dmmfPropertyName]!) + } else if (propertyType === "BigInt") { + formattedData[propertyName] = formData[propertyName] + ? BigInt(formData[propertyName]!) : null; } else if ( - dmmfPropertyType === "String" && + propertyType === "String" && ["data-url", "file"].includes( - editOptions?.[dmmfPropertyName]?.format ?? "" + editOptions?.[propertyName]?.format ?? "" ) && - isUploadParameters(formData[dmmfPropertyName]) + isUploadParameters(formData[propertyName]) ) { - const uploadHandler = - editOptions?.[dmmfPropertyName]?.handler?.upload; + const uploadHandler = editOptions?.[propertyName]?.handler?.upload; const uploadErrorMessage = - editOptions?.[dmmfPropertyName]?.handler?.uploadErrorMessage; + editOptions?.[propertyName]?.handler?.uploadErrorMessage; if (!uploadHandler) { console.warn( @@ -887,19 +927,19 @@ export const formattedFormData = async ( } else { try { const uploadResult = await uploadHandler( - ...(formData[dmmfPropertyName] as unknown as UploadParameters) + ...(formData[propertyName] as unknown as UploadParameters) ); if (typeof uploadResult !== "string") { console.warn( "Upload handler must return a string, fallback to no-op for field " + - dmmfPropertyName.toString() + propertyName.toString() ); } else { - formattedData[dmmfPropertyName] = uploadResult; + formattedData[propertyName] = uploadResult; } } catch (e) { errors.push({ - field: dmmfPropertyName.toString(), + field: propertyName.toString(), message: uploadErrorMessage ?? `Upload failed: ${(e as Error).message}`, @@ -907,7 +947,7 @@ export const formattedFormData = async ( } } } else { - formattedData[dmmfPropertyName] = formData[dmmfPropertyName]; + formattedData[propertyName] = formData[propertyName]; } } } @@ -975,8 +1015,10 @@ export const applyVisiblePropertiesInSchema = ( schema: Schema ) => { const modelName = resource; - const model = models.find((model) => model.name === modelName); - if (!model) return schema; + const modelSchema = schema.definitions[ + modelName + ] as SchemaDefinitions[ModelName]; + if (!modelSchema) return schema; const display = edit?.display; const fields = edit?.fields; if (display) { @@ -999,17 +1041,16 @@ const fillDescriptionInSchema = ( ) => { return (schema: Schema) => { const modelName = resource; - const model = models.find((model) => model.name === modelName); - if (!model) return schema; - model.fields.forEach((dmmfProperty) => { - const dmmfPropertyName = dmmfProperty.name as Field; - const fieldValue = - schema.definitions[modelName].properties[ - dmmfPropertyName as Field - ]; - if (fieldValue && editOptions?.fields?.[dmmfPropertyName]?.helperText) { + const modelSchema = schema.definitions[ + modelName + ] as SchemaDefinitions[ModelName]; + if (!modelSchema) return schema; + Object.entries(modelSchema.properties).forEach(([name, value]) => { + const propertyName = name as Field; + const fieldValue = schema.definitions[modelName].properties[propertyName]; + if (fieldValue && editOptions?.fields?.[propertyName]?.helperText) { fieldValue.description = - editOptions?.fields?.[dmmfPropertyName]?.helperText; + editOptions?.fields?.[propertyName]?.helperText; } }); return schema; @@ -1020,27 +1061,29 @@ export const changeFormatInSchema = (resource: M, editOptions: EditOptions) => (schema: Schema) => { const modelName = resource; - const model = models.find((model) => model.name === modelName); - if (!model) return schema; - model.fields.forEach((dmmfProperty) => { - const dmmfPropertyName = dmmfProperty.name as Field; + const modelSchema = schema.definitions[ + modelName + ] as SchemaDefinitions[ModelName]; + if (!modelSchema) return schema; + Object.entries(modelSchema.properties).forEach(([name, value]) => { + const propertyName = name as Field; const fieldValue = schema.definitions[modelName].properties[ - dmmfPropertyName as Field + propertyName as Field ]; - if (fieldValue && dmmfProperty.type === "Json") { + if (fieldValue && value.__nextadmin?.type === "Json") { fieldValue.type = "string"; } - if (fieldValue && editOptions?.fields?.[dmmfPropertyName]?.input) { + if (fieldValue && editOptions?.fields?.[propertyName]?.input) { fieldValue.format = "string"; - } else if (editOptions?.fields?.[dmmfPropertyName]?.format) { + } else if (editOptions?.fields?.[propertyName]?.format) { if (fieldValue) { - if (editOptions?.fields?.[dmmfPropertyName]?.format === "file") { + if (editOptions?.fields?.[propertyName]?.format === "file") { fieldValue.format = "data-url"; } else { - fieldValue.format = editOptions?.fields?.[dmmfPropertyName]?.format; + fieldValue.format = editOptions?.fields?.[propertyName]?.format; } } } diff --git a/yarn.lock b/yarn.lock index 2720ba62..a3aeffe1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2733,7 +2733,7 @@ __metadata: version: 0.0.0-use.local resolution: "@premieroctet/next-admin-generator-prisma@workspace:packages/generator-prisma" dependencies: - "@premieroctet/next-admin-json-schema": "workspace:*" + "@premieroctet/next-admin-json-schema": "workspace:^" "@prisma/generator-helper": "npm:^5.20.0" "@prisma/internals": "npm:^5.20.0" "@types/json-schema": "npm:^7.0.15" @@ -2747,7 +2747,7 @@ __metadata: languageName: unknown linkType: soft -"@premieroctet/next-admin-json-schema@workspace:*, @premieroctet/next-admin-json-schema@workspace:packages/json-schema": +"@premieroctet/next-admin-json-schema@workspace:^, @premieroctet/next-admin-json-schema@workspace:packages/json-schema": version: 0.0.0-use.local resolution: "@premieroctet/next-admin-json-schema@workspace:packages/json-schema" dependencies: @@ -2770,6 +2770,7 @@ __metadata: "@heroicons/react": "npm:^2.0.18" "@monaco-editor/react": "npm:^4.6.0" "@picocss/pico": "npm:^1.5.7" + "@premieroctet/next-admin-json-schema": "workspace:^" "@radix-ui/react-checkbox": "npm:^1.0.4" "@radix-ui/react-dialog": "npm:^1.0.5" "@radix-ui/react-dropdown-menu": "npm:^2.0.6"