-
-
Notifications
You must be signed in to change notification settings - Fork 361
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: strongly typed item methods #1659
base: main
Are you sure you want to change the base?
feat: strongly typed item methods #1659
Conversation
CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅ |
1446a81
to
107cd29
Compare
@fishcharlie Hoping to get your input on my design/implementation of strongly typed item methods. I've created a draft PR for now just in case the design needs rework. Once you're happy with the PR I'll go ahead and update documentation & mark it as ready. I've done the best I can to make sure that anywhere an |
I dedicate any and all copyright interest in this software to the public domain. I make this dedication for the benefit of the public at large and to the detriment of my heirs and successors. I intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. |
@@ -439,21 +439,21 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl | |||
// latestTableDetails: DynamoDB.DescribeTableOutput; | |||
// pendingTaskPromise: () => Promise<void>; | |||
Item: typeof ItemCarrier; |
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.
@fishcharlie Is being able to access the underlying Item
class from a model part of public interface? If it is not, then there aren't any issues. However if it is, then we'll have to figure out a way to better type this property.
For example, this is possible:
const UserModel = dynamoose.model('User', userSchema)
const user = new UserModel.Item({ id: 'foobar' })
This works fine in JS, however typing this correctly in TS has proven challenging. I made a naive attempt at fixing the types:
Item: typeof ItemCarrier; | |
Item: T & typeof ItemCarrier; |
However, TypeScript understandably isn't happy with this, as the internal Item
class created within Model
is not assignable to the generic we pass in:
Type 'typeof Item' is not assignable to type 'T & typeof Item'.
Type 'typeof Item' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'typeof Item'.
Haven't been able to find a good solution to this yet.
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.
Is being able to access the underlying Item class from a model part of public interface?
It might work. But it's not supported. If someone tries to do that in their code I would point them to this: https://dynamoosejs.com/other/FAQ#can-i-use-an-undocumented-property-class-method-or-function-in-dynamoose.
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.
Okay perfect 👍 .
It's also mentioned in the TS docs that documentation should be treated as the source of truth, so we can set this aside until Model.Item
becomes public interface.
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.
Overall this looks really good.
Beyond the code level comments I made the only other high level suggestion is that we need documentation for this. All the documentation either exists in the code itself using JSDoc or in docs/docs_src
.
packages/dynamoose/lib/General.ts
Outdated
@@ -14,7 +14,9 @@ export type KeyObject = {[attribute: string]: string | number}; | |||
// An item representing a DynamoDB key | |||
export type InputKey = string | number | KeyObject; | |||
|
|||
interface ModelItemConstructor<T extends Item> { | |||
type Expand<T> = {[KeyType in keyof T]: T[KeyType]} & {}; |
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 the & {}
?
Also, can you give an example of what this would look like? For some reason this type seems very complex at first glance. It's not obviously clear what it's doing and what it's purpose is. Maybe add a comment above with an example or something? Or a better description?
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.
Also should this be in a separate file?
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.
Good points! This type definitely could use a JSDoc explaining its function & intended use. Also agree that it should be moved to a separate file. Best location for it from what I can see would be lib/Types.ts
. How does that sound?
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.
lib/Types.ts
works for me 🎉
packages/dynamoose/lib/Item.ts
Outdated
export interface AnyItemMethods { | ||
[key: string]: any; | ||
} |
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.
It's not immediately clear what this is doing. Seems more general than just AnyItemMethods
. Why do we have to use any
vs Function
? If it's item methods, shouldn't that be a function?
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.
Fair point. I encountered an issue with TS early on omitting the ItemMethods
generic when calling dynamoose.model
, however I believe that requiring custom methods to extend ItemMethods
addresses this. Let me revisit this and see if it's possible to completely remove it.
@@ -439,21 +439,21 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl | |||
// latestTableDetails: DynamoDB.DescribeTableOutput; | |||
// pendingTaskPromise: () => Promise<void>; | |||
Item: typeof ItemCarrier; |
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.
Is being able to access the underlying Item class from a model part of public interface?
It might work. But it's not supported. If someone tries to do that in their code I would point them to this: https://dynamoosejs.com/other/FAQ#can-i-use-an-undocumented-property-class-method-or-function-in-dynamoose.
@@ -11,3 +11,5 @@ const shouldPassSaveWithReturnItemCallback = user.save({"return": "item"}, () => | |||
|
|||
// @ts-expect-error | |||
const shouldFailWithInvalidReturnType = user.save({"return": "invalid-return-type"}); | |||
|
|||
const shouldPassCustomMethodAccess = user.resetPassword(); |
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.
Maybe we should also have a test with a method that doesn't exist? We can use // @ts-expect-error
for that one.
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.
Definitely. Test coverage across edge cases was minimal as I was unsure if you'd be happy with the approach of the PR. Will update/refactor tests & documentation to be more comprehensive.
@@ -89,7 +95,7 @@ const userSchema = new dynamoose.Schema({ | |||
"age": Number | |||
}); | |||
|
|||
export const UserTypedModel = dynamoose.model<User>( | |||
export const UserTypedModel = dynamoose.model<User, UserMethods>( |
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.
What happens here if you don't pass in UserMethods
? Does it still work? I kinda think we should be breaking this out into two different tests. The existing tests are just fine. We should have new tests for these UserMethods that don't modify the old tests.
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.
As part of the interface, it should work. As per my previous comment, I will make sure this is covered in tests.
@@ -110,3 +116,6 @@ UserTypedModel.update({"id": "foo"}, { | |||
UserTypedModel.update({"id": "foo"}, { | |||
"$REMOVE":{"age":null} | |||
}); | |||
|
|||
// @ts-expect-error | |||
const shouldFailItemCustomMethodAccess = UserTypedModel.resetPassword(); |
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.
So are model methods outside the scope of this PR? This PR only deals with item methods it looks like?
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.
That's correct, I've decided to address them separately to keep scope focused. Happy to include model methods as well if that suits you?
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.
@tranhl Ideally I'd like to review both PRs before merging either of them. I don't want to be left in a state where one piece is done and people ask why the other piece wasn't implemented.
You can decide how to best structure it. I'd personally prefer 2 separate PRs so that it's easier to review and the context is easier to understand. Sadly I don't think GitHub has the concept of having one PR depend on another.
This pull request is stale because it has been open 7 days with no activity. Remove stale label or comment or this will be closed in 3 days. |
It would be great to have this feature live! |
0d570ee
to
f13f9d5
Compare
Summary:
This PR adds support for strongly-typed custom item methods via a second generic type parameter to
dynamoose.model
.Code sample:
GitHub linked issue:
Implements part of #1647
Type (select 1):
Is this a breaking change? (select 1):
Is this ready to be merged into Dynamoose? (select 1):
Are all the tests currently passing on this PR? (select 1):
Other: