[WIP] create v2 of Entity attributes
This is a proposal for a new way to define Entity attributes. Answers partially https://github.com/jeremydaly/dynamodb-toolbox/issues/303
I put the code in src/v2. This PR is not meant to be merged in the main branch (for now).
I propose a new way to define item properties:
import { item, string, boolean, number, map, list, Input, Output } from 'v2/attributes'
const someItem = item({
reqString: string().required(), // or string({ required: true })
hiddenBoolean: boolean().hidden(), // or boolean({ hidden: true })
// default value type is correctly enforced
// you can also provide a function (that takes no arg)
defaultNumber: number().default(42), // or number({ default: 42 })
// you can also set required and hidden props to maps & lists
// ...but no default (type computations explode, should be treated in a separate method of item, like `computeDefaults`)
map: map({
myMapProp: string().required()
}).required(), // or map(..., { required: true })
// In lists, elements MUST be required and MUST NOT be hidden
list: list(string().required()) // or list(..., { required: true })
})
type InputItem = Input<typeof someItem> // <= recursively infers the correct input
type OutputItem = Output<typeof someItem> // <= recursively infers the correct output
I just added a new commit to enable typed defaults computing. The API is something like:
import { item, string, ComputedDefault } from 'v2/attributes'
const myItem = item({
optProp: string(),
optPropWithInitDef: string().default('foo'),
// ComputedDefault is a JS Symbol (outside the scope of any string value)
// It serves as tagging props as needing inputs from other props to be computed
optPropWithCompDef: string().default(ComputedDefault),
reqProp: string().required(),
reqPropWithInitDef: string().required().default('bar'),
reqPropWithCompDef: string().required().default(ComputedDefault)
}).computeDefaults((preCompute) => { // <= the result of initial (non-computed) defaults + user input
... // compute computedDefaults here
return postCompute // <= the final item (user input + all defaults)
})
The magic is:
- I managed to correctly typed
preComputeandpostCompute - Type inference is recursive through lists and maps
In this example, they would be equal to:
type PreComp = {
optProp?: string | undefined
optPropWithInitDef: string // <= at least initial (non-computed) default is provided
optPropWithCompDef?: string | undefined // <= user input or nothing
reqProp: string // <= required from user input
reqPropWithInitDef: string // <= at least initial default is provided
reqPropWithCompDef?: string // <= user input or nothing
}
type PostComp = {
optProps?: string | undefined
optPropWithInitDef: string
optPropWithCompDef: string // <= now, prop is required thanks to the ComputedDefault tag
reqProp: string
reqPropWithInitDef: string
reqPropWithCompDef: string // <= same
}
Note that all the props (required, hidden, computeDefaults etc...) will be available at runtime, and that this is just a proposal for item definition. No usage is yet done of all those exported builders, but implementation in a new version of the Entity class is secondary and will be not be too hard I think:
const MyEntity = new Entity({
...
item: item(...).computedDefaults(...) // <= no need for the `as const` statement anymore !
})
Also, note that the final JS is very minimal, probably less than 100 lines ✨
@jeremydaly Having a lot a fun here :)
Just added a third commit to add a savedAs option on attributes and a SavedAs utility type to infer the type of data in dynamodb:
const testItem = item({
str: string().savedAs('foo'),
map: map({
renamed: string().required().savedAs('bar')
})
.required()
.savedAs('baz')
})
type SavedData = SavedAs<typeof testItem>
// equivalent to
type SavedData = {
baz: {
bar: string;
};
foo?: string | undefined;
}
What do you think ? Should I continue in this direction ?
@ThomasAribart looks awesome 🔥
@jeremydaly I've updated the branch destination, added JS Docs to (most) constants and changed the folder name to v1 :) The tests seem to fail, I have no idea why, but in theory you're good to merge 👍
@jeremydaly I've updated the branch destination, added JS Docs to (most) constants and changed the folder name to v1 :) The tests seem to fail, I have no idea why, but in theory you're good to merge 👍
It seems that you only updated package.json but not package.lock.
In addition, I think that you @jeremydaly and I should sync on this(and v1) together. Wdyt about talking over Zoom in one of the next few days?
@jeremydaly @naorpeled Okay I had to upgrade TS to 4.3.0 for some reason 🤷♂️ (probably a bug in earlier versions), but tests are alright now !
We had a long chat tuesday on zoom with @jeremydaly, but sure I'd be very happy to ! We agreed to have complete type inference for the v1, merge this into a v1 branch for now !
Meanwhile, feel free to try out stuff in the playground folder !