Skip to content

Commit

Permalink
feat: created nullable object option without getter (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
yishayweb authored Oct 8, 2023
1 parent bbfdc60 commit 1e7b2af
Show file tree
Hide file tree
Showing 8 changed files with 932 additions and 18 deletions.
6 changes: 5 additions & 1 deletion src/glossary.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GraphQLSchema } from 'graphql'
import { GraphQLHandler, RestHandler } from 'msw'
import { Database } from './db/Database'
import { NullableProperty } from './nullable'
import { NullableObject, NullableProperty } from './nullable'
import { PrimaryKey } from './primaryKey'
import {
BulkQueryOptions,
Expand All @@ -28,6 +28,7 @@ export type ModelDefinitionValue =
| PrimaryKey<any>
| ModelValueTypeGetter
| NullableProperty<any>
| NullableObject<any>
| OneOf<any, boolean>
| ManyOf<any, boolean>
| NestedModelDefinition
Expand All @@ -36,6 +37,7 @@ export type NestedModelDefinition = {
[propertyName: string]:
| ModelValueTypeGetter
| NullableProperty<any>
| NullableObject<any>
| OneOf<any, boolean>
| ManyOf<any, boolean>
| NestedModelDefinition
Expand Down Expand Up @@ -221,6 +223,8 @@ export type Value<
: // Extract underlying value type of nullable properties
Target[Key] extends NullableProperty<any>
? ReturnType<Target[Key]['getValue']>
: Target[Key] extends NullableObject<any>
? Partial<Value<Target[Key]['objectDefinition'], Dictionary>> | null
: // Extract value type from OneOf relations.
Target[Key] extends OneOf<infer ModelName, infer Nullable>
? Nullable extends true
Expand Down
19 changes: 17 additions & 2 deletions src/model/createModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import { ParsedModelDefinition } from './parseModelDefinition'
import { defineRelationalProperties } from './defineRelationalProperties'
import { PrimaryKey } from '../primaryKey'
import { Relation } from '../relations/Relation'
import { NullableProperty } from '../nullable'
import { NullableObject, NullableProperty } from '../nullable'
import { isModelValueType } from '../utils/isModelValueType'
import { getDefinition } from './getDefinition'

const log = debug('createModel')

Expand Down Expand Up @@ -51,7 +52,7 @@ export function createModel<
const publicProperties = properties.reduce<Record<string, unknown>>(
(properties, propertyName) => {
const initialValue = get(initialValues, propertyName)
const propertyDefinition = get(definition, propertyName)
const propertyDefinition = getDefinition(definition, propertyName)

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

if (propertyDefinition instanceof NullableObject) {
if (
initialValue === null ||
(propertyDefinition.defaultsToNull && initialValue === undefined)
) {
// this is for all the cases we want to override the inner values of
// the nullable object and just set it to be null. it happens when:
// 1. the initial value of the nullable object is null
// 2. the initial value of the nullable object is not defined and the definition defaults to null
set(properties, propertyName, null)
}
return properties
}

invariant(
initialValue !== null,
'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.',
Expand Down
35 changes: 35 additions & 0 deletions src/model/getDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NullableObject, NullableProperty } from '../nullable'
import { ModelDefinition } from '../glossary'
import { isObject } from '../utils/isObject'
import { isFunction } from 'lodash'

export function getDefinition(
definition: ModelDefinition,
propertyName: string[],
) {
return propertyName.reduce((reducedDefinition, property) => {
const value = reducedDefinition[property]

if (value instanceof NullableProperty) {
return value
}

if (value instanceof NullableObject) {
// in case the propertyName array includes NullableObject, we get
// the NullableObject definition and continue the reduce loop
if (property !== propertyName.at(-1)) {
return value.objectDefinition
}
// in case the propertyName array ends with NullableObject, we just return it and if
// it should get the value of null, it will override its inner properties
return value
}

// getter functions and nested objects
if (isFunction(value) || isObject(value)) {
return value
}

return
}, definition)
}
17 changes: 16 additions & 1 deletion src/model/parseModelDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { PrimaryKey } from '../primaryKey'
import { isObject } from '../utils/isObject'
import { Relation, RelationsList } from '../relations/Relation'
import { NullableProperty } from '../nullable'
import { NullableObject, NullableProperty } from '../nullable'

const log = debug('parseModelDefinition')

Expand Down Expand Up @@ -76,6 +76,21 @@ function deepParseModelDefinition<Dictionary extends ModelDictionary>(
continue
}

if (value instanceof NullableObject) {
deepParseModelDefinition(
dictionary,
modelName,
value.objectDefinition,
propertyPath,
result,
)

// after the recursion calls we want to set the nullable object itself to be part of the properties
// because in case it will get the value of null we want to override its inner values
result.properties.push(propertyPath)
continue
}

// Relations.
if (value instanceof Relation) {
// Store the relations in a separate object.
Expand Down
10 changes: 7 additions & 3 deletions src/model/updateEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { Relation, RelationKind } from '../relations/Relation'
import { ENTITY_TYPE, PRIMARY_KEY, Entity, ModelDefinition } from '../glossary'
import { isObject } from '../utils/isObject'
import { inheritInternalProperties } from '../utils/inheritInternalProperties'
import { NullableProperty } from '../nullable'
import { NullableObject, NullableProperty } from '../nullable'
import { spread } from '../utils/spread'
import { getDefinition } from './getDefinition'

const log = debug('updateEntity')

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

const propertyDefinition = get(definition, propertyPath)
const propertyDefinition = getDefinition(definition, propertyPath)

log('property definition for "%s":', propertyPath, propertyDefinition)

if (propertyDefinition == null) {
Expand Down Expand Up @@ -183,7 +185,9 @@ export function updateEntity(
}

invariant(
nextValue !== null || propertyDefinition instanceof NullableProperty,
nextValue !== null ||
propertyDefinition instanceof NullableProperty ||
propertyDefinition instanceof NullableObject,
'Failed to update "%s" on "%s": cannot set a non-nullable property to null.',
propertyName,
entity[ENTITY_TYPE],
Expand Down
47 changes: 36 additions & 11 deletions src/nullable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { ModelValueType } from './glossary'
import { ModelValueType, NestedModelDefinition } from './glossary'
import { ManyOf, OneOf, Relation, RelationKind } from './relations/Relation'

export class NullableObject<ValueType extends NestedModelDefinition> {
public objectDefinition: ValueType
public defaultsToNull: boolean

constructor(definition: ValueType, defaultsToNull: boolean) {
this.objectDefinition = definition
this.defaultsToNull = defaultsToNull
}
}

export type NullableGetter<ValueType extends ModelValueType> =
() => ValueType | null

Expand All @@ -12,14 +22,21 @@ export class NullableProperty<ValueType extends ModelValueType> {
}
}

export function nullable<ValueType extends NestedModelDefinition>(
value: ValueType,
options?: { defaultsToNull?: boolean },
): NullableObject<ValueType>

export function nullable<ValueType extends ModelValueType>(
value: NullableGetter<ValueType>,
options?: { defaultsToNull?: boolean },
): NullableProperty<ValueType>

export function nullable<
ValueType extends Relation<any, any, any, { nullable: false }>,
>(
value: ValueType,
options?: { defaultsToNull?: boolean },
): ValueType extends Relation<infer Kind, infer Key, any, { nullable: false }>
? Kind extends RelationKind.ManyOf
? ManyOf<Key, true>
Expand All @@ -29,18 +46,26 @@ export function nullable<
export function nullable(
value:
| NullableGetter<ModelValueType>
| Relation<any, any, any, { nullable: false }>,
| Relation<any, any, any, { nullable: false }>
| NestedModelDefinition,
options?: { defaultsToNull?: boolean },
) {
if (value instanceof Relation) {
return new Relation({
kind: value.kind,
to: value.target.modelName,
attributes: {
...value.attributes,
nullable: true,
},
})
}

if (typeof value === 'object') {
return new NullableObject(value, !!options?.defaultsToNull)
}

if (typeof value === 'function') {
return new NullableProperty(value)
}

return new Relation({
kind: value.kind,
to: value.target.modelName,
attributes: {
...value.attributes,
nullable: true,
},
})
}
Loading

0 comments on commit 1e7b2af

Please sign in to comment.