Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add @AllowNull, @AutoIncrement, @Comment, @Default, @PrimaryKey, and validation decorators #15384

Merged
merged 10 commits into from
Dec 10, 2022
93 changes: 93 additions & 0 deletions src/decorators/legacy/attribute-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { ModelAttributeColumnOptions, ModelStatic } from '../../model.js';
import { Model } from '../../model.js';
import { registerModelAttributeOptions } from '../shared/model.js';
import type {
OptionalParameterizedPropertyDecorator,
RequiredParameterizedPropertyDecorator,
} from './decorator-utils.js';
import {
createOptionallyParameterizedPropertyDecorator,
DECORATOR_NO_DEFAULT,
throwMustBeInstanceProperty,
throwMustBeMethod,
} from './decorator-utils.js';

/**
* Creates a decorator that registers Attribute Options. Parameters are mandatory.
*
* @param decoratorName The name of the decorator (must be equal to its export key)
* @param callback The callback that will return the Attribute Options.
*/
export function createRequiredAttributeOptionsDecorator<T>(
decoratorName: string,
callback: (
option: T,
target: Object,
propertyName: string | symbol,
propertyDescriptor: PropertyDescriptor | undefined,
) => Partial<ModelAttributeColumnOptions>,
): RequiredParameterizedPropertyDecorator<T> {
return createOptionalAttributeOptionsDecorator(decoratorName, DECORATOR_NO_DEFAULT, callback);
}

/**
* Creates a decorator that registers Attribute Options. Parameters are optional.
*
* @param decoratorName The name of the decorator (must be equal to its export key)
* @param defaultValue The default value, if no parameter was provided.
* @param callback The callback that will return the Attribute Options.
*/
export function createOptionalAttributeOptionsDecorator<T>(
decoratorName: string,
defaultValue: T | typeof DECORATOR_NO_DEFAULT,
callback: (
option: T,
target: Object,
propertyName: string | symbol,
propertyDescriptor: PropertyDescriptor | undefined,
) => Partial<ModelAttributeColumnOptions>,
): OptionalParameterizedPropertyDecorator<T> {
return createOptionallyParameterizedPropertyDecorator(
ephys marked this conversation as resolved.
Show resolved Hide resolved
decoratorName,
defaultValue,
(decoratorOption, target, propertyName, propertyDescriptor) => {
const attributeOptions = callback(decoratorOption, target, propertyName, propertyDescriptor);

annotate(decoratorName, target, propertyName, propertyDescriptor, attributeOptions);
},
);
}

function annotate(
decoratorName: string,
target: Object,
propertyName: string | symbol,
propertyDescriptor: PropertyDescriptor | undefined,
options: Partial<ModelAttributeColumnOptions>,
): void {
if (typeof propertyName === 'symbol') {
throw new TypeError('Symbol Model Attributes are not currently supported. We welcome a PR that implements this feature.');
}

if (typeof target === 'function') {
throwMustBeInstanceProperty(decoratorName, target, propertyName);
}

if (!(target instanceof Model)) {
throwMustBeMethod(decoratorName, target, propertyName);
}

options = { ...options };

if (propertyDescriptor) {
if (propertyDescriptor.get) {
options.get = propertyDescriptor.get;
}

if (propertyDescriptor.set) {
options.set = propertyDescriptor.set;
}
}

registerModelAttributeOptions(target.constructor as ModelStatic, propertyName, options);
}
88 changes: 27 additions & 61 deletions src/decorators/legacy/attribute.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { isDataType } from '../../dialects/abstract/data-types-utils.js';
import type { DataType } from '../../dialects/abstract/data-types.js';
import type { ModelAttributeColumnOptions, ModelStatic } from '../../model.js';
import { Model } from '../../model.js';
import type { ModelAttributeColumnOptions } from '../../model.js';
import { columnToAttribute } from '../../utils/deprecations.js';
import { registerModelAttributeOptions } from '../shared/model.js';
import { createOptionalAttributeOptionsDecorator, createRequiredAttributeOptionsDecorator } from './attribute-utils.js';
import type { PropertyOrGetterDescriptor } from './decorator-utils.js';
import { makeParameterizedPropertyDecorator } from './decorator-utils.js';

type AttributeDecoratorOption = DataType | Partial<ModelAttributeColumnOptions> | undefined;
type AttributeDecoratorOption = DataType | Partial<ModelAttributeColumnOptions>;

