-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #53 from dsdavis4/enum_attribute
Enum attribute
- Loading branch information
Showing
14 changed files
with
789 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { z } from "zod"; | ||
import type DynaRecord from "../../DynaRecord"; | ||
import Metadata from "../../metadata"; | ||
import type { AttributeDecoratorContext, AttributeOptions } from "../types"; | ||
|
||
/** | ||
* Extends {@link AttributeOptions} but requires that the allowed values are set on `values` | ||
*/ | ||
export interface EnumAttributeOptions extends AttributeOptions { | ||
// track this issue for supporting more than strings with zod runtime validations | ||
// https://github.com/colinhacks/zod/issues/2686 | ||
values: [string, ...string[]]; | ||
} | ||
|
||
/** | ||
* A decorator for marking class fields as enum attributes within the context of a single-table design entity. Only the types specified in `values` are allowed via both the type system and runtime schema checks. | ||
* | ||
* This enforces a union type of the specified fields listed in `values` | ||
* | ||
* @template T The entity the decorator is applied to. | ||
* @template K The type of the attribute, restricted to the values listed in `values` prop of EnumAttributeOptions | ||
* @param props An optional object of {@link EnumAttributeOptions}, including the allowed values, configuration options such as metadata attributes and additional property characteristics. | ||
* @returns A class field decorator function that targets and initializes the class's prototype to register the attribute with the ORM's metadata system, ensuring proper handling and validation of the entity's enum values. | ||
* | ||
* Usage example: | ||
* ```typescript | ||
* class Product extends BaseEntity { | ||
* @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" | ||
* | ||
* @EnumAttribute({ alias: 'MyNullableField', nullable: true, values: ["val-1", "val-2"] }) | ||
* public myNullableField?: "val-1" | "val-2"; // Set to Optional for nullable attributes | ||
* } | ||
* ``` | ||
*/ | ||
function EnumAttribute< | ||
T extends DynaRecord, | ||
const K extends P["values"][number], | ||
const P extends EnumAttributeOptions | ||
>(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.enum(props.values), | ||
...props | ||
}); | ||
}); | ||
} | ||
}; | ||
} | ||
|
||
export default EnumAttribute; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
/* eslint-disable @typescript-eslint/no-unused-vars */ | ||
import { Entity, EnumAttribute } from "../../../src/decorators"; | ||
import { MockTable } from "../../integration/mockModels"; | ||
import Metadata from "../../../src/metadata"; | ||
import { ZodEnum, ZodNullable, type ZodString, type ZodOptional } from "zod"; | ||
import { type ForeignKey, type NullableForeignKey } from "../../../src"; | ||
|
||
type EnumValues = "val-1" | "val-2"; | ||
|
||
@Entity | ||
class MyEntity extends MockTable { | ||
@EnumAttribute({ alias: "SomeEnum", values: ["val-1", "val-2"] }) | ||
public readonly someEnum: EnumValues; | ||
|
||
@EnumAttribute({ values: ["val-1", "val-2"] }) | ||
public readonly noAliasEnum: EnumValues; | ||
|
||
@EnumAttribute({ | ||
alias: "SomeNullableEnum", | ||
values: ["val-1", "val-2"], | ||
nullable: true | ||
}) | ||
public readonly someNullableEnum?: EnumValues; | ||
} | ||
|
||
describe("EnumAttribute", () => { | ||
it("uses the provided table alias as attribute metadata if one is provided", () => { | ||
expect.assertions(1); | ||
|
||
expect(Metadata.getEntityAttributes(MyEntity.name).someEnum).toEqual({ | ||
name: "someEnum", | ||
alias: "SomeEnum", | ||
nullable: false, | ||
type: expect.any(ZodEnum) | ||
}); | ||
}); | ||
|
||
it("defaults attribute metadata alias to the table key if alias is not provided", () => { | ||
expect.assertions(1); | ||
|
||
expect(Metadata.getEntityAttributes(MyEntity.name).noAliasEnum).toEqual({ | ||
name: "noAliasEnum", | ||
alias: "noAliasEnum", | ||
nullable: false, | ||
type: expect.any(ZodEnum) | ||
}); | ||
}); | ||
|
||
it("zod type is optional if nullable is true", () => { | ||
expect.assertions(1); | ||
|
||
expect( | ||
Metadata.getEntityAttributes(MyEntity.name).someNullableEnum | ||
).toEqual({ | ||
name: "someNullableEnum", | ||
alias: "SomeNullableEnum", | ||
nullable: true, | ||
type: expect.any(ZodNullable<ZodOptional<ZodString>>) | ||
}); | ||
}); | ||
|
||
describe("types", () => { | ||
it("can be applied to enum attributes", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-no-error: EnumValues is a valid type | ||
@EnumAttribute({ alias: "Key1", values: ["1", "2"] }) | ||
public key1: "1" | "2"; | ||
} | ||
|
||
@Entity | ||
class ModelTwo extends MockTable { | ||
// @ts-expect-no-error: EnumValues is a valid type | ||
@EnumAttribute({ alias: "Key1", values: ["val-1", "val-2"] }) | ||
public key1: EnumValues; | ||
} | ||
}); | ||
|
||
it("does not allow the property its applied to to be optional if its not nullable", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-error: EnumAttributes cant be optional unless nullable | ||
@EnumAttribute({ alias: "Key1", values: ["val-1", "val-2"] }) | ||
public key1?: EnumValues; | ||
} | ||
}); | ||
|
||
it("does allow the property its applied to to be optional if its nullable", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-no-error: EnumAttributes can be optional if nullable | ||
@EnumAttribute({ | ||
alias: "Key1", | ||
values: ["val-1", "val-2"], | ||
nullable: true | ||
}) | ||
public key1?: EnumValues; | ||
} | ||
}); | ||
|
||
it("does not support values that are not part of the enum", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-error: Only enum values types are allowed | ||
@EnumAttribute({ alias: "Key1", values: ["1", "2"] }) | ||
public key1: "1" | "3"; | ||
} | ||
|
||
@Entity | ||
class ModelTwo extends MockTable { | ||
// @ts-expect-error: Only enum values types are allowed | ||
@EnumAttribute({ alias: "Key1", values: ["val-1", "val-3"] }) | ||
public key1: EnumValues; | ||
} | ||
|
||
@Entity | ||
class ModelThree extends MockTable { | ||
// @ts-expect-error: Only enum values types are allowed | ||
@EnumAttribute({ alias: "Key1", values: ["1", "2"] }) | ||
public key1: string; | ||
} | ||
}); | ||
|
||
it("'alias' is optional", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-no-error: Alias prop is optional | ||
@EnumAttribute({ values: ["1", "2"] }) | ||
public key1: "1" | "2"; | ||
} | ||
}); | ||
|
||
it("if nullable is false the attribute is required", () => { | ||
@Entity | ||
class SomeModel extends MockTable { | ||
// @ts-expect-no-error: Nullable properties are required | ||
@EnumAttribute({ | ||
alias: "Key1", | ||
values: ["val-1", "val-2"], | ||
nullable: false | ||
}) | ||
public key1: EnumValues; | ||
|
||
// @ts-expect-error: Nullable properties are required | ||
@EnumAttribute({ | ||
alias: "Key2", | ||
values: ["val-1", "val-2"], | ||
nullable: false | ||
}) | ||
public key2?: EnumValues; | ||
} | ||
}); | ||
|
||
it("nullable defaults to false and makes the property required", () => { | ||
@Entity | ||
class SomeModel extends MockTable { | ||
// @ts-expect-no-error: Nullable properties are required | ||
@EnumAttribute({ values: ["val-1", "val-2"] }) | ||
public key1: EnumValues; | ||
|
||
// @ts-expect-error: Nullable properties are required | ||
@EnumAttribute({ alias: "Key2", values: ["val-1", "val-2"] }) | ||
public key2?: EnumValues; | ||
} | ||
}); | ||
|
||
it("when nullable is true, it will allow the property to be optional", () => { | ||
@Entity | ||
class SomeModel extends MockTable { | ||
// @ts-expect-no-error: Nullable properties are optional | ||
@EnumAttribute({ values: ["val-1", "val-2"], nullable: true }) | ||
public key1?: EnumValues; | ||
} | ||
}); | ||
|
||
it("ForeignKey is not a valid type to apply the EnumAttribute decorator", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-error: ForeignKey is not a valid type for Attribute decorator | ||
@EnumAttribute({ values: ["val-1", "val-2"] }) | ||
public key1: ForeignKey; | ||
} | ||
}); | ||
|
||
it("NullableForeignKey is not a valid type to apply the EnumAttribute decorator", () => { | ||
@Entity | ||
class ModelOne extends MockTable { | ||
// @ts-expect-error: NullableForeignKey is not a valid type for EnumAttribute decorator | ||
@EnumAttribute({ | ||
alias: "Key1", | ||
nullable: true, | ||
values: ["val-1", "val-2"] | ||
}) | ||
public key1?: NullableForeignKey; | ||
} | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.