-
-
Notifications
You must be signed in to change notification settings - Fork 52
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
Add support for null property values #143
Conversation
Thank you for opening this, @olivierwilkinson! We should discuss the initial/nullable values in model definitions in general, there's room for improvement. The main points I'd focus on are:
|
No problem, and thanks for the rapid response! Yeah I agree there are more general purpose ways of defining nullable properties. I was toying with an API similar to const db = factory({
account: {
id: primaryKey(String),
firstName: nullable(String),
lastName: nullable<string>(() => null),
address: {
street: String,
houseNumber: nullable(Number),
livesWith: nullable(manyOf("account")),
},
referrer: nullable(oneOf("account")),
},
}); Where properties that are omitted during creation use the factory function passed to nullable for the initial value. I'm not sure of the case where you want a null and only null value, but it could be achieved with: alwaysNull: nullable(() => null) Is this closer to what you had in mind? I opted for the minimal solution with this PR as I wasn't sure of the corner cases. I may have a go at the above for a bit of fun if there aren't any glaring issues? It might bring up more to talk about, and at the very least would improve my understanding of the library! |
0e419d8
to
73808a9
Compare
@kettanaito I did end up implementing the above API. I think it's a much better proposal than my original attempt so I pushed to this branch rather than opening a new PR. It was a blast getting to grips with everything! I'm looking forwards to hearing your feedback. 😄 |
89c43f9
to
cbf4f7a
Compare
cbf4f7a
to
206d392
Compare
@@ -69,13 +70,25 @@ function deepParseModelDefinition<Dictionary extends ModelDictionary>( | |||
continue | |||
} | |||
|
|||
if (value instanceof NullableProperty) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/utils/isModelValueType.ts
Outdated
@@ -0,0 +1,11 @@ | |||
import { ModelValueType } from '../glossary' | |||
|
|||
export function isModelValueType(value: any): value is ModelValueType { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe value
is not quite any
. It should be value: ModelDefinitionValue
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, when I do that I get the following error message:
A type predicate's type must be assignable to its parameter's type.
I think I can tweak the logic to make it work for any value, then the type is valid as any
. What do you think about this?
function isPrimitiveValueType(value: any): value is PrimitiveValueType {
return (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
value?.constructor?.name === 'Date'
)
}
export function isModelValueType(value: any): value is ModelValueType {
return (
isPrimitiveValueType(value) ||
(Array.isArray(value) && value.every(isPrimitiveValueType))
)
}
I can't think of a case where this would cause a false positive but I could be missing something.
initialValue?.constructor.name === 'Date' || | ||
Array.isArray(initialValue) | ||
) { | ||
if (propertyDefinition instanceof NullableProperty) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
I think there's a TypeScript version mismatch between the local environment and the CI. |
Hey, @olivierwilkinson 👋 I've pushed a couple of changes to your remote:
The build and tests pass on local, so I'll switch to look into this CI issue. |
New `nullable` function provides an API for defining nullable properties as well as nullable relations. The arguments passed to `nullable` are identical to those passed to factory when the property is not nullable. This means `nullable` should feel natural to use for users familiar to the project. For example to define a nullable property the user passes the factory function they would use to normally define the property to `nullable` when defining the db. This matches the `primaryKey` method as a bonus. To define a nullable relation they would pass the `oneOf` or `manyOf` function with the model name directly to `nullable`. The model API methods are all typed to know when it's possible for a property to be null, and an invariant violation is raised for js users who try to update a non-nullable property to null. Queries on a null property value are treated as non-matching, this means entities with a null value for the property are ignored. Update the README to include docs on how to define `nullable` properties and relations.
56c9686
to
e64bdef
Compare
I've updated the feature branch so now CI will report the versions of different tools (we're mainly interested in |
Very nice! Makes more sense for nullable to live in attributes looking at it now. 😄 Thanks for looking into the CI issue 🙌 I wasn't sure how I was going to fix that haha |
I think the CI failure is related to the node: v14.18.1
npm: 6.14.15
typescript: @mswjs/[email protected] /home/runner/work/data/data
+└── [email protected]
-tsc: Version 4.4.4 While locally I have: typescript: @mswjs/[email protected] /mswjs/data
+└── [email protected]
+tsc: Version 4.3.5 Looks like CI is installing the latest |
@olivierwilkinson, perhaps a silly question, but do we have any runtime checks that a non-nullable property can't be set to null when calling |
Not a silly question at all! We have invariant checks that prevent updating a non-nullable property or relation to null, but none for creation. Would you like me to add some for creation, or do you want me to remove the checks during update to avoid a breaking change? |
If you have a chance, could you please add a few test cases to the existing I think we should be explicit about this, so if the user wants to use P.S. Don't forget to pull the changes first ;) |
Haha I won't squash either don't worry 😆 That is all done! 😄 |
When creating models using null as the initial value for non-nullable properties and relations should cause an error to enforce use of the `nullable` function.
3d9972c
to
3f3614d
Compare
Thank you for adding the logic and tests when creating non-nullable properties with null. I've pushed to your branch, adjusting the wording, if you don't mind. It's beneficial to contain the current state in the error message ("a non-nullable property/relation") as well as explain why using "nullable" is necessary (and not necessarily what the user wants). The changes look fantastic. I'll look into the failing CI and try to align the TS versions so the build is green. |
Issue:
|
481226f
to
4a75bb6
Compare
Okay, so it wasn't a TS versions mismatch, in the end, it was a regression in primary key check. Once I pushed the fix, the CI is back to normal now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A hard and definite "approved". You've done an incredible job bringing nullable support to Data! Thank you, @olivierwilkinson, and welcome to contributors! 🎉
We are always looking for collaborators, so if any other issue in Data or MSW ecosystem libraries interests you, do not hesitate to discuss and dive into them. I know we could use your expertise.
Great! No worries at all, I was struggling to figure out a concise way of phrasing it and I like what you changed it to 😄 |
Oh wow it hadn't occured to me that's how they are comparing them, good to keep in mind! It makes sense I suppose since before your change both |
Amazing 🥳 Ah I'm happy to hear that! I'm keenly interested so I would love to continue contributing where I am able to. Thank you for putting so much of your own time and effort into reviewing and contributing to make it so polished. I'm really happy with the end result so I appreciate it a lot. This has been a pleasure! 😁 I'm going to go make some nullable properties now 💥 |
All thanks go to you, really. It's by the power of great contributions that we can deliver quality software, together. Thank you! |
closes #142
New
nullable
function provides an API for defining nullable propertiesas well as nullable relations.
The arguments passed to
nullable
are identical to those passed tofactory when the property is not nullable. This means
nullable
shouldfeel natural to use for users familiar to the project.
For example to define a nullable property the user passes the factory
function they would use to normally define the property to
nullable
when defining the db. This matches the
primaryKey
method as a bonus.To define a nullable relation they would pass the
oneOf
ormanyOf
function with the model name directly to
nullable
.The model API methods are all typed to know when it's possible for a
property to be null, and an invariant violation is raised for js users
who try to update a non-nullable property to null.