Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make oneOf relation required if non-nullable #279

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
lib
*.log
*.log
.idea
2 changes: 1 addition & 1 deletion src/glossary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export type Value<
Target[Key] extends OneOf<infer ModelName, infer Nullable>
? Nullable extends true
? PublicEntity<Dictionary, ModelName> | null
: PublicEntity<Dictionary, ModelName> | undefined
: PublicEntity<Dictionary, ModelName>
: // Extract value type from ManyOf relations.
Target[Key] extends ManyOf<infer ModelName, infer Nullable>
? Nullable extends true
Expand Down
38 changes: 26 additions & 12 deletions src/model/defineRelationalProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ import {
PRIMARY_KEY,
Value,
} from '../glossary'
import { RelationKind, RelationsList } from '../relations/Relation'
import { Relation, RelationKind, RelationsList } from '../relations/Relation'

const log = debug('defineRelationalProperties')

const getInvariantMessagePrefix = (
entity: Entity<any, any>,
relation: Relation<any, any, any, any>,
propertyPath: string[],
) =>
`Failed to define a "${relation.kind}" relationship to "${
relation.target.modelName
}" at "${entity[ENTITY_TYPE]}.${propertyPath.join('.')}" (${
entity[PRIMARY_KEY]
}: "${entity[entity[PRIMARY_KEY]]}")`

