From 14fba22a9e4c518515b3e047df951bc29ed8c561 Mon Sep 17 00:00:00 2001 From: Chris <37954566+codingnuclei@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:03:23 +0000 Subject: [PATCH 01/10] feat: enable entities to define derived state --- src/classes/Entity/Entity.ts | 386 ++++++++++++++++++----------------- src/lib/parseEntity.ts | 5 +- src/lib/parseMapping.ts | 4 + 3 files changed, 209 insertions(+), 186 deletions(-) diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts index ef7c7d002..4a0d768d8 100644 --- a/src/classes/Entity/Entity.ts +++ b/src/classes/Entity/Entity.ts @@ -81,29 +81,29 @@ class Entity = Readonly, WritableAttributeDefinitions extends AttributeDefinitions = Writable, Attributes extends ParsedAttributes = string extends Name - ? ParsedAttributes - : If, - // 🔨 TOIMPROVE: Use EntityTable in attributes parsing - ParseAttributes, - ParsedAttributes>, + ? ParsedAttributes + : If, + // 🔨 TOIMPROVE: Use EntityTable in attributes parsing + ParseAttributes, + ParsedAttributes>, $Item = string extends Name - ? any - : If, - // 🔨 TOIMPROVE: Use EntityTable in item infering - InferItem, - EntityItemOverlay>, + ? any + : If, + // 🔨 TOIMPROVE: Use EntityTable in item infering + InferItem, + EntityItemOverlay>, // Necessary to cast in a second step to prevent infinite loop during type check Item extends O.Object = string extends Name ? O.Object : A.Cast<$Item, O.Object>, CompositePrimaryKey extends O.Object = string extends Name - ? O.Object - : If, - InferCompositePrimaryKey, - O.Object>> { + ? O.Object + : If, + InferCompositePrimaryKey, + O.Object>> { // @ts-ignore public _typesOnly: { _entityItemOverlay: EntityItemOverlay @@ -119,6 +119,7 @@ class Entity - formatItem()(schema.attributes, linked, item, include), + return data.map(item => { + const formattedItem = formatItem()(schema.attributes, linked, item, include) + const derivedAttributes = this.derived + .map(derivedAtrribute => schema.attributes[derivedAtrribute].derive(formattedItem)) + return { + ...formattedItem, + ...derivedAttributes + } + } ) as any } else { - return formatItem()(schema.attributes, linked, data, include) as any + const formattedItem = formatItem()(schema.attributes, linked, data, include) as any + const derivedAttributes = this.derived + .map(derivedAtrribute => schema.attributes[derivedAtrribute].derive(formattedItem)) + return { + ...formattedItem, + ...derivedAttributes + } } } @@ -283,16 +299,16 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: $GetOptions = {}, - params: Partial = {}, - ): Promise>, - GetCommandInput, - If>, - GetCommandOutput, - Compute>]>>>>>> { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: $GetOptions = {}, + params: Partial = {}, + ): Promise>, + GetCommandInput, + If>, + GetCommandOutput, + Compute>]>>>>>> { const getParams = this.getParams, ResponseAttributes extends ShownItemAttributes = ShownItemAttributes>( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: { attributes?: ResponseAttributes[] } = {}, - ): { - Entity: Entity - } & TransactGetItem { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: { attributes?: ResponseAttributes[] } = {}, + ): { + Entity: Entity + } & TransactGetItem { // Destructure options to check for extraneous arguments const { attributes, // ProjectionExpression @@ -404,10 +420,10 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: $GetOptions = {}, - params: Partial = {}, - ): GetCommandInput { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: $GetOptions = {}, + params: Partial = {}, + ): GetCommandInput { // Extract schema and merge defaults const { schema, defaults, linked, _table } = this const data = normalizeData()( @@ -494,19 +510,19 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: RawDeleteOptions = {}, - params: Partial = {}, - ): Promise>, - DeleteCommandInput, - If>, - DeleteCommandOutput, - If, A.Equals>, - O.Omit, - O.Update>]>>>>>> { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: RawDeleteOptions = {}, + params: Partial = {}, + ): Promise>, + DeleteCommandInput, + If>, + DeleteCommandOutput, + If, A.Equals>, + O.Omit, + O.Update>]>>>>>> { const deleteParams = this.deleteParams, ResponseAttributes extends ItemAttributes = ItemAttributes>( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: TransactionOptions = {}, - params?: Partial, - ): { Delete: Delete } { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: TransactionOptions = {}, + params?: Partial, + ): { Delete: Delete } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -612,10 +628,10 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: RawDeleteOptions = {}, - params: Partial = {}, - ): DeleteCommandInput { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: RawDeleteOptions = {}, + params: Partial = {}, + ): DeleteCommandInput { // Extract schema and merge defaults const { schema, defaults, linked, _table } = this const data = normalizeData()( @@ -721,31 +737,31 @@ class Entity( - item: UpdateItem, - options: $UpdateOptions = {}, - params: UpdateCustomParams = {}, - ): Promise>, - UpdateCommandInput, - If>, - UpdateCommandOutput, - If, - Omit, - O.Update, A.Equals>, - FirstDefined<[O.Pick, EntityItemOverlay, MethodItemOverlay]>, - If, - A.Equals>, - FirstDefined<[MethodItemOverlay, O.Pick, EntityItemOverlay]>>>>>>>> { + item: UpdateItem, + options: $UpdateOptions = {}, + params: UpdateCustomParams = {}, + ): Promise>, + UpdateCommandInput, + If>, + UpdateCommandOutput, + If, + Omit, + O.Update, A.Equals>, + FirstDefined<[O.Pick, EntityItemOverlay, MethodItemOverlay]>, + If, + A.Equals>, + FirstDefined<[MethodItemOverlay, O.Pick, EntityItemOverlay]>>>>>>>> { // Generate the payload const updateParams = this.updateParams, ResponseAttributes extends ItemAttributes = ItemAttributes, StrictSchemaCheck extends boolean | undefined = true>( - item: UpdateItem, - options: TransactionOptions = {}, - params?: UpdateCustomParams, - ): { Update: Update } { + item: UpdateItem, + options: TransactionOptions = {}, + params?: UpdateCustomParams, + ): { Update: Update } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -841,27 +857,27 @@ class Entity( - item: UpdateItem, - options: $UpdateOptions = {}, - { - SET = [], - REMOVE = [], - ADD = [], - DELETE = [], - ExpressionAttributeNames = {}, - ExpressionAttributeValues = {}, - ...params - }: UpdateCustomParams = {}, - ): UpdateCommandInput { + item: UpdateItem, + options: $UpdateOptions = {}, + { + SET = [], + REMOVE = [], + ADD = [], + DELETE = [], + ExpressionAttributeNames = {}, + ExpressionAttributeValues = {}, + ...params + }: UpdateCustomParams = {}, + ): UpdateCommandInput { // Validate operation types if (!Array.isArray(SET)) error('SET must be an array') if (!Array.isArray(REMOVE)) error('REMOVE must be an array') @@ -1233,24 +1249,24 @@ class Entity( - item: PutItem, - options: $PutOptions = {}, - params: Partial = {}, - ): Promise>, - PutCommandInput, - If>, - PutCommandOutput, - // If MethodItemOverlay is defined, ReturnValues is not inferred from args anymore - If, A.Equals>, - O.Omit, - O.Update>]>>>>>> { + item: PutItem, + options: $PutOptions = {}, + params: Partial = {}, + ): Promise>, + PutCommandInput, + If>, + PutCommandOutput, + // If MethodItemOverlay is defined, ReturnValues is not inferred from args anymore + If, A.Equals>, + O.Omit, + O.Update>]>>>>>> { const putParams = this.putParams( - item: PutItem, - options: $PutBatchOptions = {}, - ): { [key: string]: WriteRequest } { + item: PutItem, + options: $PutBatchOptions = {}, + ): { [key: string]: WriteRequest } { const payload = this.putParams, ResponseAttributes extends ItemAttributes = ItemAttributes, StrictSchemaCheck extends boolean | undefined = true>( - item: PutItem, - options: TransactionOptions = {}, - params?: Partial, - ): { Put: Put } { + item: PutItem, + options: TransactionOptions = {}, + params?: Partial, + ): { Put: Put } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -1379,15 +1395,15 @@ class Entity( - item: PutItem, - options: $PutOptions = {}, - params: Partial = {}, - ): PutCommandInput { + item: PutItem, + options: $PutOptions = {}, + params: Partial = {}, + ): PutCommandInput { // Extract schema and defaults const { schema, defaults, required, linked, _table } = this @@ -1537,9 +1553,9 @@ class Entity, ResponseAttributes extends ItemAttributes = ItemAttributes>( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: TransactionOptions = {}, - ): { ConditionCheck: ConditionCheck } { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: TransactionOptions = {}, + ): { ConditionCheck: ConditionCheck } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -1581,10 +1597,10 @@ class Entity( - pk: any, - options: EntityQueryOptions = {}, - params: Partial = {}, - ) { + pk: any, + options: EntityQueryOptions = {}, + params: Partial = {}, + ) { if (!this.table) { throw new Error('Entity table is not defined') } @@ -1599,7 +1615,7 @@ class Entity( - options: ScanOptions = {}, params: Partial = {}) { + options: ScanOptions = {}, params: Partial = {}) { if (!this.table) { throw new Error('Entity table is not defined') } diff --git a/src/lib/parseEntity.ts b/src/lib/parseEntity.ts index 72fc050cf..bede501c6 100644 --- a/src/lib/parseEntity.ts +++ b/src/lib/parseEntity.ts @@ -14,6 +14,7 @@ export interface TrackingInfo { required: any linked: Linked keys: any + derived: string[] } export interface Linked { @@ -164,7 +165,8 @@ export function parseEntity< defaults: {}, // tracks default attributes required: {}, linked: {}, - keys: {} // tracks partition/sort/index keys + keys: {}, // tracks partition/sort/index keys + derived: [], } const schema = parseEntityAttributes(attributes, track) // removed nested attribute? @@ -188,6 +190,7 @@ export function parseEntity< defaults: track.defaults, required: track.required, linked: track.linked, + derived: track.derived, autoExecute, autoParse, typeHidden, diff --git a/src/lib/parseMapping.ts b/src/lib/parseMapping.ts index 441f76c32..dcaa1a7da 100644 --- a/src/lib/parseMapping.ts +++ b/src/lib/parseMapping.ts @@ -132,6 +132,10 @@ export default ( error(`'${prop}' must be a boolean, string, or array`) } break + case 'derive': + if (typeof config[prop] !== 'function') error(`'${prop}' must be a function`) + track + break default: error(`'${prop}' is not a valid property type`) } From 8ff3bea9abf0df6565d95d7829c115ebcb740e3a Mon Sep 17 00:00:00 2001 From: Chris <37954566+codingnuclei@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:21:22 +0000 Subject: [PATCH 02/10] style: revert affected formatting --- src/classes/Entity/Entity.ts | 370 +++++++++++++++++------------------ 1 file changed, 185 insertions(+), 185 deletions(-) diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts index 4a0d768d8..690e5c890 100644 --- a/src/classes/Entity/Entity.ts +++ b/src/classes/Entity/Entity.ts @@ -81,29 +81,29 @@ class Entity = Readonly, WritableAttributeDefinitions extends AttributeDefinitions = Writable, Attributes extends ParsedAttributes = string extends Name - ? ParsedAttributes - : If, - // 🔨 TOIMPROVE: Use EntityTable in attributes parsing - ParseAttributes, - ParsedAttributes>, + ? ParsedAttributes + : If, + // 🔨 TOIMPROVE: Use EntityTable in attributes parsing + ParseAttributes, + ParsedAttributes>, $Item = string extends Name - ? any - : If, - // 🔨 TOIMPROVE: Use EntityTable in item infering - InferItem, - EntityItemOverlay>, + ? any + : If, + // 🔨 TOIMPROVE: Use EntityTable in item infering + InferItem, + EntityItemOverlay>, // Necessary to cast in a second step to prevent infinite loop during type check Item extends O.Object = string extends Name ? O.Object : A.Cast<$Item, O.Object>, CompositePrimaryKey extends O.Object = string extends Name - ? O.Object - : If, - InferCompositePrimaryKey, - O.Object>> { + ? O.Object + : If, + InferCompositePrimaryKey, + O.Object>> { // @ts-ignore public _typesOnly: { _entityItemOverlay: EntityItemOverlay @@ -262,7 +262,7 @@ class Entity { const formattedItem = formatItem()(schema.attributes, linked, item, include) @@ -277,7 +277,7 @@ class Entity schema.attributes[derivedAtrribute].derive(formattedItem)) + .map(derivedAtrribute => schema.attributes[derivedAtrribute].derive(formattedItem)) return { ...formattedItem, ...derivedAttributes @@ -299,16 +299,16 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: $GetOptions = {}, - params: Partial = {}, - ): Promise>, - GetCommandInput, - If>, - GetCommandOutput, - Compute>]>>>>>> { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: $GetOptions = {}, + params: Partial = {}, + ): Promise>, + GetCommandInput, + If>, + GetCommandOutput, + Compute>]>>>>>> { const getParams = this.getParams, ResponseAttributes extends ShownItemAttributes = ShownItemAttributes>( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: { attributes?: ResponseAttributes[] } = {}, - ): { - Entity: Entity - } & TransactGetItem { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: { attributes?: ResponseAttributes[] } = {}, + ): { + Entity: Entity + } & TransactGetItem { // Destructure options to check for extraneous arguments const { attributes, // ProjectionExpression @@ -420,10 +420,10 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: $GetOptions = {}, - params: Partial = {}, - ): GetCommandInput { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: $GetOptions = {}, + params: Partial = {}, + ): GetCommandInput { // Extract schema and merge defaults const { schema, defaults, linked, _table } = this const data = normalizeData()( @@ -510,19 +510,19 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: RawDeleteOptions = {}, - params: Partial = {}, - ): Promise>, - DeleteCommandInput, - If>, - DeleteCommandOutput, - If, A.Equals>, - O.Omit, - O.Update>]>>>>>> { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: RawDeleteOptions = {}, + params: Partial = {}, + ): Promise>, + DeleteCommandInput, + If>, + DeleteCommandOutput, + If, A.Equals>, + O.Omit, + O.Update>]>>>>>> { const deleteParams = this.deleteParams, ResponseAttributes extends ItemAttributes = ItemAttributes>( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: TransactionOptions = {}, - params?: Partial, - ): { Delete: Delete } { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: TransactionOptions = {}, + params?: Partial, + ): { Delete: Delete } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -628,10 +628,10 @@ class Entity( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: RawDeleteOptions = {}, - params: Partial = {}, - ): DeleteCommandInput { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: RawDeleteOptions = {}, + params: Partial = {}, + ): DeleteCommandInput { // Extract schema and merge defaults const { schema, defaults, linked, _table } = this const data = normalizeData()( @@ -737,31 +737,31 @@ class Entity( - item: UpdateItem, - options: $UpdateOptions = {}, - params: UpdateCustomParams = {}, - ): Promise>, - UpdateCommandInput, - If>, - UpdateCommandOutput, - If, - Omit, - O.Update, A.Equals>, - FirstDefined<[O.Pick, EntityItemOverlay, MethodItemOverlay]>, - If, - A.Equals>, - FirstDefined<[MethodItemOverlay, O.Pick, EntityItemOverlay]>>>>>>>> { + item: UpdateItem, + options: $UpdateOptions = {}, + params: UpdateCustomParams = {}, + ): Promise>, + UpdateCommandInput, + If>, + UpdateCommandOutput, + If, + Omit, + O.Update, A.Equals>, + FirstDefined<[O.Pick, EntityItemOverlay, MethodItemOverlay]>, + If, + A.Equals>, + FirstDefined<[MethodItemOverlay, O.Pick, EntityItemOverlay]>>>>>>>> { // Generate the payload const updateParams = this.updateParams, ResponseAttributes extends ItemAttributes = ItemAttributes, StrictSchemaCheck extends boolean | undefined = true>( - item: UpdateItem, - options: TransactionOptions = {}, - params?: UpdateCustomParams, - ): { Update: Update } { + item: UpdateItem, + options: TransactionOptions = {}, + params?: UpdateCustomParams, + ): { Update: Update } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -857,27 +857,27 @@ class Entity( - item: UpdateItem, - options: $UpdateOptions = {}, - { - SET = [], - REMOVE = [], - ADD = [], - DELETE = [], - ExpressionAttributeNames = {}, - ExpressionAttributeValues = {}, - ...params - }: UpdateCustomParams = {}, - ): UpdateCommandInput { + item: UpdateItem, + options: $UpdateOptions = {}, + { + SET = [], + REMOVE = [], + ADD = [], + DELETE = [], + ExpressionAttributeNames = {}, + ExpressionAttributeValues = {}, + ...params + }: UpdateCustomParams = {}, + ): UpdateCommandInput { // Validate operation types if (!Array.isArray(SET)) error('SET must be an array') if (!Array.isArray(REMOVE)) error('REMOVE must be an array') @@ -1249,24 +1249,24 @@ class Entity( - item: PutItem, - options: $PutOptions = {}, - params: Partial = {}, - ): Promise>, - PutCommandInput, - If>, - PutCommandOutput, - // If MethodItemOverlay is defined, ReturnValues is not inferred from args anymore - If, A.Equals>, - O.Omit, - O.Update>]>>>>>> { + item: PutItem, + options: $PutOptions = {}, + params: Partial = {}, + ): Promise>, + PutCommandInput, + If>, + PutCommandOutput, + // If MethodItemOverlay is defined, ReturnValues is not inferred from args anymore + If, A.Equals>, + O.Omit, + O.Update>]>>>>>> { const putParams = this.putParams( - item: PutItem, - options: $PutBatchOptions = {}, - ): { [key: string]: WriteRequest } { + item: PutItem, + options: $PutBatchOptions = {}, + ): { [key: string]: WriteRequest } { const payload = this.putParams, ResponseAttributes extends ItemAttributes = ItemAttributes, StrictSchemaCheck extends boolean | undefined = true>( - item: PutItem, - options: TransactionOptions = {}, - params?: Partial, - ): { Put: Put } { + item: PutItem, + options: TransactionOptions = {}, + params?: Partial, + ): { Put: Put } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -1395,15 +1395,15 @@ class Entity( - item: PutItem, - options: $PutOptions = {}, - params: Partial = {}, - ): PutCommandInput { + item: PutItem, + options: $PutOptions = {}, + params: Partial = {}, + ): PutCommandInput { // Extract schema and defaults const { schema, defaults, required, linked, _table } = this @@ -1553,9 +1553,9 @@ class Entity, ResponseAttributes extends ItemAttributes = ItemAttributes>( - item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, - options: TransactionOptions = {}, - ): { ConditionCheck: ConditionCheck } { + item: FirstDefined<[MethodCompositeKeyOverlay, EntityCompositeKeyOverlay, CompositePrimaryKey]>, + options: TransactionOptions = {}, + ): { ConditionCheck: ConditionCheck } { // Destructure options to check for extraneous arguments const { conditions, // ConditionExpression @@ -1597,10 +1597,10 @@ class Entity( - pk: any, - options: EntityQueryOptions = {}, - params: Partial = {}, - ) { + pk: any, + options: EntityQueryOptions = {}, + params: Partial = {}, + ) { if (!this.table) { throw new Error('Entity table is not defined') } @@ -1615,7 +1615,7 @@ class Entity( - options: ScanOptions = {}, params: Partial = {}) { + options: ScanOptions = {}, params: Partial = {}) { if (!this.table) { throw new Error('Entity table is not defined') } @@ -1692,4 +1692,4 @@ export const shouldExecute = (execute: boolean | undefined, autoExecute: boolean execute === true || (execute === undefined && autoExecute) export const shouldParse = (parse: boolean | undefined, autoParse: boolean): boolean => - parse === true || (parse === undefined && autoParse) + parse === true || (parse === undefined && autoParse) \ No newline at end of file From cc54c88706e2e3d007ef7bc4d7d237653777a02c Mon Sep 17 00:00:00 2001 From: Chris <37954566+codingnuclei@users.noreply.github.com> Date: Sat, 2 Mar 2024 19:27:28 +0000 Subject: [PATCH 03/10] fix: add missing track push for derived state --- src/lib/parseMapping.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/parseMapping.ts b/src/lib/parseMapping.ts index dcaa1a7da..83a755a1e 100644 --- a/src/lib/parseMapping.ts +++ b/src/lib/parseMapping.ts @@ -134,7 +134,7 @@ export default ( break case 'derive': if (typeof config[prop] !== 'function') error(`'${prop}' must be a function`) - track + track.derived.push(field) break default: error(`'${prop}' is not a valid property type`) From ad5d32ab97a29fa956347348acd26dcd2ec69814 Mon Sep 17 00:00:00 2001 From: Chris <37954566+codingnuclei@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:53:07 +0000 Subject: [PATCH 04/10] refactor: add basic types and simplify --- src/classes/Entity/Entity.ts | 25 ++++++------------------- src/classes/Entity/types.ts | 4 +++- src/lib/formatItem.ts | 11 ++++++++++- src/lib/parseEntity.ts | 1 + 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts index 690e5c890..d15b00608 100644 --- a/src/classes/Entity/Entity.ts +++ b/src/classes/Entity/Entity.ts @@ -119,7 +119,7 @@ class Entity { - const formattedItem = formatItem()(schema.attributes, linked, item, include) - const derivedAttributes = this.derived - .map(derivedAtrribute => schema.attributes[derivedAtrribute].derive(formattedItem)) - return { - ...formattedItem, - ...derivedAttributes - } - } - ) as any + return formatItem()(schema.attributes, linked, item, include, derived) + }) as any } else { - const formattedItem = formatItem()(schema.attributes, linked, data, include) as any - const derivedAttributes = this.derived - .map(derivedAtrribute => schema.attributes[derivedAtrribute].derive(formattedItem)) - return { - ...formattedItem, - ...derivedAttributes - } + return formatItem()(schema.attributes, linked, data, include, derived) as any } } diff --git a/src/classes/Entity/types.ts b/src/classes/Entity/types.ts index feffcdba1..a8882a24a 100644 --- a/src/classes/Entity/types.ts +++ b/src/classes/Entity/types.ts @@ -52,6 +52,7 @@ export type KeyAttributeDefinition = { alias: never map: never setType: never + derive: never } export type PartitionKeyDefinition = Partial & { @@ -94,7 +95,8 @@ export type PureAttributeDefinition = Partial<{ setType: DynamoDBKeyTypes delimiter: string prefix: string - suffix: string + suffix: string, + derive: (data: {}) => any }> export type CompositeAttributeDefinition = diff --git a/src/lib/formatItem.ts b/src/lib/formatItem.ts index ae77678e0..24c8845dd 100644 --- a/src/lib/formatItem.ts +++ b/src/lib/formatItem.ts @@ -52,6 +52,7 @@ export default () => ( linked: Linked, item: any, include: string[] = [], + derived: string[] ) => { // TODO: Support nested maps? // TODO: include alias support? @@ -60,7 +61,7 @@ export default () => ( // Intialize validate type const validateType = validateTypes() - return Object.keys(item).reduce((acc, field) => { + const formattedItem = Object.keys(item).reduce((acc, field) => { const link = linked[field] || (attributes[field] && attributes[field].alias && linked[attributes[field].alias!]) @@ -114,6 +115,14 @@ export default () => ( [(attributes[field] && attributes[field].alias) || field]: transformedValue, }) }, {}) + + const derivedAttribute = derived + .map(derivedAtrribute => attributes[derivedAtrribute].derive?.(formattedItem)) + + return { + ...formattedItem, + ...derivedAttribute + } } function escapeRegExp(text: string) { diff --git a/src/lib/parseEntity.ts b/src/lib/parseEntity.ts index bede501c6..46330aeac 100644 --- a/src/lib/parseEntity.ts +++ b/src/lib/parseEntity.ts @@ -45,6 +45,7 @@ export type ParsedEntity< autoExecute: AutoExecute | undefined linked: Linked defaults: any + derived: string[] required: any table?: EntityTable | undefined, setTable?: (table: NextTable) => ParsedEntity From c658b7a40c56c21ec89078484663a2766da3914a Mon Sep 17 00:00:00 2001 From: Chris <37954566+codingnuclei@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:56:18 +0000 Subject: [PATCH 05/10] style: revert style changes --- src/classes/Entity/Entity.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts index d15b00608..8f4955eb4 100644 --- a/src/classes/Entity/Entity.ts +++ b/src/classes/Entity/Entity.ts @@ -264,9 +264,9 @@ class Entity { - return formatItem()(schema.attributes, linked, item, include, derived) - }) as any + return data.map(item => + formatItem()(schema.attributes, linked, item, include, derived) + ) as any } else { return formatItem()(schema.attributes, linked, data, include, derived) as any } From 43fe005a28ae2d06a532b4b8ad2788e812983410 Mon Sep 17 00:00:00 2001 From: Chris <37954566+codingnuclei@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:58:28 +0000 Subject: [PATCH 06/10] style: revert white space --- src/classes/Entity/Entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts index 8f4955eb4..1c44ccb45 100644 --- a/src/classes/Entity/Entity.ts +++ b/src/classes/Entity/Entity.ts @@ -264,7 +264,7 @@ class Entity + return data.map(item => formatItem()(schema.attributes, linked, item, include, derived) ) as any } else { From 47441c0bb34b53481f43b0dc536530d21c2aa6be Mon Sep 17 00:00:00 2001 From: codingnuclei Date: Thu, 7 Mar 2024 22:20:33 +0000 Subject: [PATCH 07/10] feat: ignore derived state fields in put/update request --- src/__tests__/parseCompositeKey.unit.test.ts | 3 ++- src/__tests__/parseMapping.unit.test.ts | 6 ++++-- src/lib/formatItem.ts | 8 ++++---- src/lib/parseMapping.ts | 7 ++++++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/__tests__/parseCompositeKey.unit.test.ts b/src/__tests__/parseCompositeKey.unit.test.ts index d1adcb9a7..0f9b3a8f4 100644 --- a/src/__tests__/parseCompositeKey.unit.test.ts +++ b/src/__tests__/parseCompositeKey.unit.test.ts @@ -11,7 +11,8 @@ const track: TrackingInfo = { defaults: [], required: [], linked: {}, - keys: [] + keys: [], + derived: [], } describe('parseCompositeKey', () => { diff --git a/src/__tests__/parseMapping.unit.test.ts b/src/__tests__/parseMapping.unit.test.ts index 890c3a663..fa61fc719 100644 --- a/src/__tests__/parseMapping.unit.test.ts +++ b/src/__tests__/parseMapping.unit.test.ts @@ -6,7 +6,8 @@ let track: TrackingInfo = { defaults: [], required: [], linked: {}, - keys: [] + keys: [], + derived: [], } beforeEach(() => { @@ -15,7 +16,8 @@ beforeEach(() => { defaults: [], required: [], linked: {}, - keys: [] + keys: [], + derived: [], } }) diff --git a/src/lib/formatItem.ts b/src/lib/formatItem.ts index 24c8845dd..7bdc73012 100644 --- a/src/lib/formatItem.ts +++ b/src/lib/formatItem.ts @@ -52,7 +52,7 @@ export default () => ( linked: Linked, item: any, include: string[] = [], - derived: string[] + derived: string[] = [] ) => { // TODO: Support nested maps? // TODO: include alias support? @@ -116,12 +116,12 @@ export default () => ( }) }, {}) - const derivedAttribute = derived - .map(derivedAtrribute => attributes[derivedAtrribute].derive?.(formattedItem)) + const derivedAttributes = derived + .map(derivedAttribute => attributes[derivedAttribute].derive?.(formattedItem)) return { ...formattedItem, - ...derivedAttribute + ...derivedAttributes } } diff --git a/src/lib/parseMapping.ts b/src/lib/parseMapping.ts index 83a755a1e..047ac6384 100644 --- a/src/lib/parseMapping.ts +++ b/src/lib/parseMapping.ts @@ -134,13 +134,18 @@ export default ( break case 'derive': if (typeof config[prop] !== 'function') error(`'${prop}' must be a function`) - track.derived.push(field) break default: error(`'${prop}' is not a valid property type`) } }) + // Set derived and force no save + if (config.derive !== undefined) { + config.save = false + track.derived.push(field) + } + // Error on alias and map if (config.alias && config.map) error(`'${field}' cannot contain both an alias and a map`) From c71240b30c84d9774c14ed00334fbb8be8023154 Mon Sep 17 00:00:00 2001 From: codingnuclei Date: Thu, 7 Mar 2024 23:00:23 +0000 Subject: [PATCH 08/10] fix: derived state incorrectly merged --- src/lib/formatItem.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/lib/formatItem.ts b/src/lib/formatItem.ts index 7bdc73012..d2f6fef52 100644 --- a/src/lib/formatItem.ts +++ b/src/lib/formatItem.ts @@ -46,7 +46,7 @@ const unwrapAttributeValue = (value: NativeAttributeValue): boolean | number | return value } -// Format item based on attribute defnition +// Format item based on attribute definition export default () => ( attributes: { [key: string]: PureAttributeDefinition }, linked: Linked, @@ -61,7 +61,7 @@ export default () => ( // Intialize validate type const validateType = validateTypes() - const formattedItem = Object.keys(item).reduce((acc, field) => { + let formattedItem = Object.keys(item).reduce((acc, field) => { const link = linked[field] || (attributes[field] && attributes[field].alias && linked[attributes[field].alias!]) @@ -116,13 +116,15 @@ export default () => ( }) }, {}) - const derivedAttributes = derived - .map(derivedAttribute => attributes[derivedAttribute].derive?.(formattedItem)) + formattedItem = derived.reduce( + (acc, derivedAttribute) => ({ + ...acc, + [derivedAttribute]: attributes[derivedAttribute].derive?.(formattedItem) + }), + formattedItem + ) - return { - ...formattedItem, - ...derivedAttributes - } + return formattedItem } function escapeRegExp(text: string) { From d3247f05908e65b871d8afd537bd5a93c6bb313b Mon Sep 17 00:00:00 2001 From: codingnuclei Date: Sat, 9 Mar 2024 11:02:22 +0000 Subject: [PATCH 09/10] test: add missing tests --- src/__tests__/formatItem.unit.test.ts | 20 +++++++++++++++++++- src/__tests__/parseEntity.unit.test.ts | 4 +++- src/__tests__/parseMapping.unit.test.ts | 16 ++++++++++++++++ src/classes/Entity/types.ts | 2 +- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/__tests__/formatItem.unit.test.ts b/src/__tests__/formatItem.unit.test.ts index ef79d792f..9149d858f 100644 --- a/src/__tests__/formatItem.unit.test.ts +++ b/src/__tests__/formatItem.unit.test.ts @@ -36,7 +36,10 @@ DefaultTable.addEntity( linked4: ['composite1', 1, { save: false, alias: 'linked4_alias' }], composite2_alias: { type: 'string', map: 'composite2' }, linked5: ['composite2_alias', 0, { save: false }], - linked6: ['composite2_alias', 1, { save: false, alias: 'linked6_alias' }] + linked6: ['composite2_alias', 1, { save: false, alias: 'linked6_alias' }], + derived: { derive: (item) => item.derivedFrom1 + item.derivedFrom2 }, + derivedFrom1: { type:'number'}, + derivedFrom2: { type:'number'}, } } as const) ) @@ -195,4 +198,19 @@ describe('formatItem', () => { }) expect(result).toEqual({ number: null }) }) + + it('calculates derived states', () => { + const result = formatItem()(DefaultTable.User.schema.attributes, DefaultTable.User.linked, { + other: 'other', + derivedFrom1: 1, + derivedFrom2: 15 + }, [], ['derived']) + + expect(result).toEqual({ + other: 'other', + derivedFrom1: 1, + derivedFrom2: 15, + derived: 16 + }) + }) }) diff --git a/src/__tests__/parseEntity.unit.test.ts b/src/__tests__/parseEntity.unit.test.ts index e56a988de..1cce1b091 100644 --- a/src/__tests__/parseEntity.unit.test.ts +++ b/src/__tests__/parseEntity.unit.test.ts @@ -14,7 +14,8 @@ const entity = { pk: { partitionKey: true }, sk: { sortKey: true }, attr1: 'number', - attr2: { type: 'list', required: true } + attr2: { type: 'list', required: true }, + attr3: { derive: () => 'test' } }, autoExecute: true, autoParse: true @@ -35,6 +36,7 @@ describe('parseEntity', () => { expect(ent.autoParse).toBe(true) expect(ent._etAlias).toBe('typeAlias') expect(ent.typeHidden).toBe(true) + expect(ent.derived).toEqual(['attr3']) }) // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/src/__tests__/parseMapping.unit.test.ts b/src/__tests__/parseMapping.unit.test.ts index fa61fc719..b73818f5f 100644 --- a/src/__tests__/parseMapping.unit.test.ts +++ b/src/__tests__/parseMapping.unit.test.ts @@ -211,4 +211,20 @@ describe('parseMapping', () => { parseMapping('attr', { type: 'string', map: 'test', alias: 'testx' }, track) }).toThrow(`'attr' cannot contain both an alias and a map`) }) + + it('parses mapping with derived', async () => { + // Parse to string so we can compare without the derive function giving false negative + expect(JSON.stringify(parseMapping('attr', { derive: () => 'test' }, track))).toEqual( + JSON.stringify({ + attr: { save: false, derive: () => 'test', type: 'string', coerce: true, } + }) + ) + }) + + it('fails on non-function derive', async () => { + expect(() => { + // @ts-expect-error + parseMapping('attr', { derive: 'test' }, track) + }).toThrow(`'derive' must be a function`) + }) }) diff --git a/src/classes/Entity/types.ts b/src/classes/Entity/types.ts index a8882a24a..e3da469e3 100644 --- a/src/classes/Entity/types.ts +++ b/src/classes/Entity/types.ts @@ -96,7 +96,7 @@ export type PureAttributeDefinition = Partial<{ delimiter: string prefix: string suffix: string, - derive: (data: {}) => any + derive: (data: Record) => any }> export type CompositeAttributeDefinition = From e8c36e57afd17c312aae988042ba32652104075a Mon Sep 17 00:00:00 2001 From: codingnuclei Date: Sat, 9 Mar 2024 11:14:11 +0000 Subject: [PATCH 10/10] docs: add derived state --- docs/docs/entity/index.md | 3 ++- docs/docs/introduction/what-is-dynamodb-toolbox.md | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docs/entity/index.md b/docs/docs/entity/index.md index 4440699ee..2702e2f18 100644 --- a/docs/docs/entity/index.md +++ b/docs/docs/entity/index.md @@ -85,7 +85,8 @@ For more control over an attribute's behavior, you can specify an object as the | prefix | `string` | `string` | A prefix to be added to an attribute when saved to DynamoDB. This prefix will be removed when parsing the data. | | suffix | `string` | `string` | A suffix to be added to an attribute when saved to DynamoDB. This suffix will be removed when parsing the data. | | transform | `function` | all | A function that transforms the input before sending to DynamoDB. This accepts two arguments, the value passed and an object containing the data from other attributes. | -| format | `function` | all | A function that transforms the DynamoDB output before sending it to the parser. This accepts two arguments, the value of the attribute and an object containing the whole item. | +| format | `function` | all | A function that transforms the DynamoDB output before sending it to the parser. This accepts two arguments, the value of the attribute and an object containing the whole item. +| derive | `function` | all | A function that takes the parsed output and returns a derived state value. This accepts one argument, an object containing the whole parsed item. | | partitionKey | `boolean` or `string` | all | Flags an attribute as the 'partitionKey' for this Entity. If set to `true`, it will be mapped to the Table's `partitionKey`. If set to the name of an **index** defined on the Table, it will be mapped to the secondary index's `partitionKey` | | sortKey | `boolean` or `string` | all | Flags an attribute as the 'sortKey' for this Entity. If set to `true`, it will be mapped to the Table's `sortKey`. If set to the name of an **index** defined on the Table, it will be mapped to the secondary index's `sortKey` | diff --git a/docs/docs/introduction/what-is-dynamodb-toolbox.md b/docs/docs/introduction/what-is-dynamodb-toolbox.md index f07875037..80a4208de 100644 --- a/docs/docs/introduction/what-is-dynamodb-toolbox.md +++ b/docs/docs/introduction/what-is-dynamodb-toolbox.md @@ -25,6 +25,7 @@ If you like working with ORMs, that's great, and you should definitely give thes - **Bidirectional Mapping and Aliasing:** When building a single table design, you can define multiple entities that map to the same table. Each entity can reuse fields (like `pk` and`sk`) and map them to different aliases depending on the item type. Your data is automatically mapped correctly when reading and writing data. - **Type Coercion and Validation:** Automatically coerce values to strings, numbers and booleans to ensure consistent data types in your DynamoDB tables. Validate `list`, `map`, and `set` types against your data. Oh yeah, and `set`s are automatically handled for you. 😉 +- **Derived State:** Define Entity derived states. These sates will be calculated from a dynamodb response for you. - **Powerful Query Builder:** Specify a `partitionKey`, and then easily configure your sortKey conditions, filters, and attribute projections to query your primary or secondary indexes. This library can even handle pagination with a simple `.next()` method. - **Simple Table Scans:** Scan through your table or secondary indexes and add filters, projections, parallel scans and more. And don't forget the pagination support with `.next()`. - **Filter and Condition Expression Builder:** Build complex Filter and Condition expressions using a standardized `array` and `object` notation. No more appending strings!