Repository: Quramy/prisma-fabbrica (ts)

Issue:
Can't give factory for optional one-to-many relation
**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

**Expected behavior**
`definePostFactory` should accept UserFactory for author field.

**Your Schema**

```
model User {
  id Int @id
  posts Post[]
}
model Post {
  id Int @id
  author User? @relation(fields: [authorId], references: [id])
  authorId Int?
}
```

**Environment (please complete the following information):**

- Database kind [e.g. PostgreSQL ]
- Node.js version [e.g. 16.0]
- `@prisma/client` version [e.g. 4.2]
- TypeScript version [e.g. 4.8.3]

**Additional context**
Add any other context about the problem here.

Please put gist URL including generated .js and .d.ts if you can :pray:


Pull request:
Fix optional relation type
It closes #33 .

Patch:
diff --git a/examples/example-prj/src/__generated__/fabbrica/index.js b/examples/example-prj/src/__generated__/fabbrica/index.js
index f8d0004..913f225 100644
--- a/examples/example-prj/src/__generated__/fabbrica/index.js
+++ b/examples/example-prj/src/__generated__/fabbrica/index.js
@@ -44,7 +44,7 @@ function defineUserFactory(args = {}) {
 }
 exports.defineUserFactory = defineUserFactory;
 function isPostauthorFactory(x) {
-    return x._factoryFor === "User";
+    return x?._factoryFor === "User";
 }
 function autoGeneratePostScalarsOrEnums() {
     return {
diff --git a/packages/prisma-fabbrica/src/templates/index.ts b/packages/prisma-fabbrica/src/templates/index.ts
index e81d724..f45473d 100644
--- a/packages/prisma-fabbrica/src/templates/index.ts
+++ b/packages/prisma-fabbrica/src/templates/index.ts
@@ -4,6 +4,10 @@ import { template } from "talt";
 
 type StripCreate<T extends string> = T extends `create${infer S}` ? Uncapitalize<S> : T;
 
+function byName<T extends { readonly name: string }>(name: string | { readonly name: string }) {
+  return (x: T) => x.name === (typeof name === "string" ? name : name.name);
+}
+
 function camelize(pascal: string) {
   return pascal[0].toLowerCase() + pascal.slice(1);
 }
@@ -56,6 +60,12 @@ function filterRequiredInputObjectTypeField(inputType: DMMF.InputType) {
   return filterRequiredFields(inputType).filter(isInputObjectTypeField);
 }
 
+function filterBelongsToField(model: DMMF.Model, inputType: DMMF.InputType) {
+  return inputType.fields
+    .filter(isInputObjectTypeField)
+    .filter(field => model.fields.find(byName(field))?.isList === false);
+}
+
 function filterEnumFields(inputType: DMMF.InputType) {
   return inputType.fields.filter(
     field =>
@@ -65,7 +75,7 @@ function filterEnumFields(inputType: DMMF.InputType) {
 
 function extractFirstEnumValue(enums: DMMF.SchemaEnum[], field: DMMF.SchemaArg) {
   const typeName = field.inputTypes[0].type;
-  const found = enums.find(e => e.name === field.inputTypes[0].type);
+  const found = enums.find(byName(typeName));
   if (!found) {
     throw new Error(`Not found enum ${typeName}`);
   }
@@ -88,7 +98,7 @@ export const importStatement = (specifier: string, prismaClientModuleSpecifier:
   `();
 
 export const scalarFieldType = (
-  modelName: string,
+  model: DMMF.Model,
   fieldName: string,
   inputType: DMMF.SchemaArgInputType,
 ): ts.TypeNode => {
@@ -116,14 +126,14 @@ export const scalarFieldType = (
       // return template.typeNode`Prisma.Json`();
       return ast.keywordTypeNode(ts.SyntaxKind.AnyKeyword);
     default:
-      throw new Error(`Unknown scalar type "${inputType.type}" for ${modelName}.${fieldName} .`);
+      throw new Error(`Unknown scalar type "${inputType.type}" for ${model.name}.${fieldName} .`);
   }
 };
 
-export const argInputType = (modelName: string, fieldName: string, inputType: DMMF.SchemaArgInputType): ts.TypeNode => {
+export const argInputType = (model: DMMF.Model, fieldName: string, inputType: DMMF.SchemaArgInputType): ts.TypeNode => {
   const fieldType = () => {
     if (inputType.location === "scalar") {
-      return scalarFieldType(modelName, fieldName, inputType);
+      return scalarFieldType(model, fieldName, inputType);
     } else if (inputType.location === "enumTypes") {
       return ast.typeReferenceNode(ast.identifier(inputType.type as string));
     } else if (inputType.location === "outputObjectTypes" || inputType.location === "inputObjectTypes") {
@@ -140,7 +150,7 @@ export const argInputType = (modelName: string, fieldName: string, inputType: DM
     : fieldType();
 };
 
-export const modelScalarOrEnumFields = (modelName: string, inputType: DMMF.InputType) =>
+export const modelScalarOrEnumFields = (model: DMMF.Model, inputType: DMMF.InputType) =>
   template.statement<ts.TypeAliasDeclaration>`
     type MODEL_SCALAR_OR_ENUM_FIELDS = ${() =>
       ast.typeLiteralNode(
@@ -149,18 +159,16 @@ export const modelScalarOrEnumFields = (modelName: string, inputType: DMMF.Input
             undefined,
             field.name,
             undefined,
-            ast.unionTypeNode(
-              field.inputTypes.map(childInputType => argInputType(modelName, field.name, childInputType)),
-            ),
+            ast.unionTypeNode(field.inputTypes.map(childInputType => argInputType(model, field.name, childInputType))),
           ),
         ),
       )}
   `({
-    MODEL_SCALAR_OR_ENUM_FIELDS: ast.identifier(`${modelName}ScalarOrEnumFields`),
+    MODEL_SCALAR_OR_ENUM_FIELDS: ast.identifier(`${model.name}ScalarOrEnumFields`),
   });
 
 export const modelBelongsToRelationFactory = (fieldType: DMMF.SchemaArg, model: DMMF.Model) => {
-  const targetModel = model.fields.find(f => f.name === fieldType.name)!;
+  const targetModel = model.fields.find(byName(fieldType))!;
   return template.statement<ts.TypeAliasDeclaration>`
     type ${() => ast.identifier(`${model.name}${fieldType.name}Factory`)} = {
       _factoryFor: ${() => ast.literalTypeNode(ast.stringLiteral(targetModel.type))};
@@ -170,7 +178,7 @@ export const modelBelongsToRelationFactory = (fieldType: DMMF.SchemaArg, model:
   `();
 };
 
-export const modelFactoryDefineInput = (modelName: string, inputType: DMMF.InputType) =>
+export const modelFactoryDefineInput = (model: DMMF.Model, inputType: DMMF.InputType) =>
   template.statement<ts.TypeAliasDeclaration>`
     type MODEL_FACTORY_DEFINE_INPUT = ${() =>
       ast.typeLiteralNode(
@@ -180,16 +188,17 @@ export const modelFactoryDefineInput = (modelName: string, inputType: DMMF.Input
             field.name,
             !field.isRequired || isScalarOrEnumField(field) ? ast.token(ts.SyntaxKind.QuestionToken) : undefined,
             ast.unionTypeNode([
-              ...(field.isRequired && isInputObjectTypeField(field)
-                ? [ast.typeReferenceNode(ast.identifier(`${modelName}${field.name}Factory`))]
+              ...((field.isRequired || model.fields.find(byName(field))!.isList === false) &&
+              isInputObjectTypeField(field)
+                ? [ast.typeReferenceNode(ast.identifier(`${model.name}${field.name}Factory`))]
                 : []),
-              ...field.inputTypes.map(childInputType => argInputType(modelName, field.name, childInputType)),
+              ...field.inputTypes.map(childInputType => argInputType(model, field.name, childInputType)),
             ]),
           ),
         ),
       )};
   `({
-    MODEL_FACTORY_DEFINE_INPUT: ast.identifier(`${modelName}FactoryDefineInput`),
+    MODEL_FACTORY_DEFINE_INPUT: ast.identifier(`${model.name}FactoryDefineInput`),
   });
 
 export const modelFactoryDefineOptions = (modelName: string, isOpionalDefaultData: boolean) =>
@@ -212,12 +221,13 @@ export const modelFactoryDefineOptions = (modelName: string, isOpionalDefaultDat
       });
 
 export const isModelAssociationFactory = (fieldType: DMMF.SchemaArg, model: DMMF.Model) => {
-  const targetModel = model.fields.find(f => f.name === fieldType.name)!;
+  const targetModel = model.fields.find(byName(fieldType))!;
   return template.statement<ts.FunctionDeclaration>`
     function ${() => ast.identifier(`is${model.name}${fieldType.name}Factory`)}(
-      x: MODEL_BELONGS_TO_RELATION_FACTORY | ${() => argInputType(model.name, fieldType.name, fieldType.inputTypes[0])}
+      x: MODEL_BELONGS_TO_RELATION_FACTORY | ${() =>
+        argInputType(model, fieldType.name, fieldType.inputTypes[0])} | undefined
     ): x is MODEL_BELONGS_TO_RELATION_FACTORY {
-      return (x as any)._factoryFor === ${() => ast.stringLiteral(targetModel.type)};
+      return (x as any)?._factoryFor === ${() => ast.stringLiteral(targetModel.type)};
     }
   `({
     MODEL_BELONGS_TO_RELATION_FACTORY: ast.typeReferenceNode(`${model.name}${fieldType.name}Factory`),
@@ -237,10 +247,10 @@ export const autoGenerateModelScalarsOrEnumsFieldArgs = (
         MODEL_NAME: ast.stringLiteral(model.name),
         FIELD_NAME: ast.stringLiteral(field.name),
         IS_ID:
-          model.fields.find(f => f.name === field.name)!.isId || model.primaryKey?.fields.includes(field.name)
+          model.fields.find(byName(field))!.isId || model.primaryKey?.fields.includes(field.name)
             ? ast.true()
             : ast.false(),
-        IS_UNIQUE: model.fields.find(f => f.name === field.name)!.isUnique ? ast.true() : ast.false(),
+        IS_UNIQUE: model.fields.find(byName(field))!.isUnique ? ast.true() : ast.false(),
       })
     : ast.stringLiteral(extractFirstEnumValue(enums, field));
 
@@ -279,7 +289,7 @@ export const defineModelFactoryInernal = (model: DMMF.Model, inputType: DMMF.Inp
         const defaultData= await resolveValue(defaultDataResolver ?? {});
         const defaultAssociations = ${() =>
           ast.objectLiteralExpression(
-            filterRequiredInputObjectTypeField(inputType).map(field =>
+            filterBelongsToField(model, inputType).map(field =>
               ast.propertyAssignment(
                 field.name,
                 template.expression`
@@ -373,15 +383,13 @@ export function getSourceFile({
     ...document.datamodel.models
       .map(model => ({ model, createInputType: findPrsimaCreateInputTypeFromModelName(document, model.name) }))
       .flatMap(({ model, createInputType }) => [
-        modelScalarOrEnumFields(model.name, createInputType),
-        ...filterRequiredInputObjectTypeField(createInputType).map(fieldType =>
+        modelScalarOrEnumFields(model, createInputType),
+        ...filterBelongsToField(model, createInputType).map(fieldType =>
           modelBelongsToRelationFactory(fieldType, model),
         ),
-        modelFactoryDefineInput(model.name, createInputType),
+        modelFactoryDefineInput(model, createInputType),
         modelFactoryDefineOptions(model.name, filterRequiredInputObjectTypeField(createInputType).length === 0),
-        ...filterRequiredInputObjectTypeField(createInputType).map(fieldType =>
-          isModelAssociationFactory(fieldType, model),
-        ),
+        ...filterBelongsToField(model, createInputType).map(fieldType => isModelAssociationFactory(fieldType, model)),
         autoGenerateModelScalarsOrEnums(model, createInputType, document.schema.enumTypes.model ?? []),
         defineModelFactoryInernal(model, createInputType),
         defineModelFactory(model.name, createInputType),


Test patch:
diff --git a/packages/prisma-fabbrica/src/templates/modelScalarOrEnumFields.test.ts b/packages/prisma-fabbrica/src/templates/modelScalarOrEnumFields.test.ts
index 37d7c94..c7401cb 100644
--- a/packages/prisma-fabbrica/src/templates/modelScalarOrEnumFields.test.ts
+++ b/packages/prisma-fabbrica/src/templates/modelScalarOrEnumFields.test.ts
@@ -52,7 +52,7 @@ describe(modelScalarOrEnumFields, () => {
     });
     const inputType = findPrsimaCreateInputTypeFromModelName(dmmf, "TestModel");
     const source = template.statement(expected)();
-    expect(printNode(modelScalarOrEnumFields("TestModel", inputType))).toBe(printNode(source).trim());
+    expect(printNode(modelScalarOrEnumFields(dmmf.datamodel.models[0], inputType))).toBe(printNode(source).trim());
   });
 
   it("does not generate for nullable field", async () => {
@@ -70,6 +70,6 @@ describe(modelScalarOrEnumFields, () => {
         id: number;
       }
     `();
-    expect(printNode(modelScalarOrEnumFields("TestModel", inputType))).toBe(printNode(expected).trim());
+    expect(printNode(modelScalarOrEnumFields(dmmf.datamodel.models[0], inputType))).toBe(printNode(expected).trim());
   });
 });
diff --git a/packages/ts-compile-testing/fixtures/relations-one-to-many/__generated__/fabbrica/index.ts b/packages/ts-compile-testing/fixtures/relations-one-to-many/__generated__/fabbrica/index.ts
index 0e6465b..f315986 100644
--- a/packages/ts-compile-testing/fixtures/relations-one-to-many/__generated__/fabbrica/index.ts
+++ b/packages/ts-compile-testing/fixtures/relations-one-to-many/__generated__/fabbrica/index.ts
@@ -57,15 +57,22 @@ type PostScalarOrEnumFields = {
     id: string;
     title: string;
 };
+type PostauthorFactory = {
+    _factoryFor: "User";
+    buildCreateInput: () => PromiseLike<Prisma.UserCreateNestedOneWithoutPostsInput["create"]>;
+};
 type PostFactoryDefineInput = {
     id?: string;
     title?: string;
-    author?: Prisma.UserCreateNestedOneWithoutPostsInput;
+    author?: PostauthorFactory | Prisma.UserCreateNestedOneWithoutPostsInput;
     reviews?: Prisma.ReviewCreateNestedManyWithoutPostInput;
 };
 type PostFactoryDefineOptions = {
     defaultData?: Resolver<PostFactoryDefineInput>;
 };
+function isPostauthorFactory(x: PostauthorFactory | Prisma.UserCreateNestedOneWithoutPostsInput | undefined): x is PostauthorFactory {
+    return (x as any)?._factoryFor === "User";
+}
 function autoGeneratePostScalarsOrEnums(): PostScalarOrEnumFields {
     return {
         id: scalarFieldValueGenerator.String({ modelName: "Post", fieldName: "id", isId: true, isUnique: false }),
@@ -76,7 +83,11 @@ function definePostFactoryInternal({ defaultData: defaultDataResolver }: PostFac
     const buildCreateInput = async (inputData: Partial<Prisma.PostCreateInput> = {}) => {
         const requiredScalarData = autoGeneratePostScalarsOrEnums();
         const defaultData = await resolveValue(defaultDataResolver ?? {});
-        const defaultAssociations = {};
+        const defaultAssociations = {
+            author: isPostauthorFactory(defaultData.author) ? {
+                create: await defaultData.author.buildCreateInput()
+            } : defaultData.author
+        };
         const data: Prisma.PostCreateInput = { ...requiredScalarData, ...defaultData, ...defaultAssociations, ...inputData };
         return data;
     };
@@ -120,11 +131,11 @@ type ReviewFactoryDefineInput = {
 type ReviewFactoryDefineOptions = {
     defaultData: Resolver<ReviewFactoryDefineInput>;
 };
-function isReviewpostFactory(x: ReviewpostFactory | Prisma.PostCreateNestedOneWithoutReviewsInput): x is ReviewpostFactory {
-    return (x as any)._factoryFor === "Post";
+function isReviewpostFactory(x: ReviewpostFactory | Prisma.PostCreateNestedOneWithoutReviewsInput | undefined): x is ReviewpostFactory {
+    return (x as any)?._factoryFor === "Post";
 }
-function isReviewreviewerFactory(x: ReviewreviewerFactory | Prisma.UserCreateNestedOneWithoutReviewsInput): x is ReviewreviewerFactory {
-    return (x as any)._factoryFor === "User";
+function isReviewreviewerFactory(x: ReviewreviewerFactory | Prisma.UserCreateNestedOneWithoutReviewsInput | undefined): x is ReviewreviewerFactory {
+    return (x as any)?._factoryFor === "User";
 }
 function autoGenerateReviewScalarsOrEnums(): ReviewScalarOrEnumFields {
     return {
diff --git a/packages/ts-compile-testing/fixtures/relations-one-to-many/sample.ts b/packages/ts-compile-testing/fixtures/relations-one-to-many/sample.ts
index 944e5e3..e9b1fab 100644
--- a/packages/ts-compile-testing/fixtures/relations-one-to-many/sample.ts
+++ b/packages/ts-compile-testing/fixtures/relations-one-to-many/sample.ts
@@ -8,3 +8,8 @@ export const ReviewFactory = defineReviewFactory({
     reviewer: UserFactory,
   },
 });
+export const PostFactoryAlt = definePostFactory({
+  defaultData: {
+    author: UserFactory,
+  },
+});
diff --git a/packages/ts-compile-testing/fixtures/relations-one-to-one/__generated__/fabbrica/index.ts b/packages/ts-compile-testing/fixtures/relations-one-to-one/__generated__/fabbrica/index.ts
index 5f2555a..2037498 100644
--- a/packages/ts-compile-testing/fixtures/relations-one-to-one/__generated__/fabbrica/index.ts
+++ b/packages/ts-compile-testing/fixtures/relations-one-to-one/__generated__/fabbrica/index.ts
@@ -10,14 +10,21 @@ type UserScalarOrEnumFields = {
     id: string;
     name: string;
 };
+type UserprofileFactory = {
+    _factoryFor: "Profile";
+    buildCreateInput: () => PromiseLike<Prisma.ProfileCreateNestedOneWithoutUserInput["create"]>;
+};
 type UserFactoryDefineInput = {
     id?: string;
     name?: string;
-    profile?: Prisma.ProfileCreateNestedOneWithoutUserInput;
+    profile?: UserprofileFactory | Prisma.ProfileCreateNestedOneWithoutUserInput;
 };
 type UserFactoryDefineOptions = {
     defaultData?: Resolver<UserFactoryDefineInput>;
 };
+function isUserprofileFactory(x: UserprofileFactory | Prisma.ProfileCreateNestedOneWithoutUserInput | undefined): x is UserprofileFactory {
+    return (x as any)?._factoryFor === "Profile";
+}
 function autoGenerateUserScalarsOrEnums(): UserScalarOrEnumFields {
     return {
         id: scalarFieldValueGenerator.String({ modelName: "User", fieldName: "id", isId: true, isUnique: false }),
@@ -28,7 +35,11 @@ function defineUserFactoryInternal({ defaultData: defaultDataResolver }: UserFac
     const buildCreateInput = async (inputData: Partial<Prisma.UserCreateInput> = {}) => {
         const requiredScalarData = autoGenerateUserScalarsOrEnums();
         const defaultData = await resolveValue(defaultDataResolver ?? {});
-        const defaultAssociations = {};
+        const defaultAssociations = {
+            profile: isUserprofileFactory(defaultData.profile) ? {
+                create: await defaultData.profile.buildCreateInput()
+            } : defaultData.profile
+        };
         const data: Prisma.UserCreateInput = { ...requiredScalarData, ...defaultData, ...defaultAssociations, ...inputData };
         return data;
     };
@@ -65,8 +76,8 @@ type ProfileFactoryDefineInput = {
 type ProfileFactoryDefineOptions = {
     defaultData: Resolver<ProfileFactoryDefineInput>;
 };
-function isProfileuserFactory(x: ProfileuserFactory | Prisma.UserCreateNestedOneWithoutProfileInput): x is ProfileuserFactory {
-    return (x as any)._factoryFor === "User";
+function isProfileuserFactory(x: ProfileuserFactory | Prisma.UserCreateNestedOneWithoutProfileInput | undefined): x is ProfileuserFactory {
+    return (x as any)?._factoryFor === "User";
 }
 function autoGenerateProfileScalarsOrEnums(): ProfileScalarOrEnumFields {
     return {