Skip to content

Commit

Permalink
feat: make oneOf relation required if non-nullable
Browse files Browse the repository at this point in the history
  • Loading branch information
timkolotov committed Jun 13, 2023
1 parent b51f8e1 commit ca448f9
Show file tree
Hide file tree
Showing 11 changed files with 71 additions and 203 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules
lib
*.log
coverage
coverage
.idea
2 changes: 1 addition & 1 deletion src/glossary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,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
23 changes: 21 additions & 2 deletions test/model/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { faker } from '@faker-js/faker'
import { factory, primaryKey, oneOf, manyOf, nullable } from '../../src'
import { identity } from '../../src/utils/identity'
import { factory, identity, primaryKey, oneOf, manyOf, nullable } from '../../src'

test('creates a new entity', () => {
const userId = faker.datatype.uuid()
Expand Down Expand Up @@ -255,3 +254,23 @@ test('throws an exception when null used as initial value for non-nullable relat
'Failed to define a "MANY_OF" relationship to "post" at "user.posts" (id: "user-1"): cannot set a non-nullable relationship to null.',
)
})

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

0 comments on commit ca448f9

Please sign in to comment.