benhutchins / dyngoose

Elegant DynamoDB object modeling for Typescript.
https://www.npmjs.com/package/dyngoose
ISC License
89 stars 14 forks source link

Multiple attribute on a property #669

Closed jcjp closed 1 year ago

jcjp commented 1 year ago

Multiple attribute type is it possible with DynamoDB or using this library? For example:

@Dyngoose.Attribute.Any()
age: number | string

Attribute Any will convert this to a string, is there a way to add multiple attributes to safely save / persist the correct value and it's type?

@Dyngoose.Attribute.String() | @Dyngoose.Attribute.Number()
age: string | number

OR

@Dyngoose.Attribute.String() | Attribute.Number()
age: string | number

OR

@Dyngoose.Attribute.Mixed()
age: string | number

I have read somewhere that when we don't pass an attribute to a field / property it will be automatically dynamic except the primary and sort keys. Is this do-able with Dyngoose, I tried removing the decorator attribute what happen was the field was skipped and was not saved to the database.

benhutchins commented 1 year ago

This isn't something that Dyngoose has implemented. It seems like a great thing to support, but it's never come up before. I'll look into adding explicit support for mixed attribute types in a future release.

For now, the best option I can recommend is to utilize the manipulateRead utility. It lets you hijack the parsing, primarily designed to help with migrations in case you convert from one attribute type to another (such as, you used to store a value as a number and now it is a string).

@Dyngoose.Attribute.Number({
  manipulateRead: (value, attributeValue, attr) => {
    // attributeValue can be null if the attribute is not set
    return attributeValue?.S != null ? attributeValue.S : value
  },
})
age: string | number

As for your other question, Dyngoose can not infer your attribute type and you must use a decorator or the property will not be considered a database attribute and will remain as a class property only (which will not be saved). Dyngoose can support some dynamically created attributes, but you must use the utility methods for getting and setting attributes rather than a class property in that case.

jcjp commented 1 year ago

This isn't something that Dyngoose has implemented. It seems like a great thing to support, but it's never come up before. I'll look into adding explicit support for mixed attribute types in a future release.

For now, the best option I can recommend is to utilize the manipulateRead utility. It lets you hijack the parsing, primarily designed to help with migrations in case you convert from one attribute type to another (such as, you used to store a value as a number and now it is a string).

@Dyngoose.Attribute.Number({
  manipulateRead: (value, attributeValue, attr) => {
    // attributeValue can be null if the attribute is not set
    return attributeValue?.S != null ? attributeValue.S : value
  },
})
age: string | number

As for your other question, Dyngoose can not infer your attribute type and you must use a decorator or the property will not be considered a database attribute and will remain as a class property only (which will not be saved). Dyngoose can support some dynamically created attributes, but you must use the utility methods for getting and setting attributes rather than a class property in that case.

Thanks this is very helpful! I will use your suggestion for now. I get a type error trying your suggestion:

Type '(value: number | BigInt | null, attributeValue: AttributeValue | null) => string | number | BigInt | null | undefined' is not assignable to type '(value: number | BigInt | null, attributeValue: AttributeValue | null, attribute: Attribute<any>) => number | BigInt | null'.
  Type 'string | number | BigInt | null | undefined' is not assignable to type 'number | BigInt | null'.
    Type 'undefined' is not assignable to type 'number | BigInt | null'.ts(2322)

Your example code is when reading but how about writting on the database, do the same concept apply for manipulateWrite my assumption that since the attribute is Number when a string is passed it will become null in value?

@Dyngoose.Attribute.Number({
  manipulateWrite: (value, attributeValue, attr) => {
    if (!value) return attributeValue.S
    return value;
  },
})
benhutchins commented 1 year ago

Yes, you can also use manipulateWrite to change how an attribute will be written. It is called after the default behavior, allowing you to override and save a value as a different attribute type if you desired.

A complete example may be:

@Dyngoose.Attribute.Number({
  manipulateRead: (value, attributeValue, attr) => {
    // attributeValue can be null if the attribute is not set
    return attributeValue?.S != null ? attributeValue.S : value
  },
  manipulateWrite: (attributeValue, value, attr) => { // yes, apparently the order of arguments is different
   return typeof value === 'string' ? { S: value } : attributeValue;
  },
})

You are correct that the attributeValue would become null when you a pass a string to the number handler. It may attempt to covert it to a number for you, but if that values, it'd become null. You'll need to create the attribute for DynamoDB. So the above should work out for what you are attempting.

benhutchins commented 1 year ago

I've addressed this with the new Dynamic attribute, you can now use:

@Dyngoose.Attribute()
age: string | number

This has a few limitations, see the docs, but this should resolve this request.

jcjp commented 1 year ago

I've addressed this with the new Dynamic attribute, you can now use:

@Dyngoose.Attribute()
age: string | number

This has a few limitations, see the docs, but this should resolve this request.

Thank you! Will check the docs.