Skip to content

Commit

Permalink
Merge pull request #73 from fdarian/fix-one-to-one-relation-optionality
Browse files Browse the repository at this point in the history
fix(generator): one-to-one optionality
  • Loading branch information
fdarian authored Jul 5, 2024
2 parents fa88c56 + d30da2c commit 1916e10
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,47 +97,66 @@ function createRelation(input: {
}
}

function holdsForeignKey(args: {
field: PrismaRelationField
model: DMMF.Model
}) {
const { field, model } = args
return model.fields.some((f) =>
field.relationFromFields.some((from) => f.name === from)
)
}

function getOneToOneOrManyRelation(
field: PrismaRelationField,
ctx: GenerateTableRelationsInput
) {
if (hasReference(field)) {
const opts = createRelationOpts({
relationName: field.relationName,
from: {
modelVarName: getModelVarName(ctx.modelModule.model),
fieldNames: field.relationFromFields,
},
to: {
modelVarName: getModelVarName(field.type),
fieldNames: field.relationToFields,
},
})
return createRelation({
referenceModelVarName: getModelVarName(field.type),
opts,
opts: holdsForeignKey({ field, model: ctx.modelModule.model })
? createRelationOpts({
relationName: field.relationName,
from: {
modelVarName: getModelVarName(ctx.modelModule.model),
fieldNames: field.relationFromFields,
},
to: {
modelVarName: getModelVarName(field.type),
fieldNames: field.relationToFields,
},
})
: undefined,
})
}

// For disambiguating relation

const opposingModel = findOpposingRelationModel(field, ctx.datamodel)
const opposingField = findOpposingRelationField(field, opposingModel)
const opts = createRelationOpts({
relationName: field.relationName,
from: {
modelVarName: getModelVarName(ctx.modelModule.model),
fieldNames: opposingField.relationToFields,
},
to: {
modelVarName: getModelVarName(field.type),
fieldNames: opposingField.relationFromFields,
},
})

return createRelation({
referenceModelVarName: getModelVarName(field.type),
opts,
opts:
holdsForeignKey({ field, model: ctx.modelModule.model }) ||
// ⚠️ This is a workaround for the following issue since this case isn't common
// https://github.com/fdarian/prisma-generator-drizzle/issues/69#issuecomment-2187174021
hasMultipleDisambiguatingRelations({
field,
model: ctx.modelModule.model,
})
? createRelationOpts({
relationName: field.relationName,
from: {
modelVarName: getModelVarName(ctx.modelModule.model),
fieldNames: opposingField.relationToFields,
},
to: {
modelVarName: getModelVarName(field.type),
fieldNames: opposingField.relationFromFields,
},
})
: undefined,
})
}

Expand Down Expand Up @@ -320,3 +339,21 @@ function opposingIsList(
const opposingModel = findOpposingRelationModel(field, ctx.datamodel)
return findOpposingRelationField(field, opposingModel).isList
}

