Skip to content

Commit 6d771d1

Browse files
borisno2claude
andauthored
Fix regression: list-only many-to-many relationships not generating synthetic back-reference fields (#356)
In 0.19.0, a change to the synthetic field generation condition (from `joinTableNaming !== 'keystone'` to `!m2mCheck.isManyToMany`) fixed one-sided many-to-one relationships with Keystone naming but accidentally broke list-only many-to-many relationships. Prisma requires both sides of any implicit many-to-many relationship to be defined. Without the synthetic back-reference field on the target model, Prisma throws a validation error: Error validating field `readBy` in model `TextMessage`: The relation field `readBy` on model `TextMessage` is missing an opposite relation field on the model `User`. Fix: always generate synthetic back-reference fields for list-only refs (no `targetField`), regardless of whether the relationship is many-to-many or many-to-one. The relation name now correctly uses `db.relationName` when explicitly set, falling back to the auto-generated `${listName}_${fieldName}`. Updates two incorrect tests that asserted synthetic fields were NOT generated for list-only many-to-many (both with Keystone naming and explicit relationName), and adds a dedicated regression test matching the reported bug scenario. https://claude.ai/code/session_01TQUjki3U3XpDWmvU3AbYNr Co-authored-by: Claude <noreply@anthropic.com>
1 parent cf3bca4 commit 6d771d1

3 files changed

Lines changed: 59 additions & 11 deletions

File tree

.changeset/silent-roads-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@opensaas/stack-cli': patch
3+
---
4+
5+
Fix regression where list-only many-to-many relationships no longer generated synthetic back-reference fields on the target model, causing Prisma schema validation errors

packages/cli/src/generator/prisma.test.ts

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,41 @@ describe('Prisma Schema Generator', () => {
319319
expect(schema).toContain('from_Post_category Post[] @relation("Post_category")')
320320
})
321321

322+
it('should generate synthetic back-reference for list-only many-to-many (regression: 0.19.0)', () => {
323+
// Regression test: relationship({ ref: 'User', many: true }) stopped generating
324+
// back-reference fields on the target model in 0.19.0, causing Prisma validation errors:
325+
// "The relation field `readBy` on model `TextMessage` is missing an opposite relation field"
326+
const config: OpenSaasConfig = {
327+
db: {
328+
provider: 'sqlite',
329+
},
330+
lists: {
331+
User: {
332+
fields: {
333+
name: text(),
334+
},
335+
},
336+
TextMessage: {
337+
fields: {
338+
content: text(),
339+
readBy: relationship({ ref: 'User', many: true }),
340+
},
341+
},
342+
},
343+
}
344+
345+
const schema = generatePrismaSchema(config)
346+
347+
// TextMessage should have the relationship with named relation
348+
expect(schema).toContain('readBy User[] @relation("TextMessage_readBy")')
349+
350+
// User MUST have the synthetic back-reference — Prisma requires both sides
351+
// of an implicit many-to-many to be defined or it throws a validation error
352+
expect(schema).toContain(
353+
'from_TextMessage_readBy TextMessage[] @relation("TextMessage_readBy")',
354+
)
355+
})
356+
322357
it('should handle multiple list-only refs pointing to same target', () => {
323358
const config: OpenSaasConfig = {
324359
db: {
@@ -999,7 +1034,7 @@ describe('Prisma Schema Generator', () => {
9991034
expect(schema).toContain('@relation("Post_tags")')
10001035
})
10011036

1002-
it('should not generate synthetic fields for list-only many-to-many with Keystone naming', () => {
1037+
it('should generate synthetic fields for list-only many-to-many with Keystone naming', () => {
10031038
const config: OpenSaasConfig = {
10041039
db: {
10051040
provider: 'sqlite',
@@ -1022,8 +1057,12 @@ describe('Prisma Schema Generator', () => {
10221057

10231058
const schema = generatePrismaSchema(config)
10241059

1025-
// Should NOT have synthetic field (handled by implicit join table)
1026-
expect(schema).not.toContain('from_Post_tags')
1060+
// Post should have the relationship field with Keystone relation name
1061+
expect(schema).toContain('tags Tag[] @relation("Post_tags")')
1062+
1063+
// Tag MUST have a synthetic back-reference field — Prisma requires both sides
1064+
// of an implicit many-to-many relationship to be explicitly defined
1065+
expect(schema).toContain('from_Post_tags Post[] @relation("Post_tags")')
10271066
})
10281067

10291068
it('should generate synthetic fields for one-sided many-to-one with Keystone naming', () => {
@@ -1248,10 +1287,12 @@ describe('Prisma Schema Generator', () => {
12481287

12491288
const schema = generatePrismaSchema(config)
12501289

1251-
// Should use per-field relationName
1290+
// Should use per-field relationName on the source side
12521291
expect(schema).toContain('tags Tag[] @relation("Post_tags")')
1253-
// Should NOT create synthetic field
1254-
expect(schema).not.toContain('from_Post_tags')
1292+
1293+
// Tag MUST have a synthetic back-reference using the same explicit relation name
1294+
// Prisma requires both sides of implicit many-to-many to be defined
1295+
expect(schema).toContain('from_Post_tags Post[] @relation("Post_tags")')
12551296
})
12561297
})
12571298

packages/cli/src/generator/prisma.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -404,12 +404,14 @@ export function generatePrismaSchema(config: OpenSaasConfig): string {
404404
}
405405
}
406406

407-
// If no target field specified, we need to add a synthetic field
408-
// (unless it's a many-to-many relationship or per-field relationName is set)
409-
const hasExplicitRelationName = relField.db?.relationName
410-
if (!targetField && !hasExplicitRelationName && !m2mCheck.isManyToMany) {
407+
// If no target field specified, we need to add a synthetic back-reference field
408+
// on the target model. Prisma requires both sides of any relationship to be
409+
// defined, including implicit many-to-many relationships.
410+
// This applies to all list-only refs (both many-to-many and many-to-one).
411+
if (!targetField) {
411412
const syntheticFieldName = `from_${listName}_${fieldName}`
412-
const relationName = `${listName}_${fieldName}`
413+
// Use explicit relation name if set, otherwise auto-generate
414+
const relationName = relField.db?.relationName ?? `${listName}_${fieldName}`
413415

414416
if (!syntheticFields.has(targetList)) {
415417
syntheticFields.set(targetList, [])

0 commit comments

Comments
 (0)