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: created nullable object option without getter #257

Merged
merged 7 commits into from
Oct 8, 2023
Merged
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
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