Skip to content

Commit 1e7b2af

Browse files
authored
feat: created nullable object option without getter (#257)
1 parent bbfdc60 commit 1e7b2af

8 files changed

+932
-18
lines changed

src/glossary.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { GraphQLSchema } from 'graphql'
22
import { GraphQLHandler, RestHandler } from 'msw'
33
import { Database } from './db/Database'
4-
import { NullableProperty } from './nullable'
4+
import { NullableObject, NullableProperty } from './nullable'
55
import { PrimaryKey } from './primaryKey'
66
import {
77
BulkQueryOptions,
@@ -28,6 +28,7 @@ export type ModelDefinitionValue =
2828
| PrimaryKey<any>
2929
| ModelValueTypeGetter
3030
| NullableProperty<any>
31+
| NullableObject<any>
3132
| OneOf<any, boolean>
3233
| ManyOf<any, boolean>
3334
| NestedModelDefinition
@@ -36,6 +37,7 @@ export type NestedModelDefinition = {
3637
[propertyName: string]:
3738
| ModelValueTypeGetter
3839
| NullableProperty<any>
40+
| NullableObject<any>
3941
| OneOf<any, boolean>
4042
| ManyOf<any, boolean>
4143
| NestedModelDefinition
@@ -221,6 +223,8 @@ export type Value<
221223
: // Extract underlying value type of nullable properties
222224
Target[Key] extends NullableProperty<any>
223225
? ReturnType<Target[Key]['getValue']>
226+
: Target[Key] extends NullableObject<any>
227+
? Partial<Value<Target[Key]['objectDefinition'], Dictionary>> | null
224228
: // Extract value type from OneOf relations.
225229
Target[Key] extends OneOf<infer ModelName, infer Nullable>
226230
? Nullable extends true

src/model/createModel.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import { ParsedModelDefinition } from './parseModelDefinition'
1717
import { defineRelationalProperties } from './defineRelationalProperties'
1818
import { PrimaryKey } from '../primaryKey'
1919
import { Relation } from '../relations/Relation'
20-
import { NullableProperty } from '../nullable'
20+
import { NullableObject, NullableProperty } from '../nullable'
2121
import { isModelValueType } from '../utils/isModelValueType'
22+
import { getDefinition } from './getDefinition'
2223

2324
const log = debug('createModel')
2425

@@ -51,7 +52,7 @@ export function createModel<
5152
const publicProperties = properties.reduce<Record<string, unknown>>(
5253
(properties, propertyName) => {
5354
const initialValue = get(initialValues, propertyName)
54-
const propertyDefinition = get(definition, propertyName)
55+
const propertyDefinition = getDefinition(definition, propertyName)
5556

5657
// Ignore relational properties at this stage.
5758
if (propertyDefinition instanceof Relation) {
@@ -77,6 +78,20 @@ export function createModel<
7778
return properties
7879
}
7980

81+
if (propertyDefinition instanceof NullableObject) {
82+
if (
83+
initialValue === null ||
84+
(propertyDefinition.defaultsToNull && initialValue === undefined)
85+
) {
86+
// this is for all the cases we want to override the inner values of
87+
// the nullable object and just set it to be null. it happens when:
88+
// 1. the initial value of the nullable object is null
89+
// 2. the initial value of the nullable object is not defined and the definition defaults to null
90+
set(properties, propertyName, null)
91+
}
92+
return properties
93+
}
94+
8095
invariant(
8196
initialValue !== null,
8297
'Failed to create a "%s" entity: a non-nullable property "%s" cannot be instantiated with null. Use the "nullable" function when defining this property to support nullable value.',

src/model/getDefinition.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { NullableObject, NullableProperty } from '../nullable'
2+
import { ModelDefinition } from '../glossary'
3+
import { isObject } from '../utils/isObject'
4+
import { isFunction } from 'lodash'
5+
6+
export function getDefinition(
7+
definition: ModelDefinition,
8+
propertyName: string[],
9+
) {
10+
return propertyName.reduce((reducedDefinition, property) => {
11+
const value = reducedDefinition[property]
12+
13+
if (value instanceof NullableProperty) {
14+
return value
15+
}
16+
17+
if (value instanceof NullableObject) {
18+
// in case the propertyName array includes NullableObject, we get
19+
// the NullableObject definition and continue the reduce loop
20+
if (property !== propertyName.at(-1)) {
21+
return value.objectDefinition
22+
}
23+
// in case the propertyName array ends with NullableObject, we just return it and if
24+
// it should get the value of null, it will override its inner properties
25+
return value
26+
}
27+
28+
// getter functions and nested objects
29+
if (isFunction(value) || isObject(value)) {
30+
return value
31+
}
32+
33+
return
34+
}, definition)
35+
}

src/model/parseModelDefinition.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { PrimaryKey } from '../primaryKey'
1010
import { isObject } from '../utils/isObject'
1111
import { Relation, RelationsList } from '../relations/Relation'
12-
import { NullableProperty } from '../nullable'
12+
import { NullableObject, NullableProperty } from '../nullable'
1313

1414
const log = debug('parseModelDefinition')
1515

@@ -76,6 +76,21 @@ function deepParseModelDefinition<Dictionary extends ModelDictionary>(
7676
continue
7777
}
7878

79+
if (value instanceof NullableObject) {
80+
deepParseModelDefinition(
81+
dictionary,
82+
modelName,
83+
value.objectDefinition,
84+
propertyPath,
85+
result,
86+
)
87+
88+
// after the recursion calls we want to set the nullable object itself to be part of the properties
89+
// because in case it will get the value of null we want to override its inner values
90+
result.properties.push(propertyPath)
91+
continue
92+
}
93+
7994
// Relations.
8095
if (value instanceof Relation) {
8196
// Store the relations in a separate object.

src/model/updateEntity.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { Relation, RelationKind } from '../relations/Relation'
66
import { ENTITY_TYPE, PRIMARY_KEY, Entity, ModelDefinition } from '../glossary'
77
import { isObject } from '../utils/isObject'
88
import { inheritInternalProperties } from '../utils/inheritInternalProperties'
9-
import { NullableProperty } from '../nullable'
9+
import { NullableObject, NullableProperty } from '../nullable'
1010
import { spread } from '../utils/spread'
11+
import { getDefinition } from './getDefinition'
1112

1213
const log = debug('updateEntity')
1314

@@ -38,7 +39,8 @@ export function updateEntity(
3839
typeof value === 'function' ? value(prevValue, entity) : value
3940
log('next value for "%s":', propertyPath, nextValue)
4041

41-
const propertyDefinition = get(definition, propertyPath)
42+
const propertyDefinition = getDefinition(definition, propertyPath)
43+
4244
log('property definition for "%s":', propertyPath, propertyDefinition)
4345

4446
if (propertyDefinition == null) {
@@ -183,7 +185,9 @@ export function updateEntity(
183185
}
184186

185187
invariant(
186-
nextValue !== null || propertyDefinition instanceof NullableProperty,
188+
nextValue !== null ||
189+
propertyDefinition instanceof NullableProperty ||
190+
propertyDefinition instanceof NullableObject,
187191
'Failed to update "%s" on "%s": cannot set a non-nullable property to null.',
188192
propertyName,
189193
entity[ENTITY_TYPE],

src/nullable.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
import { ModelValueType } from './glossary'
1+
import { ModelValueType, NestedModelDefinition } from './glossary'
22
import { ManyOf, OneOf, Relation, RelationKind } from './relations/Relation'
33

4+
export class NullableObject<ValueType extends NestedModelDefinition> {
5+
public objectDefinition: ValueType
6+
public defaultsToNull: boolean
7+
8+
constructor(definition: ValueType, defaultsToNull: boolean) {
9+
this.objectDefinition = definition
10+
this.defaultsToNull = defaultsToNull
11+
}
12+
}
13+
414
export type NullableGetter<ValueType extends ModelValueType> =
515
() => ValueType | null
616

@@ -12,14 +22,21 @@ export class NullableProperty<ValueType extends ModelValueType> {
1222
}
1323
}
1424

25+
export function nullable<ValueType extends NestedModelDefinition>(
26+
value: ValueType,
27+
options?: { defaultsToNull?: boolean },
28+
): NullableObject<ValueType>
29+
1530
export function nullable<ValueType extends ModelValueType>(
1631
value: NullableGetter<ValueType>,
32+
options?: { defaultsToNull?: boolean },
1733
): NullableProperty<ValueType>
1834

1935
export function nullable<
2036
ValueType extends Relation<any, any, any, { nullable: false }>,
2137
>(
2238
value: ValueType,
39+
options?: { defaultsToNull?: boolean },
2340
): ValueType extends Relation<infer Kind, infer Key, any, { nullable: false }>
2441
? Kind extends RelationKind.ManyOf
2542
? ManyOf<Key, true>
@@ -29,18 +46,26 @@ export function nullable<
2946
export function nullable(
3047
value:
3148
| NullableGetter<ModelValueType>
32-
| Relation<any, any, any, { nullable: false }>,
49+
| Relation<any, any, any, { nullable: false }>
50+
| NestedModelDefinition,
51+
options?: { defaultsToNull?: boolean },
3352
) {
53+
if (value instanceof Relation) {
54+
return new Relation({
55+
kind: value.kind,
56+
to: value.target.modelName,
57+
attributes: {
58+
...value.attributes,
59+
nullable: true,
60+
},
61+
})
62+
}
63+
64+
if (typeof value === 'object') {
65+
return new NullableObject(value, !!options?.defaultsToNull)
66+
}
67+
3468
if (typeof value === 'function') {
3569
return new NullableProperty(value)
3670
}
37-
38-
return new Relation({
39-
kind: value.kind,
40-
to: value.target.modelName,
41-
attributes: {
42-
...value.attributes,
43-
nullable: true,
44-
},
45-
})
4671
}

0 commit comments

Comments
 (0)