export const Attribute = makeParameterizedPropertyDecorator<AttributeDecoratorOption>(undefined, (
option: AttributeDecoratorOption,
target: Object,
propertyName: string | symbol,
propertyDescriptor?: PropertyDescriptor,
) => {
if (!option) {
throw new Error('Decorator @Attribute requires an argument');
export const Attribute = createRequiredAttributeOptionsDecorator<AttributeDecoratorOption>('Attribute', attrOptionOrDataType => {
if (isDataType(attrOptionOrDataType)) {
return {
type: attrOptionOrDataType,
};
}

annotate(target, propertyName, propertyDescriptor, option);
return attrOptionOrDataType;
});

/**
Expand All @@ -35,58 +30,29 @@ export function Column(optionsOrDataType: DataType | ModelAttributeColumnOptions
type UniqueOptions = NonNullable<ModelAttributeColumnOptions['unique']>;

/**
* Sets the unique option true for annotated property
* Configures the unique option of the attribute.
*/
export const Unique = makeParameterizedPropertyDecorator<UniqueOptions>(true, (
option: UniqueOptions,
target: Object,
propertyName: string | symbol,
propertyDescriptor?: PropertyDescriptor,
) => {
annotate(target, propertyName, propertyDescriptor, { unique: option });
});

function annotate(
target: Object,
propertyName: string | symbol,
propertyDescriptor: PropertyDescriptor | undefined,
optionsOrDataType: Partial<ModelAttributeColumnOptions> | DataType,
): void {
if (typeof propertyName === 'symbol') {
throw new TypeError('Symbol Model Attributes are not currently supported. We welcome a PR that implements this feature.');
}
export const Unique = createOptionalAttributeOptionsDecorator<UniqueOptions>('Unique', true, (unique: UniqueOptions) => ({ unique }));

if (typeof target === 'function') {
throw new TypeError(
`Decorator @Attribute has been used on "${target.name}.${String(propertyName)}", which is static. This decorator can only be used on instance properties, setters and getters.`,
);
}
/**
* Makes the attribute accept null values. Opposite of {@link NotNull}.
*/
export const AllowNull = createOptionalAttributeOptionsDecorator<boolean>('AllowNull', true, (allowNull: boolean) => ({ allowNull }));

if (!(target instanceof Model)) {
throw new TypeError(
`Decorator @Attribute has been used on "${target.constructor.name}.${String(propertyName)}", but class "${target.constructor.name}" does not extend Model. This decorator can only be used on models.`,
);
}
/**
* Makes the attribute reject null values. Opposite of {@link AllowNull}.
*/
export const NotNull = createOptionalAttributeOptionsDecorator<boolean>('NotNull', true, (notNull: boolean) => ({ allowNull: !notNull }));

let options: Partial<ModelAttributeColumnOptions>;
export const AutoIncrement = createOptionalAttributeOptionsDecorator<boolean>('AutoIncrement', true, (autoIncrement: boolean) => ({ autoIncrement }));

if (isDataType(optionsOrDataType)) {
options = {
type: optionsOrDataType,
};
} else {
options = { ...optionsOrDataType };
}
export const PrimaryKey = createOptionalAttributeOptionsDecorator<boolean>('PrimaryKey', true, (primaryKey: boolean) => ({ primaryKey }));

if (propertyDescriptor) {
if (propertyDescriptor.get) {
options.get = propertyDescriptor.get;
}
export const Comment = createRequiredAttributeOptionsDecorator<string>('Comment', (comment: string) => ({ comment }));

if (propertyDescriptor.set) {
options.set = propertyDescriptor.set;
}
}
export const Default = createRequiredAttributeOptionsDecorator<unknown>('Default', (defaultValue: unknown) => ({ defaultValue }));

registerModelAttributeOptions(target.constructor as ModelStatic, propertyName, options);
}
/**
* Sets the name of the column (in the database) this attribute maps to.
*/
export const ColumnName = createRequiredAttributeOptionsDecorator<string>('ColumnName', (columnName: string) => ({ field: columnName }));
98 changes: 89 additions & 9 deletions src/decorators/legacy/decorator-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,121 @@ export type PropertyOrGetterDescriptor = (
propertyDescriptor?: PropertyDescriptor,
) => void;

export interface ParameterizedPropertyDecorator<T> {
export interface OptionalParameterizedPropertyDecorator<T> {
// @Decorator()
(): PropertyOrGetterDescriptor;
// @Decorator(value)
(options: T): PropertyOrGetterDescriptor;

// @Decorator
(target: Object, propertyName: string | symbol, propertyDescriptor?: PropertyDescriptor): void;
}

export interface RequiredParameterizedPropertyDecorator<T> {
// @Decorator(value)
(options: T): PropertyOrGetterDescriptor;
}

export const DECORATOR_NO_DEFAULT = Symbol('DECORATOR_NO_DEFAULT');

/**
* Creates a decorator that MUST receive a parameter
*
* @param name
* @param callback The callback that will be executed once the decorator is applied.
*/
export function createParameterizedPropertyDecorator<T>(
name: string,
callback: (
option: T,
target: Object,
propertyName: string | symbol,
propertyDescriptor: PropertyDescriptor | undefined,
) => void,
): RequiredParameterizedPropertyDecorator<T> {
return createOptionallyParameterizedPropertyDecorator(name, DECORATOR_NO_DEFAULT, callback);
}

/**
* Makes a decorator that can optionally receive a parameter
* Creates a decorator that can optionally receive a parameter
*
* @param name
* @param defaultValue The value to use if no parameter is provided.
* @param callback The callback that will be executed once the decorator is applied.
*/
export function makeParameterizedPropertyDecorator<T>(
defaultValue: T,
export function createOptionallyParameterizedPropertyDecorator<T>(
name: string,
defaultValue: T | typeof DECORATOR_NO_DEFAULT,
callback: (
option: T,
target: Object,
propertyName: string | symbol,
propertyDescriptor: PropertyDescriptor | undefined,
) => void,
): ParameterizedPropertyDecorator<T> {
return function decorator(...args: [options: T] | Parameters<PropertyOrGetterDescriptor>) {
if (args.length === 1) {
): OptionalParameterizedPropertyDecorator<T> {
return function decorator(...args: [] | [options: T] | Parameters<PropertyOrGetterDescriptor>) {
// note: cannot use <= 1, because TypeScript uses this to infer the type of "args".
evanrittenhouse marked this conversation as resolved.
Show resolved Hide resolved
if (args.length === 0 || args.length === 1) {
evanrittenhouse marked this conversation as resolved.
Show resolved Hide resolved
return function parameterizedDecorator(
target: Object,
propertyName: string | symbol,
propertyDescriptor?: PropertyDescriptor | undefined,
) {
callback(args[0], target, propertyName, propertyDescriptor ?? Object.getOwnPropertyDescriptor(target, propertyName));
const value = args[0] ?? defaultValue;
if (value === DECORATOR_NO_DEFAULT) {
throw new Error(`Decorator @${name} requires an argument (used on ${getPropertyName(target, propertyName)})`);
}

callback(value, target, propertyName, propertyDescriptor ?? Object.getOwnPropertyDescriptor(target, propertyName));
};
}

if (defaultValue === DECORATOR_NO_DEFAULT) {
throw new Error(`Decorator @${name} requires an argument (used on ${getPropertyName(args[0], args[1])})`);
}

callback(defaultValue, args[0], args[1], args[2] ?? Object.getOwnPropertyDescriptor(args[0], args[1]));

// this method only returns something if args.length === 1, but typescript doesn't understand it
return undefined as unknown as PropertyOrGetterDescriptor;
};
}

export function throwMustBeStaticProperty(decoratorName: string, target: Object, propertyName: string | symbol): never {
throw new TypeError(
`Decorator @${decoratorName} has been used on ${getPropertyName(target, propertyName)}, which is an instance property. This decorator can only be used on static properties, setters and getters.`,
);
}

export function throwMustBeModel(decoratorName: string, target: Object, propertyName: string | symbol): never {
throw new TypeError(
`Decorator @${decoratorName} has been used on ${getPropertyName(target, propertyName)}, but class "${getClassName(target)}" does not extend Model. This decorator can only be used on models.`,
);
}

export function throwMustBeInstanceProperty(decoratorName: string, target: Object, propertyName: string | symbol): never {
throw new TypeError(
`Decorator @${decoratorName} has been used on ${getPropertyName(target, propertyName)}, which is static. This decorator can only be used on instance properties, setters and getters.`,
);
}

export function throwMustBeMethod(decoratorName: string, target: Object, propertyName: string | symbol): never {
throw new TypeError(
`Decorator @${decoratorName} has been used on ${getPropertyName(target, propertyName)}, which is not a method. This decorator can only be used on methods.`,
);
}

export function getPropertyName(obj: object, property: string | symbol): string {
if (typeof obj === 'function') {
return `${obj.name}.${String(property)}`;
}

return `${obj.constructor.name}#${String(property)}`;
}

export function getClassName(obj: object): string {
if (typeof obj === 'function') {
return obj.name;
}

return obj.constructor.name;
}