Skip to content

Commit

Permalink
Merge pull request #54 from dsdavis4/custom_id
Browse files Browse the repository at this point in the history
Add support for custom id fields
  • Loading branch information
dsdavis4 authored Oct 18, 2024
2 parents 2ed30c4 + 14cbe4b commit 4067b96
Show file tree
Hide file tree
Showing 26 changed files with 842 additions and 119 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ By default, each entity will have [default attributes](https://dyna-record.com/t

- The partition key defined on the [table](#table) class
- The sort key defined on the [table](#table) class
- [id](https://dyna-record.com/classes/default.html#id) - The auto generated uuid for the model
- [id](https://dyna-record.com/classes/default.html#id) - The id for the model. This will be an autogenerated uuid unless [IdAttribute](<(https://dyna-record.com/functions/IdAttribute.html)>) is set on a non-nullable entity attribute.
- [type](https://dyna-record.com/classes/default.html#type) - The type of the entity. Value is the entity class name
- [createdAt](https://dyna-record.com/classes/default.html#updatedAt) - The timestamp of when the entity was created
- [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) - Timestamp of when the entity was updated last
Expand Down Expand Up @@ -143,6 +143,7 @@ Use the attribute decorators below to define attributes on a model. The decorato
- [@BooleanAttribute](https://dyna-record.com/functions/BooleanAttribute.html)
- [@DateAttribute](https://dyna-record.com/functions/DateAttribute.html)
- [@EnumAttribute](https://dyna-record.com/functions/EnumAttribute.html)
- [@IdAttribute](https://dyna-record.com/functions/IdAttribute.html)

- The [alias](https://dyna-record.com/interfaces/AttributeOptions.html#alias) option allows you to specify the attribute name as it appears in the DynamoDB table, different from your class property name.
- Set nullable attributes as optional for optimal type safety
Expand Down Expand Up @@ -309,7 +310,7 @@ class Student extends OtherTable {

[Docs](https://dyna-record.com/classes/default.html#create)

The create method is used to insert a new record into a DynamoDB table. This method automatically handles key generation (using UUIDs), timestamps for [createdAt](https://dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) fields, and the management of relationships between entities. It leverages AWS SDK's [TransactWriteCommand](https://www.google.com/search?q=aws+transact+write+command&oq=aws+transact+write+command&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIGCAEQRRg7MgYIAhBFGDvSAQgzMjAzajBqN6gCALACAA&sourceid=chrome&ie=UTF-8) for transactional integrity, ensuring either complete success or rollback in case of any failure. The method handles conditional checks to ensure data integrity and consistency during creation. If a foreignKey is set on create, dyna-record will de-normalize the data required in order to support the relationship
The create method is used to insert a new record into a DynamoDB table. This method automatically handles key generation (using UUIDs or custom id field if [IdAttribute](<(https://dyna-record.com/functions/IdAttribute.html)>) is set), timestamps for [createdAt](https://dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) fields, and the management of relationships between entities. It leverages AWS SDK's [TransactWriteCommand](https://www.google.com/search?q=aws+transact+write+command&oq=aws+transact+write+command&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIGCAEQRRg7MgYIAhBFGDvSAQgzMjAzajBqN6gCALACAA&sourceid=chrome&ie=UTF-8) for transactional integrity, ensuring either complete success or rollback in case of any failure. The method handles conditional checks to ensure data integrity and consistency during creation. If a foreignKey is set on create, dyna-record will de-normalize the data required in order to support the relationship

To use the create method, call it on the model class you wish to create a new record for. Pass the properties of the new record as an object argument to the method. Only attributes defined on the model can be configured, and will be enforced via types and runtime schema validation.

Expand Down Expand Up @@ -341,6 +342,7 @@ The method is designed to throw errors under various conditions, such as transac

- Automatic Timestamp Management: The [createdAt](https://dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) fields are managed automatically and reflect the time of creation and the last update, respectively.
- Automatic ID Generation: Each entity created gets a unique [id](https://dyna-record.com/classes/default.html#id) generated by the uuidv4 method.
- This can be customized [IdAttribute](<(https://dyna-record.com/functions/IdAttribute.html)>) to support custom id attributes
- Relationship Management: The ORM manages entity relationships through DynamoDB's single-table design patterns, creating and maintaining the necessary links between related entities.
- Conditional Checks: To ensure data integrity, the create method performs various conditional checks, such as verifying the existence of entities that new records relate to.
- Error Handling: Errors during the creation process are handled gracefully, with specific errors thrown for different failure scenarios, such as conditional check failures or transaction cancellations.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dyna-record",
"version": "0.1.4",
"version": "0.1.5",
"description": "Typescript Object Relational Mapper (ORM) for Dynamo",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
6 changes: 2 additions & 4 deletions src/decorators/attributes/BooleanAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ function BooleanAttribute<
context: AttributeDecoratorContext<T, K, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityAttribute(this.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
type: z.boolean(),
Expand Down
6 changes: 2 additions & 4 deletions src/decorators/attributes/DateAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ function DateAttribute<
context: AttributeDecoratorContext<T, K, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityAttribute(this.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
serializers: dateSerializer,
Expand Down
8 changes: 3 additions & 5 deletions src/decorators/attributes/EnumAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface EnumAttributeOptions extends AttributeOptions {
*
* Usage example:
* ```typescript
* class Product extends BaseEntity {
* class Product extends TableClass {
* @EnumAttribute({ alias: 'SomeField', values: ["val-1", "val-2"] })
* public someField: "val-1" | "val-2"; // Attribute representing the union/enum types specified in `values`. In this case the only allowed values are "val-1" and "val-2"
*
Expand All @@ -43,10 +43,8 @@ function EnumAttribute<
context: AttributeDecoratorContext<T, K, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityAttribute(this.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
type: z.enum(props.values),
Expand Down
8 changes: 3 additions & 5 deletions src/decorators/attributes/ForeignKeyAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { AttributeDecoratorContext, AttributeOptions } from "../types";
*
* Usage example:
* ```typescript
* class Order extends BaseEntity {
* class Order extends TableClass {
* @ForeignKeyAttribute({ alias: 'UserID' })
* public userId: ForeignKey; // Foreign key to the User entity. Cannot be optional.
*
Expand Down Expand Up @@ -46,10 +46,8 @@ function ForeignKeyAttribute<T extends DynaRecord, P extends AttributeOptions>(
>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityAttribute(this.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
type: z.string(),
Expand Down
42 changes: 42 additions & 0 deletions src/decorators/attributes/IdAttribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type DynaRecord from "../../DynaRecord";
import Metadata from "../../metadata";

/**
* An optional decorator for marking class fields as the primary identifier (ID) within the context of a single-table design entity.
*
* By default, if no IdAttribute is specified on an entity, then a uuid is generated for each entity
*
* Use this decorator to specify the field you want to use as the id for each entity. For example, if you wanted a users email to be their id.
*
* This decorator registers the decorated field as the unique identifier for the entity, ensuring proper entity identity management within the ORM.
*
* IMPORTANT! - The ID field is a required field and must be a unique identifier for each entity instance. It cannot be applied to nullable attributes
*
* @template T The entity the decorator is applied to, extending {@link DynaRecord}.
* @template K The type of the field being marked as the ID.
* @param _value This parameter is unused but required for compatibility with the class field decorator structure.
* @param context Provides metadata about the field being decorated, including its name and class.
* @returns A class field decorator function that registers the decorated field as the entity's ID within the ORM metadata system.
*
* Usage example:
* ```typescript
* class User extends TableClass {
* @IdAttribute
* @StringAttribute({ alias: "Email" })
* public readonly email: string;
* }
* ```
*
* Here, `@IdAttribute` decorates `email` of `User` as the primary identifier, ensuring it is registered and managed as the entity's unique key.
*/
function IdAttribute<T extends DynaRecord, K extends string>(
_value: undefined,
context: ClassFieldDecoratorContext<T, K>
) {
return function (this: T, value: K) {
Metadata.addEntityIdField(this.constructor.name, context.name.toString());
return value;
};
}

export default IdAttribute;
6 changes: 2 additions & 4 deletions src/decorators/attributes/NumberAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ function NumberAttribute<
context: AttributeDecoratorContext<T, K, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityAttribute(this.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
type: z.number(),
Expand Down
8 changes: 3 additions & 5 deletions src/decorators/attributes/PartitionKeyAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { NonNullAttributeOptions } from "../types";
*
* Usage example:
* ```typescript
* class User extends BaseEntity {
* class User extends TableClass {
* @PartitionKeyAttribute()
* public pk: PartitionKey;
* }
Expand All @@ -30,10 +30,8 @@ function PartitionKeyAttribute<T extends DynaRecord, K extends PartitionKey>(
context: ClassFieldDecoratorContext<T, K>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addPartitionKeyAttribute(entity, {
context.addInitializer(function (this: T) {
Metadata.addPartitionKeyAttribute(this, {
attributeName: context.name.toString(),
type: z.string(),
...props
Expand Down
8 changes: 3 additions & 5 deletions src/decorators/attributes/SortKeyAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { NonNullAttributeOptions } from "../types";
*
* Usage example:
* ```typescript
* class User extends BaseEntity {
* class User extends TableClass {
* @SortKeyAttribute()
* public sk: SortKey;
* }
Expand All @@ -30,10 +30,8 @@ function SortKeyAttribute<T extends DynaRecord, K extends SortKey>(
context: ClassFieldDecoratorContext<T, K>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addSortKeyAttribute(entity, {
context.addInitializer(function (this: T) {
Metadata.addSortKeyAttribute(this, {
attributeName: context.name.toString(),
type: z.string(),
...props
Expand Down
8 changes: 3 additions & 5 deletions src/decorators/attributes/StringAttribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
*
* Usage example:
* ```typescript
* class Product extends BaseEntity {
* class Product extends TableClass {
* @StringAttribute({ alias: 'SKU' })
* public stockKeepingUnit: string; // Simple string attribute representing the product's SKU
*
Expand All @@ -40,10 +40,8 @@ function StringAttribute<
context: AttributeDecoratorContext<T, NotForeignKey<K>, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityAttribute(this.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
type: z.string(),
Expand Down
1 change: 1 addition & 0 deletions src/decorators/attributes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export { default as StringAttribute } from "./StringAttribute";
export { default as BooleanAttribute } from "./BooleanAttribute";
export { default as NumberAttribute } from "./NumberAttribute";
export { default as EnumAttribute } from "./EnumAttribute";
export { default as IdAttribute } from "./IdAttribute";
export * from "./serializers";
8 changes: 3 additions & 5 deletions src/decorators/relationships/BelongsTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import type { BelongsToField, BelongsToProps } from "./types";
*
* Usage example:
* ```typescript
* class Order extends BaseEntity {
* class Order extends TableClass {
* @ForeignKeyProperty({ alias: "UserId" })
* public readonly userId: ForeignKey;
*
Expand All @@ -43,10 +43,8 @@ function BelongsTo<T extends DynaRecord, K extends DynaRecord>(
>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityRelationship(entity.constructor.name, {
context.addInitializer(function (this: T) {
Metadata.addEntityRelationship(this.constructor.name, {
type: "BelongsTo",
propertyName: context.name as keyof DynaRecord,
target: getTarget(),
Expand Down
9 changes: 4 additions & 5 deletions src/decorators/relationships/HasAndBelongsToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ interface HasAndBelongsToManyProps<
*
* Usage example:
* ```typescript
* class User extends BaseEntity {
* class User extends TableClass {
* @HasAndBelongsToMany(() => Group, {
* targetKey: 'users',
* through: () => ({
Expand All @@ -67,7 +67,7 @@ interface HasAndBelongsToManyProps<
* public groups: Group[];
* }
*
* class Group extends BaseEntity {
* class Group extends TableClass {
* @HasAndBelongsToMany(() => User, {
* targetKey: 'groups',
* through: () => ({
Expand Down Expand Up @@ -96,12 +96,11 @@ function HasAndBelongsToMany<
) {
return (_value: undefined, context: ClassFieldDecoratorContext<K, T[]>) => {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);
context.addInitializer(function (this: K) {
const target = getTarget();
const { joinTable, foreignKey } = props.through();

Metadata.addEntityRelationship(entity.constructor.name, {
Metadata.addEntityRelationship(this.constructor.name, {
type: "HasAndBelongsToMany",
propertyName: context.name as keyof DynaRecord,
target,
Expand Down
10 changes: 4 additions & 6 deletions src/decorators/relationships/HasMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ interface HasManyProps<T extends DynaRecord> {
*
* Usage example:
* ```typescript
* class User extends BaseEntity {
* class User extends TableClass {
* @HasMany(() => Post, { foreignKey: 'userId' })
* public posts: Post[];
* }
*
* class Post extends BaseEntity {
* class Post extends TableClass {
* @ForeignKeyProperty()
* public readonly userId: ForeignKey;
*
Expand All @@ -39,10 +39,8 @@ function HasMany<T extends DynaRecord, K extends DynaRecord>(
) {
return (_value: undefined, context: ClassFieldDecoratorContext<K, T[]>) => {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityRelationship(entity.constructor.name, {
context.addInitializer(function (this: K) {
Metadata.addEntityRelationship(this.constructor.name, {
type: "HasMany",
propertyName: context.name as keyof DynaRecord,
target: getTarget(),
Expand Down
10 changes: 4 additions & 6 deletions src/decorators/relationships/HasOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ interface HasOneProps<T extends DynaRecord> {
*
* Usage example:
* ```typescript
* class User extends BaseEntity {
* class User extends TableClass {
* @HasOne(() => Profile, { foreignKey: 'userId' })
* public profile?: Profile;
* }
*
* class Profile extends BaseEntity {
* class Profile extends TableClass {
* @ForeignKeyProperty()
* public readonly userId: ForeignKey;
*
Expand All @@ -42,10 +42,8 @@ function HasOne<T extends DynaRecord, K extends DynaRecord>(
context: ClassFieldDecoratorContext<K, Optional<T>>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityRelationship(entity.constructor.name, {
context.addInitializer(function (this: K) {
Metadata.addEntityRelationship(this.constructor.name, {
type: "HasOne",
propertyName: context.name as keyof DynaRecord,
target: getTarget(),
Expand Down
5 changes: 5 additions & 0 deletions src/metadata/EntityMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class EntityMetadata {

public readonly EntityClass: EntityClass;

/**
* Optional attribute of an entity, used with @IdAttribute decorator when an entity has a custom id field
*/
public idField: string;

/**
* Zod schema for runtime validation on entity attributes. Validates all attributes (used on Create)
*/
Expand Down
10 changes: 10 additions & 0 deletions src/metadata/MetadataStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,16 @@ class MetadataStorage {
entityMetadata.addAttribute(meta);
}

/**
* Store the entities optional id field attribute. Used with @IdAttribute
* @param entityName
* @param fieldName
*/
public addEntityIdField(entityName: string, fieldName: string): void {
const entityMetadata = this.#entities[entityName];
entityMetadata.idField = fieldName;
}

/**
* Adds the partition key attribute to Table metadata storage
* @param entityClass
Expand Down
Loading

0 comments on commit 4067b96

Please sign in to comment.