Skip to content

Commit 8ff30ad

Browse files
committed
feat: make oneOf relation required if non-nullable
1 parent 313d921 commit 8ff30ad

11 files changed

+70
-202
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
lib
3-
*.log
3+
*.log
4+
.idea

src/glossary.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export type Value<
229229
Target[Key] extends OneOf<infer ModelName, infer Nullable>
230230
? Nullable extends true
231231
? PublicEntity<Dictionary, ModelName> | null
232-
: PublicEntity<Dictionary, ModelName> | undefined
232+
: PublicEntity<Dictionary, ModelName>
233233
: // Extract value type from ManyOf relations.
234234
Target[Key] extends ManyOf<infer ModelName, infer Nullable>
235235
? Nullable extends true

src/model/defineRelationalProperties.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,21 @@ import {
99
PRIMARY_KEY,
1010
Value,
1111
} from '../glossary'
12-
import { RelationKind, RelationsList } from '../relations/Relation'
12+
import { Relation, RelationKind, RelationsList } from '../relations/Relation'
1313

1414
const log = debug('defineRelationalProperties')
1515

16+
const getInvariantMessagePrefix = (
17+
entity: Entity<any, any>,
18+
relation: Relation<any, any, any, any>,
19+
propertyPath: string[],
20+
) =>
21+
`Failed to define a "${relation.kind}" relationship to "${
22+
relation.target.modelName
23+
}" at "${entity[ENTITY_TYPE]}.${propertyPath.join('.')}" (${
24+
entity[PRIMARY_KEY]
25+
}: "${entity[entity[PRIMARY_KEY]]}")`
26+
1627
export function defineRelationalProperties(
1728
entity: Entity<any, any>,
1829
initialValues: Partial<Value<any, ModelDictionary>>,
@@ -23,12 +34,15 @@ export function defineRelationalProperties(
2334
log('defining relational properties...', { entity, initialValues, relations })
2435

2536
for (const { propertyPath, relation } of relations) {
37+
const invariantMessagePrefix = getInvariantMessagePrefix(
38+
entity,
39+
relation,
40+
propertyPath,
41+
)
42+
2643
invariant(
2744
dictionary[relation.target.modelName],
28-
'Failed to define a "%s" relational property to "%s" on "%s": cannot find a model by the name "%s".',
29-
relation.kind,
30-
propertyPath.join('.'),
31-
entity[ENTITY_TYPE],
45+
`${invariantMessagePrefix}: cannot find a model by the name "%s".`,
3246
relation.target.modelName,
3347
)
3448

@@ -39,14 +53,14 @@ export function defineRelationalProperties(
3953

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

44-
relation.kind,
45-
relation.target.modelName,
46-
entity[ENTITY_TYPE],
47-
propertyPath.join('.'),
48-
entity[PRIMARY_KEY],
49-
entity[entity[PRIMARY_KEY]],
59+
invariant(
60+
relation.kind !== RelationKind.OneOf ||
61+
references !== undefined ||
62+
relation.attributes.nullable,
63+
`${invariantMessagePrefix}: a value must be provided for a non-nullable relationship.`,
5064
)
5165

5266
log(

test/model/create.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { faker } from '@faker-js/faker'
2-
import { NullableObject, NullableProperty } from '../../src/nullable'
32
import { factory, primaryKey, oneOf, manyOf, nullable } from '../../src'
43
import { identity } from '../../src/utils/identity'
54

@@ -709,3 +708,23 @@ describe('nullable objects with complex structure and null definition', () => {
709708
})
710709
})
711710
})
711+
712+
test('throws an exception when value is not provided for a non-nullable oneOf relation', () => {
713+
const db = factory({
714+
user: {
715+
id: primaryKey(String),
716+
city: oneOf('city'),
717+
},
718+
city: {
719+
id: primaryKey(String),
720+
},
721+
})
722+
723+
expect(() => {
724+
db.user.create({
725+
id: 'user-1',
726+
})
727+
}).toThrowError(
728+
'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.',
729+
)
730+
})

test/model/relationalProperties.test-d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const db = factory({
88
post: {
99
id: primaryKey(String),
1010
text: String,
11-
author: oneOf('user'),
11+
author: nullable(oneOf('user')),
1212
reply: nullable(oneOf('post')),
1313
likedBy: nullable(manyOf('user')),
1414
},
@@ -17,7 +17,7 @@ const db = factory({
1717
const user = db.user.create()
1818
const post = db.post.create()
1919

20-
// @ts-expect-error author is potentially undefined
20+
// @ts-expect-error author is potentially null
2121
post.author.id
2222

2323
// @ts-expect-error reply is potentially null

test/model/relationalProperties.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
RelationsList,
66
} from '../../src/relations/Relation'
77
import { defineRelationalProperties } from '../../src/model/defineRelationalProperties'
8-
import { testFactory } from '../../test/testUtils'
8+
import { testFactory } from '../testUtils'
99

1010
it('marks relational properties as enumerable', () => {
1111
const { db, dictionary, databaseInstance } = testFactory({
@@ -27,6 +27,7 @@ it('marks relational properties as enumerable', () => {
2727
const post = db.post.create({
2828
id: 'post-1',
2929
title: 'Test Post',
30+
author: user,
3031
})
3132

3233
const relations: RelationsList = [

test/relations/bi-directional.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/**
22
* @see https://github.com/mswjs/data/issues/139
33
*/
4-
import { factory, manyOf, oneOf, primaryKey } from '@mswjs/data'
4+
import { factory, manyOf, oneOf, primaryKey, nullable } from '@mswjs/data'
55

66
test('supports creating a bi-directional one-to-one relationship', () => {
77
const db = factory({
88
user: {
99
id: primaryKey(String),
10-
partner: oneOf('user'),
10+
partner: nullable(oneOf('user')),
1111
},
1212
})
1313

@@ -117,7 +117,7 @@ test('supports querying by a bi-directional one-to-one relationship', () => {
117117
const db = factory({
118118
user: {
119119
id: primaryKey(String),
120-
partner: oneOf('user'),
120+
partner: nullable(oneOf('user'))
121121
},
122122
})
123123

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

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

208208
// Find posts in one-to-many direction
209209
const posts = db.post.findMany({
@@ -304,7 +304,7 @@ test('supports updating using an entity with a bi-directional one-to-one relatio
304304
const db = factory({
305305
user: {
306306
id: primaryKey(String),
307-
partner: oneOf('user'),
307+
partner: nullable(oneOf('user')),
308308
},
309309
})
310310

@@ -343,7 +343,7 @@ test('supports updating using an entity with a bi-directional one-to-many relati
343343
post: {
344344
id: primaryKey(String),
345345
title: String,
346-
author: oneOf('user'),
346+
author: nullable(oneOf('user')),
347347
},
348348
})
349349

test/relations/many-to-one.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ test('updates a many-to-one relational property without initial value', () => {
325325
},
326326
post: {
327327
id: primaryKey(String),
328-
author: oneOf('user'),
328+
author: nullable(oneOf('user')),
329329
},
330330
})
331331

@@ -423,7 +423,7 @@ test('does not throw any error when a many-to-one entity is created without a re
423423
post: {
424424
id: primaryKey(String),
425425
title: String,
426-
author: oneOf('user'),
426+
author: nullable(oneOf('user')),
427427
},
428428
})
429429

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

test/relations/one-to-one.create.test.ts

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ it('creates a non-nullable relationship', () => {
231231
).toEqual(expectedCountry)
232232
})
233233

234-
it('creates a non-nullable relationship without the initial value', () => {
234+
it('forbids creating a non-nullable relationship without the initial value', () => {
235235
const { db, entity } = testFactory({
236236
country: {
237237
code: primaryKey(String),
@@ -242,24 +242,13 @@ it('creates a non-nullable relationship without the initial value', () => {
242242
},
243243
})
244244

245-
const country = db.country.create({
246-
code: 'uk',
247-
})
248-
249-
const expectedCountry = entity('country', {
250-
code: 'uk',
251-
capital: undefined,
252-
})
253-
254-
expect(country).toEqual(expectedCountry)
255-
expect(db.country.findFirst({ where: { code: { equals: 'uk' } } })).toEqual(
256-
expectedCountry,
245+
expect(() =>
246+
db.country.create({
247+
code: 'uk',
248+
})
249+
).toThrow(
250+
'Failed to define a "ONE_OF" relationship to "city" at "country.capital" (code: "uk"): a value must be provided for a non-nullable relationship.',
257251
)
258-
expect(
259-
db.country.findFirst({
260-
where: { capital: { name: { equals: 'Manchester' } } },
261-
}),
262-
).toEqual(null)
263252
})
264253

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

353-
it('creates a non-nullable unique relationship without initial value', () => {
354-
const { db, entity } = testFactory({
355-
country: {
356-
code: primaryKey(String),
357-
capital: oneOf('city', { unique: true }),
358-
},
359-
city: {
360-
name: primaryKey(String),
361-
},
362-
})
363-
364-
const country = db.country.create({
365-
code: 'uk',
366-
})
367-
368-
const expectedCountry = entity('country', {
369-
code: 'uk',
370-
capital: undefined,
371-
})
372-
373-
expect(country).toEqual(expectedCountry)
374-
expect(db.country.findFirst({ where: { code: { equals: 'uk' } } })).toEqual(
375-
expectedCountry,
376-
)
377-
})
378-
379342
it('forbids creating a unique relationship to already referenced entity', () => {
380343
const { db } = testFactory({
381344
country: {

test/relations/one-to-one.operations.test.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,32 +53,6 @@ it('supports querying through a non-nullable relationship with initial value', (
5353
).toEqual(null)
5454
})
5555

56-
it('supports querying through a non-nullable relationship without initial value', () => {
57-
const { db } = testFactory({
58-
country: {
59-
code: primaryKey(String),
60-
capital: oneOf('city'),
61-
},
62-
city: {
63-
name: primaryKey(String),
64-
},
65-
})
66-
67-
db.country.create({
68-
code: 'uk',
69-
})
70-
71-
// Querying through the relationship is permitted
72-
// but since it hasn't been set, no queries will match.
73-
expect(
74-
db.country.findFirst({
75-
where: {
76-
capital: { name: { equals: 'London' } },
77-
},
78-
}),
79-
).toEqual(null)
80-
})
81-
8256
it('supports querying through a deeply nested non-nullable relationship', () => {
8357
const { db, entity } = testFactory({
8458
user: {

0 commit comments

Comments
 (0)