dynamodb-toolbox icon indicating copy to clipboard operation
dynamodb-toolbox copied to clipboard

[WIP] create v2 of Entity attributes

Open ThomasAribart opened this issue 3 years ago • 2 comments

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

ThomasAribart avatar Aug 04 '22 08:08 ThomasAribart

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 preCompute and postCompute
  • 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 ✨

ThomasAribart avatar Aug 04 '22 15:08 ThomasAribart

@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 avatar Aug 06 '22 13:08 ThomasAribart

@ThomasAribart looks awesome 🔥

naorpeled avatar Aug 16 '22 07:08 naorpeled

@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 👍

ThomasAribart avatar Aug 17 '22 13:08 ThomasAribart

@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?

naorpeled avatar Aug 17 '22 14:08 naorpeled

@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 !

ThomasAribart avatar Aug 18 '22 16:08 ThomasAribart