Skip to content

Commit

Permalink
feat: add @AllowNull, @AutoIncrement, @Comment, @Default, `@P…
Browse files Browse the repository at this point in the history
…rimaryKey`, and validation decorators (#15384)
  • Loading branch information
ephys committed Dec 10, 2022
1 parent 98dc4c2 commit 08eb398
Show file tree
Hide file tree
Showing 21 changed files with 1,043 additions and 445 deletions.
93 changes: 93 additions & 0 deletions src/decorators/legacy/attribute-utils.ts
@@ -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(
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
@@ -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
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".
if (args.length === 0 || args.length === 1) {
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;
}

0 comments on commit 08eb398

Please sign in to comment.