Skip to content

Commit

Permalink
Merge pull request #49 from dsdavis4/schema_validation
Browse files Browse the repository at this point in the history
Schema validation
  • Loading branch information
dsdavis4 authored Oct 7, 2024
2 parents e87a0bd + 45a9394 commit 39d9df3
Show file tree
Hide file tree
Showing 48 changed files with 1,366 additions and 739 deletions.
58 changes: 19 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,49 +134,37 @@ class Course extends MyTable {

### Attributes

For [natively supported data types](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes), define attributes using the [@Attribute](https://dyna-record.com/functions/Attribute.html) or [@NullableAttribute](https://dyna-record.com/functions/NullableAttribute.html) decorators. This decorator maps class properties to DynamoDB table attributes.
For [natively supported data types](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html#HowItWorks.DataTypes), define attributes using the [@Attribute](https://dyna-record.com/functions/Attribute.html) decorator. This decorator maps class properties to DynamoDB table attributes.

Use the attribute decorators below to define attributes on a model. The decorator maps class properties to DynamoDB table attributes.

- Attribute decorators

- [@StringAttribute](https://dyna-record.com/functions/StringAttribute.html)
- [@NumberAttribute](https://dyna-record.com/functions/NumberAttribute.html)
- [@BooleanAttribute](https://dyna-record.com/functions/BooleanAttribute.html)
- [@DateAttribute](https://dyna-record.com/functions/DateAttribute.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
- Attempting to remove a non-nullable attribute will result in a [NullConstrainViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)

```typescript
import { Entity, Attribute, NullableAttribute } from "dyna-record";
import { Entity, Attribute } from "dyna-record";

@Entity
class Student extends MyTable {
@Attribute({ alias: "Username" }) // Sets alias if field in Dynamo is different then on the model
@StringAttribute({ alias: "Username" }) // Sets alias if field in Dynamo is different then on the model
public username: string;

@Attribute() // Dynamo field and entity field are the same
@StringAttribute() // Dynamo field and entity field are the same
public email: string;

@NullableAttribute()
@NumberAttribute({ nullable: true })
public someAttribute?: number; // Mark as optional
}
```

### Date Attributes

Dates are not natively supported in Dynamo. To define a date attribute use [@DateAttribute](https://dyna-record.com/functions/DateAttribute.html) or [@NullableDateAttribute](https://dyna-record.com/functions/DateNullableAttribute.html) decorators. dyna-record will save the values as ISO strings in Dynamo, but serialize them as JS date objects on the entity instance

- 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
- Attempting to remove a non-nullable attribute will result in a [NullConstrainViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)

```typescript
import { Entity, DateAttribute, NullableDateAttribute } from "dyna-record";

@Entity
class Student extends MyTable {
@DateAttribute()
public readonly signUpDate: Date;

@NullableDateAttribute({ alias: "LastLogin" })
public readonly lastLogin?: Date; // Set as optional
}
```

### Foreign Keys

Define foreign keys in order to support [@BelongsTo](https://dyna-record.com/functions/BelongsTo.html) relationships. A foreign key is required for [@HasOne](https://dyna-record.com/functions/HasOne.html) and [@HasMany](https://dyna-record.com/functions/HasMany.html) relationships.
Expand All @@ -190,7 +178,6 @@ import {
Entity,
ForeignKeyAttribute,
ForeignKey,
NullableForeignKeyAttribute,
NullableForeignKey,
BelongsTo
} from "dyna-record";
Expand All @@ -206,7 +193,7 @@ class Assignment extends MyTable {

@Entity
class Course extends MyTable {
@NullableForeignKeyAttribute()
@ForeignKeyAttribute({ nullable: true })
public readonly teacherId?: NullableForeignKey; // Set as optional

@BelongsTo(() => Teacher, { foreignKey: "teacherId" })
Expand All @@ -220,8 +207,7 @@ Dyna-Record supports defining relationships between entities such as [@HasOne](h

A relationship can be defined as nullable or non-nullable. Non-nullable relationships will be enforced via transactions and violations will result in [NullConstraintViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)

- [@ForeignKeyAttribute](https://dyna-record.com/functions/ForeignKeyAttribute.html) is used to define a foreign key that links to another entity and is not nullable.
- [@NullableForeignKeyAttribute](https://dyna-record.com/functions/NullableForeignKeyAttribute.html) is used to define a foreign key that links to another entity and is nullable.
- [@ForeignKeyAttribute](https://dyna-record.com/functions/ForeignKeyAttribute.html) is used to define a foreign key that links to another entity
- Relationship decorators ([@HasOne](#hasone), [@HasMany](#hasmany), [@BelongsTo](https://dyna-record.com/functions/BelongsTo.html), [@HasAndBelongsToMany](#hasandbelongstomany)) define how entities relate to each other.

#### HasOne
Expand Down Expand Up @@ -260,13 +246,7 @@ class Grade extends MyTable {
[Docs](https://dyna-record.com/functions/HasMany.html)

```typescript
import {
Entity,
NullableForeignKeyAttribute,
NullableForeignKey,
BelongsTo,
HasMany
} from "dyna-record";
import { Entity, NullableForeignKey, BelongsTo, HasMany } from "dyna-record";

@Entity
class Teacher extends MyTable {
Expand All @@ -277,8 +257,8 @@ class Teacher extends MyTable {

@Entity
class Course extends MyTable {
@NullableForeignKeyAttribute()
public readonly teacherId?: NullableForeignKey;
@ForeignKeyAttribute({ nullable: true })
public readonly teacherId?: NullableForeignKey; // Mark as optional

// 'teacherId' Must be defined on self as ForeignKey or NullableForeignKey
@BelongsTo(() => Teacher, { foreignKey: "teacherId" })
Expand Down
23 changes: 16 additions & 7 deletions package-lock.json

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

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dyna-record",
"version": "0.0.20",
"version": "0.1.0",
"description": "Typescript Object Relational Mapper (ORM) for Dynamo",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -27,7 +27,8 @@
"@aws-sdk/client-dynamodb": "^3.502.0",
"@aws-sdk/lib-dynamodb": "^3.502.0",
"@aws-sdk/util-dynamodb": "^3.509.0",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@aws-sdk/types": "^3.502.0",
Expand Down
8 changes: 4 additions & 4 deletions src/DynaRecord.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Metadata, { tableDefaultFields } from "./metadata";
import { type QueryOptions as QueryBuilderOptions } from "./query-utils";
import { Attribute, DateAttribute } from "./decorators";
import { DateAttribute, StringAttribute } from "./decorators";
import {
FindById,
type FindByIdOptions,
Expand Down Expand Up @@ -51,13 +51,13 @@ abstract class DynaRecord implements DynaRecordBase {
/**
* A unique identifier for the entity itself, automatically generated upon creation.
*/
@Attribute({ alias: tableDefaultFields.id.alias })
@StringAttribute({ alias: tableDefaultFields.id.alias })
public readonly id: string;

/**
* The type of the Entity
*/
@Attribute({ alias: tableDefaultFields.type.alias })
@StringAttribute({ alias: tableDefaultFields.type.alias })
public readonly type: string;

/**
Expand Down Expand Up @@ -233,7 +233,7 @@ abstract class DynaRecord implements DynaRecordBase {
* Update an entity. If foreign keys are included in the attribute then:
* - BelongsToLinks will be created accordingly
* - If the entity already had a foreign key relationship, then those BelongsToLinks will be deleted
* - If the foreign key is not nullable then a {@link NullConstraintViolationError} is thrown. See {@link NullableForeignKeyAttribute}
* - If the foreign key is not nullable then a {@link NullConstraintViolationError} is thrown.
* - Validation errors will be thrown if the attribute being removed is not nullable
* @param id - The id of the entity to update
* @param attributes - Attributes to update
Expand Down
47 changes: 0 additions & 47 deletions src/decorators/attributes/Attribute.ts

This file was deleted.

53 changes: 53 additions & 0 deletions src/decorators/attributes/BooleanAttribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { z } from "zod";
import type DynaRecord from "../../DynaRecord";
import Metadata from "../../metadata";
import type { AttributeDecoratorContext, AttributeOptions } from "../types";

/**
* A decorator for marking class fields as boolean attributes within the context of a single-table design entity
*
* Can be set to nullable via decorator props
*
* @template T The class type that the decorator is applied to, ensuring type safety and integration within specific class instances.
* @template K A type constraint extending `boolean`, ensuring that the decorator is only applied to class fields specifically intended to represent booleans.
* @param props An {@link AttributeOptions} object providing configuration options for the attribute, such as its `alias` which allows the attribute to be referred to by an alternative name in the database context. The `nullable` property is also set to `false` by default.
* @returns A class field decorator function that operates within the class field's context. It configures the field as a boolean attribute and defines how it should be serialized and deserialized to/from DynamoDB.
*
* Usage example:
* ```typescript
* class MyEntity extends MyTable {
* @BooleanAttribute({ alias: 'MyField' })
* public myField: boolean;
*
* @BooleanAttribute({ alias: 'MyNullableField', nullable: true })
* public myField?: boolean; // Set to Optional
* }
* ```
*
* Here, `@BooleanAttribute` decorates `myField` of `MyEntity`, marking it as an entity attribute with an alias 'MyField' for ORM purposes.
*/
function BooleanAttribute<
T extends DynaRecord,
K extends boolean,
P extends AttributeOptions
>(props?: P) {
return function (
_value: undefined,
context: AttributeDecoratorContext<T, K, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
attributeName: context.name.toString(),
nullable: props?.nullable,
type: z.boolean(),
...props
});
});
}
};
}

export default BooleanAttribute;
29 changes: 19 additions & 10 deletions src/decorators/attributes/DateAttribute.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,52 @@
import { z } from "zod";
import type DynaRecord from "../../DynaRecord";
import Metadata from "../../metadata";
import type { AttributeOptions } from "../types";
import type { AttributeDecoratorContext, AttributeOptions } from "../types";
import { dateSerializer } from "./serializers";

/**
* Similar to '@Attribute' but specific to Dates since Dates are not native types to dynamo
* A decorator for marking class fields as date attributes within the context of a single-table design entity
*
* Does not allow property to be optional.
* Because dates are not natively supported data types in dynamo, they will be converted to iso string's within dynamo, and serialized to date objects on the entity class
*
* Can be set to nullable via decorator props
*
* @template T The class type that the decorator is applied to, ensuring type safety and integration within specific class instances.
* @template K A type constraint extending `Date`, ensuring that the decorator is only applied to class fields specifically intended to represent dates.
* @param props An {@link AttributeOptions} object providing configuration options for the attribute, such as its `alias` which allows the attribute to be referred to by an alternative name in the database context. The `nullable` property is also set to `false` by default; the attribute must not be empty.
* @param props An {@link AttributeOptions} object providing configuration options for the attribute, such as its `alias` which allows the attribute to be referred to by an alternative name in the database context. The `nullable` property is also set to `false` by default.
* @returns A class field decorator function that operates within the class field's context. It configures the field as a date attribute and defines how it should be serialized and deserialized to/from DynamoDB.
*
* Usage example:
* ```typescript
* class MyEntity extends MyTable {
* @DateAttribute({ alias: 'MyField' })
* public myField: Date;
*
* @DateAttribute({ alias: 'MyNullableField', nullable: true })
* public myField?: Date; // Set to Optional
* }
* ```
*
* Here, `@Attribute` decorates `myField` of `MyEntity`, marking it as an entity attribute with an alias 'MyField' for ORM purposes.
* Here, `@DateAttribute` decorates `myField` of `MyEntity`, marking it as an entity attribute with an alias 'MyField' for ORM purposes.
*/
function DateAttribute<T extends DynaRecord, K extends Date>(
props?: AttributeOptions
) {
function DateAttribute<
T extends DynaRecord,
K extends Date,
P extends AttributeOptions
>(props?: P) {
return function (
_value: undefined,
context: ClassFieldDecoratorContext<T, K>
context: AttributeDecoratorContext<T, K, P>
) {
if (context.kind === "field") {
context.addInitializer(function () {
const entity: DynaRecord = Object.getPrototypeOf(this);

Metadata.addEntityAttribute(entity.constructor.name, {
attributeName: context.name.toString(),
nullable: false,
nullable: props?.nullable,
serializers: dateSerializer,
type: z.date(),
...props
});
});
Expand Down
Loading

0 comments on commit 39d9df3

Please sign in to comment.