function hasMultipleDisambiguatingRelations(args: {
field: PrismaRelationField
model: DMMF.Model
}): boolean {
let count = 0
for (const field of args.model.fields) {
if (
field.type === args.field.type &&
isRelationField(field) &&
!hasReference(field)
) {
count++
}
if (count > 1) return true
}
return false
}
7 changes: 7 additions & 0 deletions packages/usage/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,13 @@ model OneToOne_A {
model OneToOne_B {
id String @id
a OneToOne_A?
c OneToOne_C?
}

model OneToOne_C {
id String @id
b OneToOne_B @relation(fields: [bId], references: [id])
bId String @unique
}

// #endregion
Expand Down
13 changes: 13 additions & 0 deletions packages/usage/tests/mysql.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { schema } from 'prisma/drizzle/schema'
import { db } from 'src/lib/postgres'
import type { Db, Schema } from 'src/lib/types'
import { testOneToOneType } from './shared/testOneToOneType'
import type { TestContext } from './utils/types'

const ctx: TestContext = {
db: db as unknown as Db,
schema: schema as unknown as Schema,
provider: 'mysql',
}

testOneToOneType(ctx)
8 changes: 8 additions & 0 deletions packages/usage/tests/postgres.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { schema } from 'prisma/drizzle/schema'
import { db } from 'src/lib/postgres'
import { testOneToOneType } from './shared/testOneToOneType'
import type { TestContext } from './utils/types'

const ctx: TestContext = { db, schema, provider: 'postgres' }

testOneToOneType(ctx)
25 changes: 23 additions & 2 deletions packages/usage/tests/shared/testOneToOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ import type { TestContext } from 'tests/utils/types'
export function testOneToOne({ db, schema }: TestContext) {
const b: typeof schema.oneToOneBs.$inferInsert = { id: createId() }
const a: typeof schema.oneToOneAs.$inferInsert = { id: createId(), bId: b.id }
const c: typeof schema.oneToOneCs.$inferInsert = { id: createId(), bId: b.id }

describe('one to one', () => {
beforeAll(async () => {
await db.insert(schema.oneToOneBs).values(b)
await db.insert(schema.oneToOneCs).values(c)
await db.insert(schema.oneToOneAs).values(a)
})

afterAll(async () => {
await db.delete(schema.oneToOneAs).where(eq(schema.oneToOneAs.id, a.id))
await db.delete(schema.oneToOneCs).where(eq(schema.oneToOneCs.id, c.id))
await db.delete(schema.oneToOneBs).where(eq(schema.oneToOneBs.id, b.id))
})

test('a.b (holds foreign key)', async () => {
test('a.b (holds foreign key - optional)', async () => {
const result = await db.query.oneToOneAs
.findFirst({
where: (oneToOneAs, { eq }) => eq(oneToOneAs.id, a.id),
Expand All @@ -34,19 +37,37 @@ export function testOneToOne({ db, schema }: TestContext) {
})
})

test('b.a (being referenced)', async () => {
test('c.b (holds foreign key - required)', async () => {
const result = await db.query.oneToOneCs
.findFirst({
where: (oneToOneCs, { eq }) => eq(oneToOneCs.id, c.id),
with: {
b: true,
},
})
.then(throwIfnull)

expect(result).toStrictEqual({
...c,
b,
})
})

test('b.{a,c} (being referenced)', async () => {
const result = await db.query.oneToOneBs
.findFirst({
where: (oneToOneBs, { eq }) => eq(oneToOneBs.id, b.id),
with: {
a: true,
c: true,
},
})
.then(throwIfnull)

expect(result).toStrictEqual({
...b,
a,
c,
})
})
})
Expand Down
65 changes: 65 additions & 0 deletions packages/usage/tests/shared/testOneToOneType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createId } from '@paralleldrive/cuid2'
import { eq } from 'drizzle-orm'
import { throwIfnull } from 'tests/utils/query'
import type { TestContext } from 'tests/utils/types'

export function testOneToOneType({ db, schema }: TestContext) {
const b: typeof schema.oneToOneBs.$inferInsert = { id: createId() }
const a: typeof schema.oneToOneAs.$inferInsert = { id: createId(), bId: b.id }
const c: typeof schema.oneToOneCs.$inferInsert = { id: createId(), bId: b.id }

describe('one to one (type)', () => {
beforeAll(async () => {
await db.insert(schema.oneToOneBs).values(b)
await db.insert(schema.oneToOneCs).values(c)
await db.insert(schema.oneToOneAs).values(a)
})

afterAll(async () => {
await db.delete(schema.oneToOneAs).where(eq(schema.oneToOneAs.id, a.id))
await db.delete(schema.oneToOneCs).where(eq(schema.oneToOneCs.id, c.id))
await db.delete(schema.oneToOneBs).where(eq(schema.oneToOneBs.id, b.id))
})

test('a.b (holds foreign key - optional)', async () => {
const result = await db.query.oneToOneAs
.findFirst({
where: (oneToOneAs, { eq }) => eq(oneToOneAs.id, a.id),
with: {
b: true,
},
})
.then(throwIfnull)

expectTypeOf(result.b).toBeNullable()
})

test('c.b (holds foreign key - required)', async () => {
const result = await db.query.oneToOneCs
.findFirst({
where: (oneToOneCs, { eq }) => eq(oneToOneCs.id, c.id),
with: {
b: true,
},
})
.then(throwIfnull)

expectTypeOf(result.b).not.toBeNullable()
})

test('b.{a,c} (being referenced)', async () => {
const result = await db.query.oneToOneBs
.findFirst({
where: (oneToOneBs, { eq }) => eq(oneToOneBs.id, b.id),
with: {
a: true,
c: true,
},
})
.then(throwIfnull)

expectTypeOf(result.a).toBeNullable()
expectTypeOf(result.c).toBeNullable()
})
})
}
13 changes: 13 additions & 0 deletions packages/usage/tests/sqlite.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { schema } from 'prisma/sqlite/drizzle/schema'
import { db } from 'src/lib/sqlite'
import type { Db, Schema } from 'src/lib/types'
import { testOneToOneType } from './shared/testOneToOneType'
import type { TestContext } from './utils/types'

const ctx: TestContext = {
db: db as unknown as Db,
schema: schema as unknown as Schema,
provider: 'sqlite',
}

testOneToOneType(ctx)
1 change: 1 addition & 0 deletions packages/usage/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"generate": {},
"pushreset:postgres": {
"cache": false,
"env": ["VITE_PG_DATABASE_URL"]
Expand Down
3 changes: 3 additions & 0 deletions packages/usage/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
typecheck: {
enabled: true,
},
},
})

0 comments on commit 1916e10

Please sign in to comment.