8000 Add support for null property values by olivierwilkinson · Pull Request #143 · mswjs/data · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add support for null property values #143

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and co 8000 ntact 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

Merged
merged 6 commits into from
Nov 17, 2021
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
115 changes: 106 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ setupWorker(

- [Model methods](#model-methods)
- **Modeling:**
- [Nullable properties](#nullable-properties)
- [Nested structures](#nested-structures)
- [Model relationships](#model-relationships)
- **Querying:**
Expand Down Expand Up @@ -334,19 +335,73 @@ db.user.toHandlers('rest', 'https://example.com')
db.user.toHandlers('graphql', 'https://example.com/graphql')
```

### Nullable properties

By default, all model properties are non-nullable. You can use the `nullable` function to mark a property as nullable:

```js
import { factory, primaryKey, nullable } from '@mswjs/data'

const db = factory({
user: {
id: primaryKey(String),
firstName: String,
// "user.age" is a nullable property.
age: nullable(Number),
},
})

db.user.create({
id: 'user-1',
firstName: 'John',
// Nullable properties can be explicit null as the initial value.
age: null,
})

db.user.update({
where: {
id: {
equals: 'user-1',
},
},
data: {
// Nullable properties can be updated to null.
age: null,
},
})
```

> You can define [Nullable relationships](#nullable-relationships) in the same manner.

When using Typescript, you can manually set the type of the property when it is
not possible to infer it from the factory function, such as when you want the
property to default to null:

```typescript
import { factory, primaryKey, nullable } from '@mswjs/data'

const db = factory({
user: {
id: primaryKey(String),
age: nullable<number>(() => null),
},
})
```

### Nested structures

You may use nested objects to design a complex structure of your model:

```js
import { factory, primaryKey } from '@mswjs/data'
import { factory, primaryKey, nullable } from '@mswjs/data'

const db = factory({
user: {
id: primaryKey(String),
address: {
billing: {
street: String,
city: nullable(String),
},
},
},
Expand All @@ -360,6 +415,7 @@ db.user.create({
address: {
billing: {
street: 'Baker st.',
city: 'London',
},
},
})
Expand All @@ -374,6 +430,7 @@ db.user.update({
address: {
billing: {
street: 'Sunwell ave.',
city: null,
},
},
},
Expand Down Expand Up @@ -408,6 +465,7 @@ factory({
- [One-to-Many](#one-to-many)
- [Many-to-One](#many-to-one)
- [Unique relationships](#unique-relationships)
- [Nullable relationships](#nullable-relationships)

Relationship is a way for a model to reference another model.

Expand Down Expand Up @@ -523,6 +581,45 @@ const john = db.user.create({ invitation })
const karl = db.user.create({ invitation })
```

#### Nullable relationships

Both `oneOf` and `manyOf` relationships may be passed to `nullable` to allow
instantiating and updating that relation to null.

```js
import { factory, primaryKey, oneOf, nullable } from '@mswjs/data'

const db = factory({
user: {
id: primaryKey(String),
invitation: nullable(oneOf('invitation')),
friends: nullable(manyOf('user')),
},
invitation: {
id: primaryKey(String),
},
})

const invitation = db.invitation.create()

// Nullable relationships are instantiated with null.
const john = db.user.create({ invitation }) // john.friends === null
const kate = db.user.create({ friends: [john] }) // kate.invitation === null

db.user.updateMany({
where: {
id: {
in: [john.id, kate.id],
},
},
data: {
// Nullable relationships can be updated to null.
invitation: null,
friends: null,
},
})
```

### Querying data

This library supports querying of the seeded data similar to how one would query a SQL database. The data is queried based on its properties. A query you construct depends on the value type you are querying.
Expand Down Expand Up @@ -703,27 +800,27 @@ const db = factory({
post: {
id: primaryKey(String),
title: String,
author: oneOf('user')
author: oneOf('user'),
},
user: {
id: primaryKey(String),
firstName: String
}
firstName: String,
},
})

// Return all posts in the "Science" category
// sorted by the post author's first name.
db.post.findMany({
where: {
category: {
equals: 'Science'
}
equals: 'Science',
},
},
orderBy: {
author: {
firstName: 'asc'
}
}
firstName: 'asc',
},
},
})
```

Expand Down
35 changes: 23 additions & 12 deletions src/glossary.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GraphQLSchema } from 'graphql'
import { GraphQLHandler, RestHandler } from 'msw'
import { NullableProperty } from './nullable'
import { PrimaryKey } from './primaryKey'
import {
BulkQueryOptions,
Expand All @@ -18,17 +19,19 @@ export type ModelValueTypeGetter = () => ModelValueType
export type ModelDefinition = Record<string, ModelDefinitionValue>

export type ModelDefinitionValue =
| ModelValueTypeGetter
| PrimaryKey<any>
| OneOf<any>
| ManyOf<any>
| ModelValueTypeGetter
| NullableProperty<any>
| OneOf<any, boolean>
| ManyOf<any, boolean>
| NestedModelDefinition

export type NestedModelDefinition = {
[propertyName: string]:
| ModelValueTypeGetter
| OneOf<any>
| ManyOf<any>
| NullableProperty<any>
| OneOf<any, boolean>
| ManyOf<any, boolean>
| NestedModelDefinition
}

Expand Down Expand Up @@ -168,9 +171,9 @@ export type UpdateManyValue<
| {
[Key in keyof Target]?: Target[Key] extends PrimaryKey
? (
prevValue: ReturnType<Target[Key]['getValue']>,
prevValue: ReturnType<Target[Key]['getPrimaryKeyValue']>,
entity: Value<Target, Dictionary>,
) => ReturnType<Target[Key]['getValue']>
) => ReturnType<Target[Key]['getPrimaryKeyValue']>
: Target[Key] extends ModelValueTypeGetter
? (
prevValue: ReturnType<Target[Key]>,
Expand All @@ -189,12 +192,20 @@ export type Value<
Dictionary extends ModelDictionary,
> = {
[Key in keyof Target]: Target[Key] extends PrimaryKey<any>
? ReturnType<Target[Key]['getPrimaryKeyValue']>
: // Extract underlying value type of nullable properties
Target[Key] extends NullableProperty<any>
? ReturnType<Target[Key]['getValue']>
: // Extract value type from relations.
Target[Key] extends OneOf<infer ModelName>
? PublicEntity<Dictionary, ModelName>
: Target[Key] extends ManyOf<infer ModelName>
? PublicEntity<Dictionary, ModelName>[]
: // Extract value type from OneOf relations.
Target[Key] extends OneOf<infer ModelName, infer Nullable>
? Nullable extends true
? PublicEntity<Dictionary, ModelName> | null
: PublicEntity<Dictionary, ModelName>
: // Extract value type from ManyOf relations.
Target[Key] extends ManyOf<infer ModelName, infer Nullable>
? Nullable extends true
? PublicEntity<Dictionary, ModelName>[] | null
: PublicEntity<Dictionary, ModelName>[]
: // Account for primitive value getters because
// native constructors (i.e. StringConstructor) satisfy
// the "AnyObject" predicate below.
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { factory } from './factory'
export { primaryKey } from './primaryKey'
export { nullable } from './nullable';
export { oneOf } from './relations/oneOf'
export { manyOf } from './relations/manyOf'
export { drop } from './db/drop'
Expand Down
31 changes: 22 additions & 9 deletions src/model/createModel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { debug } from 'debug'
import { invariant } from 'outvariant'
import get from 'lodash/get'
import set from 'lodash/set'
import isFunction from 'lodash/isFunction'
Expand All @@ -16,6 +17,8 @@ import { ParsedModelDefinition } from './parseModelDefinition'
import { defineRelationalProperties } from './defineRelationalProperties'
import { PrimaryKey } from '../primaryKey'
import { Relation } from '../relations/Relation'
import { NullableProperty } from '../nullable'
import { isModelValueType } from '../utils/isModelValueType'

const log = debug('createModel')

Expand Down Expand Up @@ -59,19 +62,29 @@ export function createModel<
set(
properties,
propertyName,
initialValue || propertyDefinition.getValue(),
initialValue || propertyDefinition.getPrimaryKeyValue(),
)
return properties
}

if (
typeof initialValue === 'string' ||
typeof initialValue === 'number' ||
typeof initialValue === 'boolean' ||
// @ts-ignore
initialValue?.constructor.name === 'Date' ||
Array.isArray(initialValue)
) {
if (propertyDefinition instanceof NullableProperty) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we checking for nullable properties first? Would some of them fall under isModelValueType check below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it would be caught by the isModelValueType check, I put it there because it felt happy next to the instanceof PrimaryKey check haha. I can move it below isModelValueType though if you'd prefer?

const value =
initialValue === null || isModelValueType(initialValue)
? initialValue
: propertyDefinition.getValue()

set(properties, propertyName, value)
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.',
modelName,
propertyName.join('.'),
)

if (isModelValueType(initialValue)) {
log(
'"%s" has a plain initial value:',
`${modelName}.${propertyName}`,
Expand Down
16 changes: 14 additions & 2 deletions src/model/defineRelationalProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,23 @@ export function defineRelationalProperties(
relation.target.modelName,
)

const references: Value<any, ModelDictionary> | undefined = get(
const references: Value<any, ModelDictionary> | null | undefined = get(
initialValues,
propertyPath,
)

invariant(
references !== null || relation.attributes.nullable,
'Failed to define a "%s" relational property to "%s" on "%s": a non-nullable relation cannot be instantiated with null. Use the "nullable" function when defining this relation to support nullable value.',
relation.kind,
propertyPath.join('.'),
entity[ENTITY_TYPE],
)

log(
`setting relational property "${entity.__type}.${propertyPath.join('.')}" with references: %j`,
`setting relational property "${entity.__type}.${propertyPath.join(
'.',
)}" with references: %j`,
relation,
references,
)
Expand All @@ -41,6 +51,8 @@ export function defineRelationalProperties(

if (references) {
relation.resolveWith(entity, references)
} else if (relation.attributes.nullable) {
relation.resolveWith(entity, null)
}
}
}
2 changes: 1 addition & 1 deletion src/model/generateRestHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function generateRestHandlers<
const primaryKey = findPrimaryKey(modelDefinition)!
const primaryKeyValue = (
modelDefinition[primaryKey] as PrimaryKey<PrimaryKeyType>
).getValue()
).getPrimaryKeyValue()
const modelPath = pluralize(modelName)
const buildUrl = createUrlBuilder(baseUrl)

Expand Down
7 changes: 7 additions & 0 deletions src/model/parseModelDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { PrimaryKey } from '../primaryKey'
import { isObject } from '../utils/isObject'
import { Relation, RelationsList } from '../relations/Relation'
import { NullableProperty } from '../nullable'

const log = debug('parseModelDefinition')

Expand Down Expand Up @@ -69,6 +70,12 @@ function deepParseModelDefinition<Dictionary extends ModelDictionary>(
continue
}

if (value instanceof NullableProperty) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it have the same effect if we omit this if altogether? It should fall down to the very last result.properties.push(), doing the same thing. Let me know if I'm missing something here.

Copy link
Contributor Author
@olivierwilkinson olivierwilkinson Nov 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NullableProperty class gets caught by the isObject util since typeof value === 'object' where value is an instance of NullableProperty returns true. I could've written it as follows though:

if (isObject<NestedModelDefinition>(value) && !(value instanceof NullableProperty)) {
  ...
}

The above is definitely clearer as to why it needs to be there but I kinda like that NullableProperty is being handled explicitly right now. I'm very happy with either so up to you what you prefer 😄 .

There may be alternatives around changing the isObject util to be more specific but I don't know the implications of that.

// Add nullable properties to the same list as regular properties
result.properties.push(propertyPath)
continue
}

// Relations.
if (value instanceof Relation) {
// Store the relations in a separate object.
Expand Down
Loading
0