Skip to content

Commit

Permalink
feat: strongly typed item methods
Browse files Browse the repository at this point in the history
  • Loading branch information
tranhl committed Feb 3, 2024
1 parent 6aec8db commit 1446a81
Show file tree
Hide file tree
Showing 12 changed files with 100 additions and 79 deletions.
12 changes: 7 additions & 5 deletions packages/dynamoose/lib/General.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Item} from "./Item";
import {Item, ItemMethods} from "./Item";
import {Model} from "./Model";

// - General
Expand All @@ -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]} & {};

interface ModelItemConstructor<T extends Item, U extends ItemMethods> {
/**
* In order to create a new item you just pass in your object into an instance of your model.
*
Expand All @@ -29,10 +31,10 @@ interface ModelItemConstructor<T extends Item> {
* // myUser is now an item instance of the User model
* ```
*/
new (object: {[key: string]: any}): T;
Model: Model<T>;
new (object: Expand<Omit<T, keyof Item>>): T & U;
Model: Model<T, U>;
}
export type ModelType<T extends Item> = T & Model<T> & ModelItemConstructor<T>;
export type ModelType<T extends Item, U extends ItemMethods> = T & Model<T, U> & ModelItemConstructor<T, U>;

// This represents an item array. This is used for the output of functions such as `scan`, `query`, and `batchGet`. These functions can extend this property to add additional properties or functions. However this represents the shared properties/functions for all item arrays.
export interface ItemArray<T> extends Array<T> {
Expand Down
8 changes: 8 additions & 0 deletions packages/dynamoose/lib/Item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ export interface ItemSettings {
type?: "fromDynamo" | "toDynamo";
}

export interface ItemMethods {
[key: string]: Function;
}

export interface AnyItemMethods {
[key: string]: any;
}

interface ItemInternalProperties {
originalObject: any;
originalSettings: ItemSettings;
Expand Down
90 changes: 45 additions & 45 deletions packages/dynamoose/lib/Model/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CustomError from "../Error";
import ModelStore from "../ModelStore";
import {Schema, SchemaDefinition, DynamoDBSetTypeResult, ValueType, IndexItem, TableIndex} from "../Schema";
import {Item as ItemCarrier, ItemSaveSettings, ItemSettings, ItemObjectFromSchemaSettings, AnyItem} from "../Item";
import {Item as ItemCarrier, ItemSaveSettings, ItemSettings, ItemObjectFromSchemaSettings, AnyItem, ItemMethods, AnyItemMethods} from "../Item";
import utils from "../utils";
import ddb from "../aws/ddb/internal";
import Internal from "../Internal";
Expand Down Expand Up @@ -131,7 +131,7 @@ interface ModelInternalProperties {
}

// Model represents a single entity (ex. User, Movie, Video, Order)
export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesClass<ModelInternalProperties> {
export class Model<T extends ItemCarrier = AnyItem, U extends ItemMethods = AnyItemMethods> extends InternalPropertiesClass<ModelInternalProperties> {
/**
* This method is the basic entry point for creating a model in Dynamoose. When you call this method a new model is created, and it returns an item initializer that you can use to create instances of the given model.
*
Expand All @@ -153,14 +153,14 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
* const Cat = dynamoose.model("Cat", new dynamoose.Schema({"name": String}));
* ```
*
* An optional TypeScript class which extends `Item` can be provided right before the function bracket. This provides type checking when using operations like `Model.create()`.
* An optional TypeScript type which extends `Item` can be provided as a generic to `dynamoose.model`. This provides type checking when using operations like `Model.create()`.
*
* ```ts
* import * as dynamoose from "dynamoose";
* import {Item} from "dynamoose/dist/Item";
*
* // Strongly typed model
* class Cat extends Item {
* interface Cat extends Item {
* id: number;
* name: string;
* }
Expand Down Expand Up @@ -439,21 +439,21 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
// latestTableDetails: DynamoDB.DescribeTableOutput;
// pendingTaskPromise: () => Promise<void>;
Item: typeof ItemCarrier;
scan: (object?: ConditionInitializer) => Scan<T>;
query: (object?: ConditionInitializer) => Query<T>;
scan: (object?: ConditionInitializer) => Scan<T & U>;
query: (object?: ConditionInitializer) => Query<T & U>;
methods: { item: { set: (name: string, fn: FunctionType) => void; delete: (name: string) => void }; set: (name: string, fn: FunctionType) => void; delete: (name: string) => void };
transaction: TransactionType;

// Batch Get
batchGet (keys: InputKey[]): Promise<ModelBatchGetItemsResponse<T>>;
batchGet (keys: InputKey[], callback: CallbackType<ModelBatchGetItemsResponse<T>, any>): void;
batchGet (keys: InputKey[]): Promise<ModelBatchGetItemsResponse<T & U>>;
batchGet (keys: InputKey[], callback: CallbackType<ModelBatchGetItemsResponse<T & U>, any>): void;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings & {"return": "request"}): Promise<DynamoDB.BatchGetItemInput>;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings & {"return": "request"}, callback: CallbackType<DynamoDB.BatchGetItemInput, any>): void;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings): Promise<ModelBatchGetItemsResponse<T>>;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings, callback: CallbackType<ModelBatchGetItemsResponse<T>, any>): void;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings & {"return": "items"}): Promise<ModelBatchGetItemsResponse<T>>;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings & {"return": "items"}, callback: CallbackType<ModelBatchGetItemsResponse<T>, any>): void;
batchGet (keys: InputKey[], settings?: ModelBatchGetSettings | CallbackType<ModelBatchGetItemsResponse<T>, any> | CallbackType<DynamoDB.BatchGetItemInput, any>, callback?: CallbackType<ModelBatchGetItemsResponse<T>, any> | CallbackType<DynamoDB.BatchGetItemInput, any>): void | Promise<DynamoDB.BatchGetItemInput> | Promise<ModelBatchGetItemsResponse<T>> {
batchGet (keys: InputKey[], settings: ModelBatchGetSettings): Promise<ModelBatchGetItemsResponse<T & U>>;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings, callback: CallbackType<ModelBatchGetItemsResponse<T & U>, any>): void;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings & {"return": "items"}): Promise<ModelBatchGetItemsResponse<T & U>>;
batchGet (keys: InputKey[], settings: ModelBatchGetSettings & {"return": "items"}, callback: CallbackType<ModelBatchGetItemsResponse<T & U>, any>): void;
batchGet (keys: InputKey[], settings?: ModelBatchGetSettings | CallbackType<ModelBatchGetItemsResponse<T & U>, any> | CallbackType<DynamoDB.BatchGetItemInput, any>, callback?: CallbackType<ModelBatchGetItemsResponse<T & U>, any> | CallbackType<DynamoDB.BatchGetItemInput, any>): void | Promise<DynamoDB.BatchGetItemInput> | Promise<ModelBatchGetItemsResponse<T & U>> {
if (typeof settings === "function") {
callback = settings;
settings = {"return": "items"};
Expand Down Expand Up @@ -528,9 +528,9 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
const localCallback: CallbackType<ItemCarrier[], any> = callback as CallbackType<ItemCarrier[], any>;
promise.then((response) => prepareResponse(response)).then((response) => localCallback(null, response)).catch((error) => localCallback(error));
} else {
return (async (): Promise<ModelBatchGetItemsResponse<T>> => {
return (async (): Promise<ModelBatchGetItemsResponse<T & U>> => {
const response = await promise;
return prepareResponse(response) as Promise<ModelBatchGetItemsResponse<T>>;
return prepareResponse(response) as Promise<ModelBatchGetItemsResponse<T & U>>;
})();
}
}
Expand Down Expand Up @@ -667,25 +667,25 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
}

// Update
update (obj: UpdatePartial<T>): Promise<T>;
update (obj: UpdatePartial<T>, callback: CallbackType<T, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>): Promise<T>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, callback: CallbackType<T, any>): void;
update (obj: UpdatePartial<T>): Promise<T & U>;
update (obj: UpdatePartial<T>, callback: CallbackType<T & U, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>): Promise<T & U>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, callback: CallbackType<T & U, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "request"}): Promise<DynamoDB.UpdateItemInput>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "request"}, callback: CallbackType<DynamoDB.UpdateItemInput, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings): Promise<T>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings, callback: CallbackType<T, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "document"}): Promise<T>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "document"}, callback: CallbackType<T, any>): void;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>): Promise<T>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, callback: CallbackType<T, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings): Promise<T & U>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings, callback: CallbackType<T & U, any>): void;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "document"}): Promise<T & U>;
update (keyObj: InputKey, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "document"}, callback: CallbackType<T & U, any>): void;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>): Promise<T & U>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, callback: CallbackType<T & U, any>): void;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "request"}): Promise<DynamoDB.UpdateItemInput>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "request"}, callback: CallbackType<DynamoDB.UpdateItemInput, any>): void;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings): Promise<T>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings, callback: CallbackType<T, any>): void;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "item"}): Promise<T>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "item"}, callback: CallbackType<T, any>): void;
update (keyObj: InputKey | ObjectType, updateObj?: UpdatePartial<T> | CallbackType<T, any> | CallbackType<DynamoDB.UpdateItemInput, any>, settings?: ModelUpdateSettings | CallbackType<T, any> | CallbackType<DynamoDB.UpdateItemInput, any>, callback?: CallbackType<T, any> | CallbackType<DynamoDB.UpdateItemInput, any>): void | Promise<T> | Promise<DynamoDB.UpdateItemInput> {
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings): Promise<T & U>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings, callback: CallbackType<T & U, any>): void;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "item"}): Promise<T & U>;
update (keyObj: ObjectType, updateObj: UpdatePartial<T>, settings: ModelUpdateSettings & {"return": "item"}, callback: CallbackType<T & U, any>): void;
update (keyObj: InputKey | ObjectType, updateObj?: UpdatePartial<T> | CallbackType<T & U, any> | CallbackType<DynamoDB.UpdateItemInput, any>, settings?: ModelUpdateSettings | CallbackType<T & U, any> | CallbackType<DynamoDB.UpdateItemInput, any>, callback?: CallbackType<T & U, any> | CallbackType<DynamoDB.UpdateItemInput, any>): void | Promise<T & U> | Promise<DynamoDB.UpdateItemInput> {
if (typeof updateObj === "function") {
callback = updateObj as CallbackType<ItemCarrier | DynamoDB.UpdateItemInput, any>; // TODO: fix this, for some reason `updateObj` has a type of Function which is forcing us to type cast it
updateObj = null;
Expand Down Expand Up @@ -924,15 +924,15 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
}

// Create
create (item: Partial<T>): Promise<T>;
create (item: Partial<T>, callback: CallbackType<T, any>): void;
create (item: Partial<T>): Promise<T & U>;
create (item: Partial<T>, callback: CallbackType<T & U, any>): void;
create (item: Partial<T>, settings: ItemSaveSettings & {return: "request"}): Promise<DynamoDB.PutItemInput>;
create (item: Partial<T>, settings: ItemSaveSettings & {return: "request"}, callback: CallbackType<DynamoDB.PutItemInput, any>): void;
create (item: Partial<T>, settings: ItemSaveSettings): Promise<T>;
create (item: Partial<T>, settings: ItemSaveSettings, callback: CallbackType<T, any>): void;
create (item: Partial<T>, settings: ItemSaveSettings & {return: "item"}): Promise<T>;
create (item: Partial<T>, settings: ItemSaveSettings & {return: "item"}, callback: CallbackType<T, any>): void;
create (item: Partial<T>, settings?: ItemSaveSettings | CallbackType<T, any> | CallbackType<DynamoDB.PutItemInput, any>, callback?: CallbackType<T, any> | CallbackType<DynamoDB.PutItemInput, any>): void | Promise<T> | Promise<DynamoDB.PutItemInput> {
create (item: Partial<T>, settings: ItemSaveSettings): Promise<T & U>;
create (item: Partial<T>, settings: ItemSaveSettings, callback: CallbackType<T & U, any>): void;
create (item: Partial<T>, settings: ItemSaveSettings & {return: "item"}): Promise<T & U>;
create (item: Partial<T>, settings: ItemSaveSettings & {return: "item"}, callback: CallbackType<T & U, any>): void;
create (item: Partial<T>, settings?: ItemSaveSettings | CallbackType<T & U, any> | CallbackType<DynamoDB.PutItemInput, any>, callback?: CallbackType<T & U, any> | CallbackType<DynamoDB.PutItemInput, any>): void | Promise<T & U> | Promise<DynamoDB.PutItemInput> {
if (typeof settings === "function" && !callback) {
callback = settings;
settings = {};
Expand Down Expand Up @@ -1004,15 +1004,15 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
}

// Get
get (key: InputKey): Promise<T>;
get (key: InputKey, callback: CallbackType<T, any>): void;
get (key: InputKey): Promise<T & U>;
get (key: InputKey, callback: CallbackType<T & U, any>): void;
get (key: InputKey, settings: ModelGetSettings & {return: "request"}): Promise<DynamoDB.GetItemInput>;
get (key: InputKey, settings: ModelGetSettings & {return: "request"}, callback: CallbackType<DynamoDB.GetItemInput, any>): void;
get (key: InputKey, settings: ModelGetSettings): Promise<T>;
get (key: InputKey, settings: ModelGetSettings, callback: CallbackType<T, any>): void;
get (key: InputKey, settings: ModelGetSettings & {return: "item"}): Promise<T>;
get (key: InputKey, settings: ModelGetSettings & {return: "item"}, callback: CallbackType<T, any>): void;
get (key: InputKey, settings?: ModelGetSettings | CallbackType<T, any> | CallbackType<DynamoDB.GetItemInput, any>, callback?: CallbackType<T, any> | CallbackType<DynamoDB.GetItemInput, any>): void | Promise<DynamoDB.GetItemInput> | Promise<T> {
get (key: InputKey, settings: ModelGetSettings): Promise<T & U>;
get (key: InputKey, settings: ModelGetSettings, callback: CallbackType<T & U, any>): void;
get (key: InputKey, settings: ModelGetSettings & {return: "item"}): Promise<T & U>;
get (key: InputKey, settings: ModelGetSettings & {return: "item"}, callback: CallbackType<T & U, any>): void;
get (key: InputKey, settings?: ModelGetSettings | CallbackType<T & U, any> | CallbackType<DynamoDB.GetItemInput, any>, callback?: CallbackType<T & U, any> | CallbackType<DynamoDB.GetItemInput, any>): void | Promise<DynamoDB.GetItemInput> | Promise<T & U> {
if (typeof settings === "function") {
callback = settings;
settings = {"return": "item"};
Expand Down Expand Up @@ -1070,7 +1070,7 @@ export class Model<T extends ItemCarrier = AnyItem> extends InternalPropertiesCl
}

// Serialize Many
serializeMany (itemsArray: ModelType<ItemCarrier>[] = [], nameOrOptions: SerializerOptions | string): any {
serializeMany (itemsArray: ModelType<ItemCarrier, U>[] = [], nameOrOptions: SerializerOptions | string): any {
return this.serializer.getInternalProperties(internalProperties).serializeMany(itemsArray, nameOrOptions);
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/dynamoose/lib/ModelStore.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import CustomError from "./Error";
import {Model} from "./Model";
import {Item} from "./Item";
import {Item, ItemMethods} from "./Item";
import Internal from "./Internal";
const {internalProperties} = Internal.General;

let models: {[name: string]: Model<Item>} = {};

const returnObject = <T extends Item>(input: Model<T> | string): Model<T> | never => {
const returnObject = <T extends Item, U extends ItemMethods>(input: Model<T, U> | string): Model<T, U> | never => {
if (input instanceof Model) {
models[input.name] = input;
return input;
} else if (typeof input === "string") {
return models[input] as Model<T>;
return models[input] as Model<T, U>;
} else {
throw new CustomError.InvalidParameter("You must pass in a Model or model name as a string.");
}
Expand Down
6 changes: 3 additions & 3 deletions packages/dynamoose/lib/Schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CustomError from "./Error";
import utils from "./utils";
import Internal from "./Internal";
import {Item, ItemObjectFromSchemaSettings} from "./Item";
import {AnyItemMethods, Item, ItemObjectFromSchemaSettings} from "./Item";
import {Model, ModelIndexes} from "./Model";
import * as DynamoDB from "@aws-sdk/client-dynamodb";
import {ModelType, ObjectType} from "./General";
Expand Down Expand Up @@ -273,7 +273,7 @@ const attributeTypes: (DynamoDBTypeResult | DynamoDBSetTypeResult)[] = utils.arr

type GeneralValueType = string | boolean | number | Buffer | Date;
export type ValueType = GeneralValueType | {[key: string]: ValueType} | ValueType[];
type AttributeType = string | StringConstructor | BooleanConstructor | NumberConstructor | typeof Buffer | DateConstructor | ObjectConstructor | ArrayConstructor | SetConstructor | symbol | Schema | ModelType<Item>;
type AttributeType = string | StringConstructor | BooleanConstructor | NumberConstructor | typeof Buffer | DateConstructor | ObjectConstructor | ArrayConstructor | SetConstructor | symbol | Schema | ModelType<Item, AnyItemMethods>;

export interface TimestampObject {
createdAt?: string | string[] | SchemaDefinition;
Expand Down Expand Up @@ -324,7 +324,7 @@ interface IndexDefinition {
}
interface AttributeDefinitionTypeSettings {
storage?: "milliseconds" | "seconds" | "iso";
model?: ModelType<Item>;
model?: ModelType<Item, AnyItemMethods>;
attributes?: string[];
separator?: string;
value?: string | boolean | number;
Expand Down
6 changes: 3 additions & 3 deletions packages/dynamoose/lib/Serializer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ObjectType, ModelType} from "./General";
import {Item} from "./Item";
import {AnyItemMethods, Item} from "./Item";
import CustomError from "./Error";
import utils from "./utils";
import {InternalPropertiesClass} from "./InternalPropertiesClass";
Expand All @@ -16,7 +16,7 @@ interface SerializerInternalProperties {
serializers: {[key: string]: SerializerOptions};
defaultSerializer?: string;

serializeMany: (itemsArray: ModelType<Item>[], nameOrOptions: SerializerOptions | string) => ObjectType[];
serializeMany: (itemsArray: ModelType<Item, AnyItemMethods>[], nameOrOptions: SerializerOptions | string) => ObjectType[];
serialize: (item: ObjectType, nameOrOptions: SerializerOptions | string) => ObjectType;
}

Expand All @@ -32,7 +32,7 @@ export class Serializer extends InternalPropertiesClass<SerializerInternalProper
"modify": (serialized: ObjectType, original: ObjectType): ObjectType => ({...original})
}
},
"serializeMany": (itemsArray: ModelType<Item>[], nameOrOptions: SerializerOptions | string): ObjectType[] => {
"serializeMany": (itemsArray: ModelType<Item, AnyItemMethods>[], nameOrOptions: SerializerOptions | string): ObjectType[] => {
if (!itemsArray || !Array.isArray(itemsArray)) {
throw new CustomError.InvalidParameter("itemsArray must be an array of item objects");
}
Expand Down

0 comments on commit 1446a81

Please sign in to comment.