export function defineRelationalProperties(
entity: Entity<any, any>,
initialValues: Partial<Value<any, ModelDictionary>>,
Expand All @@ -23,12 +34,15 @@ export function defineRelationalProperties(
log('defining relational properties...', { entity, initialValues, relations })

for (const { propertyPath, relation } of relations) {
const invariantMessagePrefix = getInvariantMessagePrefix(
entity,
relation,
propertyPath,
)

invariant(
dictionary[relation.target.modelName],
'Failed to define a "%s" relational property to "%s" on "%s": cannot find a model by the name "%s".',
relation.kind,
propertyPath.join('.'),
entity[ENTITY_TYPE],
`${invariantMessagePrefix}: cannot find a model by the name "%s".`,
relation.target.modelName,
)

Expand All @@ -39,14 +53,14 @@ export function defineRelationalProperties(

invariant(
references !== null || relation.attributes.nullable,
'Failed to define a "%s" relationship to "%s" at "%s.%s" (%s: "%s"): cannot set a non-nullable relationship to null.',
`${invariantMessagePrefix}: cannot set a non-nullable relationship to null.`,
)

relation.kind,
relation.target.modelName,
entity[ENTITY_TYPE],
propertyPath.join('.'),
entity[PRIMARY_KEY],
entity[entity[PRIMARY_KEY]],
invariant(
relation.kind !== RelationKind.OneOf ||
references !== undefined ||
relation.attributes.nullable,
`${invariantMessagePrefix}: a value must be provided for a non-nullable relationship.`,
)

log(
Expand Down
21 changes: 20 additions & 1 deletion test/model/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { faker } from '@faker-js/faker'
import { NullableObject, NullableProperty } from '../../src/nullable'
import { factory, primaryKey, oneOf, manyOf, nullable } from '../../src'
import { identity } from '../../src/utils/identity'

Expand Down Expand Up @@ -709,3 +708,23 @@ describe('nullable objects with complex structure and null definition', () => {
})
})
})

test('throws an exception when value is not provided for a non-nullable oneOf relation', () => {
const db = factory({
user: {
id: primaryKey(String),
city: oneOf('city'),
},
city: {
id: primaryKey(String),
},
})

expect(() => {
db.user.create({
id: 'user-1',
})
}).toThrowError(
'Failed to define a "ONE_OF" relationship to "city" at "user.city" (id: "user-1"): a value must be provided for a non-nullable relationship.',
)
})
4 changes: 2 additions & 2 deletions test/model/relationalProperties.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const db = factory({
post: {
id: primaryKey(String),
text: String,
author: oneOf('user'),
author: nullable(oneOf('user')),
reply: nullable(oneOf('post')),
likedBy: nullable(manyOf('user')),
},
Expand All @@ -17,7 +17,7 @@ const db = factory({
const user = db.user.create()
const post = db.post.create()

// @ts-expect-error author is potentially undefined
// @ts-expect-error author is potentially null
post.author.id

// @ts-expect-error reply is potentially null
Expand Down
3 changes: 2 additions & 1 deletion test/model/relationalProperties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
RelationsList,
} from '../../src/relations/Relation'
import { defineRelationalProperties } from '../../src/model/defineRelationalProperties'
import { testFactory } from '../../test/testUtils'
import { testFactory } from '../testUtils'

it('marks relational properties as enumerable', () => {
const { db, dictionary, databaseInstance } = testFactory({
Expand All @@ -27,6 +27,7 @@ it('marks relational properties as enumerable', () => {
const post = db.post.create({
id: 'post-1',
title: 'Test Post',
author: user,
})

const relations: RelationsList = [
Expand Down
14 changes: 7 additions & 7 deletions test/relations/bi-directional.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/**
* @see https://github.com/mswjs/data/issues/139
*/
import { factory, manyOf, oneOf, primaryKey } from '@mswjs/data'
import { factory, manyOf, oneOf, primaryKey, nullable } from '@mswjs/data'

test('supports creating a bi-directional one-to-one relationship', () => {
const db = factory({
user: {
id: primaryKey(String),
partner: oneOf('user'),
partner: nullable(oneOf('user')),
},
})

Expand Down Expand Up @@ -117,7 +117,7 @@ test('supports querying by a bi-directional one-to-one relationship', () => {
const db = factory({
user: {
id: primaryKey(String),
partner: oneOf('user'),
partner: nullable(oneOf('user'))
},
})

Expand Down Expand Up @@ -202,8 +202,8 @@ test('supports querying by a bi-directional one-to-many relationship', () => {
})

// Create unrelated user and post to ensure they are not included.
db.user.create({ id: 'user-unrelated' })
db.post.create({ id: 'post-unrelated' })
const unrelatedUser = db.user.create({ id: 'user-unrelated' })
db.post.create({ id: 'post-unrelated', author: unrelatedUser })

// Find posts in one-to-many direction
const posts = db.post.findMany({
Expand Down Expand Up @@ -304,7 +304,7 @@ test('supports updating using an entity with a bi-directional one-to-one relatio
const db = factory({
user: {
id: primaryKey(String),
partner: oneOf('user'),
partner: nullable(oneOf('user')),
},
})

Expand Down Expand Up @@ -343,7 +343,7 @@ test('supports updating using an entity with a bi-directional one-to-many relati
post: {
id: primaryKey(String),
title: String,
author: oneOf('user'),
author: nullable(oneOf('user')),
},
})

Expand Down
5 changes: 3 additions & 2 deletions test/relations/many-to-one.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ test('updates a many-to-one relational property without initial value', () => {
},
post: {
id: primaryKey(String),
author: oneOf('user'),
author: nullable(oneOf('user')),
},
})

Expand Down Expand Up @@ -423,7 +423,7 @@ test('does not throw any error when a many-to-one entity is created without a re
post: {
id: primaryKey(String),
title: String,
author: oneOf('user'),
author: nullable(oneOf('user')),
},
})

Expand All @@ -437,6 +437,7 @@ test('does not throw any error when a many-to-one entity is created without a re
[PRIMARY_KEY]: 'id',
id: 'post-1',
title: 'First post',
author: null,
})
})

Expand Down
51 changes: 7 additions & 44 deletions test/relations/one-to-one.create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ it('creates a non-nullable relationship', () => {
).toEqual(expectedCountry)
})

it('creates a non-nullable relationship without the initial value', () => {
it('forbids creating a non-nullable relationship without the initial value', () => {
const { db, entity } = testFactory({
country: {
code: primaryKey(String),
Expand All @@ -242,24 +242,13 @@ it('creates a non-nullable relationship without the initial value', () => {
},
})

const country = db.country.create({
code: 'uk',
})

const expectedCountry = entity('country', {
code: 'uk',
capital: undefined,
})

expect(country).toEqual(expectedCountry)
expect(db.country.findFirst({ where: { code: { equals: 'uk' } } })).toEqual(
expectedCountry,
expect(() =>
db.country.create({
code: 'uk',
})
).toThrow(
'Failed to define a "ONE_OF" relationship to "city" at "country.capital" (code: "uk"): a value must be provided for a non-nullable relationship.',
)
expect(
db.country.findFirst({
where: { capital: { name: { equals: 'Manchester' } } },
}),
).toEqual(null)
})

it('forbids creating a non-nullable relationship with null as initial value', () => {
Expand Down Expand Up @@ -350,32 +339,6 @@ it('creates a non-nullable unique relationship with initial value', () => {
).toEqual(expectedCountry)
})

it('creates a non-nullable unique relationship without initial value', () => {
const { db, entity } = testFactory({
country: {
code: primaryKey(String),
capital: oneOf('city', { unique: true }),
},
city: {
name: primaryKey(String),
},
})

const country = db.country.create({
code: 'uk',
})

const expectedCountry = entity('country', {
code: 'uk',
capital: undefined,
})

expect(country).toEqual(expectedCountry)
expect(db.country.findFirst({ where: { code: { equals: 'uk' } } })).toEqual(
expectedCountry,
)
})

it('forbids creating a unique relationship to already referenced entity', () => {
const { db } = testFactory({
country: {
Expand Down
26 changes: 0 additions & 26 deletions test/relations/one-to-one.operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,32 +53,6 @@ it('supports querying through a non-nullable relationship with initial value', (
).toEqual(null)
})

it('supports querying through a non-nullable relationship without initial value', () => {
const { db } = testFactory({
country: {
code: primaryKey(String),
capital: oneOf('city'),
},
city: {
name: primaryKey(String),
},
})

db.country.create({
code: 'uk',
})

// Querying through the relationship is permitted
// but since it hasn't been set, no queries will match.
expect(
db.country.findFirst({
where: {
capital: { name: { equals: 'London' } },
},
}),
).toEqual(null)
})

it('supports querying through a deeply nested non-nullable relationship', () => {
const { db, entity } = testFactory({
user: {
Expand Down
Loading
Loading