mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-29 19:10:19 +03:00
fix: FieldMetadata default value and options better validation (#2785)
* fix: wip better field metadata validation * fix: remove files * fix: default value and options validation * fix: small fix * fix: try to limit patch * fix: tests * Update server/src/metadata/field-metadata/validators/is-field-metadata-options.validator.ts Co-authored-by: Weiko <corentin@twenty.com> * fix: lint * fix: standard fields update security --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
parent
b09100e3f3
commit
93decaceab
21
.vscode/launch.json
vendored
Normal file
21
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Server Debug",
|
||||||
|
"runtimeExecutable": "yarn",
|
||||||
|
"cwd": "${workspaceFolder}/server",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"start:debug",
|
||||||
|
"--",
|
||||||
|
"--inspect-brk"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"restart": true,
|
||||||
|
"protocol": "auto",
|
||||||
|
"port": 9229,
|
||||||
|
"autoAttachChildProcesses": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
5601
server/patches/class-validator+0.14.0.patch
Normal file
5601
server/patches/class-validator+0.14.0.patch
Normal file
File diff suppressed because one or more lines are too long
@ -1,17 +1,54 @@
|
|||||||
import { Catch, UnauthorizedException } from '@nestjs/common';
|
import { ArgumentsHost, Catch, HttpException } from '@nestjs/common';
|
||||||
import { GqlExceptionFilter } from '@nestjs/graphql';
|
import { GqlContextType, GqlExceptionFilter } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { GraphQLError } from 'graphql';
|
import { TypeORMError } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthenticationError,
|
||||||
|
BaseGraphQLError,
|
||||||
|
ForbiddenError,
|
||||||
|
} from 'src/filters/utils/graphql-errors.util';
|
||||||
|
|
||||||
|
const graphQLPredefinedExceptions = {
|
||||||
|
401: AuthenticationError,
|
||||||
|
403: ForbiddenError,
|
||||||
|
};
|
||||||
|
|
||||||
@Catch()
|
@Catch()
|
||||||
export class ExceptionFilter implements GqlExceptionFilter {
|
export class ExceptionFilter implements GqlExceptionFilter {
|
||||||
catch(exception: Error) {
|
catch(exception: HttpException | TypeORMError, host: ArgumentsHost) {
|
||||||
if (exception instanceof UnauthorizedException) {
|
if (host.getType<GqlContextType>() !== 'graphql') {
|
||||||
throw new GraphQLError('Unauthorized', {
|
return null;
|
||||||
extensions: {
|
}
|
||||||
code: 'UNAUTHENTICATED',
|
|
||||||
},
|
if (exception instanceof TypeORMError) {
|
||||||
});
|
const error = new BaseGraphQLError(
|
||||||
|
exception.name,
|
||||||
|
'INTERNAL_SERVER_ERROR',
|
||||||
|
);
|
||||||
|
|
||||||
|
error.stack = exception.stack;
|
||||||
|
error.extensions['response'] = exception.message;
|
||||||
|
|
||||||
|
return error;
|
||||||
|
} else if (exception instanceof HttpException) {
|
||||||
|
let error: BaseGraphQLError;
|
||||||
|
|
||||||
|
if (exception.getStatus() in graphQLPredefinedExceptions) {
|
||||||
|
error = new graphQLPredefinedExceptions[exception.getStatus()](
|
||||||
|
exception.message,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
error = new BaseGraphQLError(
|
||||||
|
exception.message,
|
||||||
|
exception.getStatus().toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.stack = exception.stack;
|
||||||
|
error.extensions['response'] = exception.getResponse();
|
||||||
|
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return exception;
|
return exception;
|
||||||
|
135
server/src/filters/utils/graphql-errors.util.ts
Normal file
135
server/src/filters/utils/graphql-errors.util.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
ASTNode,
|
||||||
|
GraphQLError,
|
||||||
|
GraphQLFormattedError,
|
||||||
|
Source,
|
||||||
|
SourceLocation,
|
||||||
|
} from 'graphql';
|
||||||
|
|
||||||
|
declare module 'graphql' {
|
||||||
|
export interface GraphQLErrorExtensions {
|
||||||
|
exception?: {
|
||||||
|
code?: string;
|
||||||
|
stacktrace?: ReadonlyArray<string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseGraphQLError extends Error implements GraphQLError {
|
||||||
|
public extensions: Record<string, any>;
|
||||||
|
override readonly name!: string;
|
||||||
|
readonly locations: ReadonlyArray<SourceLocation> | undefined;
|
||||||
|
readonly path: ReadonlyArray<string | number> | undefined;
|
||||||
|
readonly source: Source | undefined;
|
||||||
|
readonly positions: ReadonlyArray<number> | undefined;
|
||||||
|
readonly nodes: ReadonlyArray<ASTNode> | undefined;
|
||||||
|
public originalError: Error | undefined;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
code?: string,
|
||||||
|
extensions?: Record<string, any>,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
|
||||||
|
// if no name provided, use the default. defineProperty ensures that it stays non-enumerable
|
||||||
|
if (!this.name) {
|
||||||
|
Object.defineProperty(this, 'name', { value: 'ApolloError' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensions?.extensions) {
|
||||||
|
throw Error(
|
||||||
|
'Pass extensions directly as the third argument of the ApolloError constructor: `new ' +
|
||||||
|
'ApolloError(message, code, {myExt: value})`, not `new ApolloError(message, code, ' +
|
||||||
|
'{extensions: {myExt: value}})`',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extensions = { ...extensions, code };
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): GraphQLFormattedError {
|
||||||
|
return toGraphQLError(this).toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
override toString(): string {
|
||||||
|
return toGraphQLError(this).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag](): string {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toGraphQLError(error: BaseGraphQLError): GraphQLError {
|
||||||
|
return new GraphQLError(error.message, {
|
||||||
|
nodes: error.nodes,
|
||||||
|
source: error.source,
|
||||||
|
positions: error.positions,
|
||||||
|
path: error.path,
|
||||||
|
originalError: error.originalError,
|
||||||
|
extensions: error.extensions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SyntaxError extends BaseGraphQLError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, 'GRAPHQL_PARSE_FAILED');
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', { value: 'SyntaxError' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends BaseGraphQLError {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message, 'GRAPHQL_VALIDATION_FAILED');
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', { value: 'ValidationError' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthenticationError extends BaseGraphQLError {
|
||||||
|
constructor(message: string, extensions?: Record<string, any>) {
|
||||||
|
super(message, 'UNAUTHENTICATED', extensions);
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', { value: 'AuthenticationError' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForbiddenError extends BaseGraphQLError {
|
||||||
|
constructor(message: string, extensions?: Record<string, any>) {
|
||||||
|
super(message, 'FORBIDDEN', extensions);
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', { value: 'ForbiddenError' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PersistedQueryNotFoundError extends BaseGraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('PersistedQueryNotFound', 'PERSISTED_QUERY_NOT_FOUND');
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', {
|
||||||
|
value: 'PersistedQueryNotFoundError',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PersistedQueryNotSupportedError extends BaseGraphQLError {
|
||||||
|
constructor() {
|
||||||
|
super('PersistedQueryNotSupported', 'PERSISTED_QUERY_NOT_SUPPORTED');
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', {
|
||||||
|
value: 'PersistedQueryNotSupportedError',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserInputError extends BaseGraphQLError {
|
||||||
|
constructor(message: string, extensions?: Record<string, any>) {
|
||||||
|
super(message, 'BAD_USER_INPUT', extensions);
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'name', { value: 'UserInputError' });
|
||||||
|
}
|
||||||
|
}
|
@ -76,7 +76,6 @@ export class S3Driver implements StorageDriver {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
|
||||||
if (error instanceof NotFound) {
|
if (error instanceof NotFound) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { ValidationPipe } from '@nestjs/common';
|
|||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import { graphqlUploadExpress } from 'graphql-upload';
|
import { graphqlUploadExpress } from 'graphql-upload';
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
|
import { useContainer } from 'class-validator';
|
||||||
|
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
@ -19,8 +20,16 @@ const bootstrap = async () => {
|
|||||||
: ['error', 'warn', 'log'],
|
: ['error', 'warn', 'log'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply class-validator container so that we can use injection in validators
|
||||||
|
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
||||||
|
|
||||||
// Apply validation pipes globally
|
// Apply validation pipes globally
|
||||||
app.useGlobalPipes(new ValidationPipe());
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
// whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
app.use(bodyParser.json({ limit: settings.storage.maxFileSize }));
|
app.use(bodyParser.json({ limit: settings.storage.maxFileSize }));
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -1,80 +1,27 @@
|
|||||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
import { Field, InputType, OmitType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { BeforeCreateOne } from '@ptc-org/nestjs-query-graphql';
|
import { IsUUID, ValidateNested } from 'class-validator';
|
||||||
import {
|
|
||||||
IsArray,
|
|
||||||
IsBoolean,
|
|
||||||
IsEnum,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
IsUUID,
|
|
||||||
ValidateNested,
|
|
||||||
} from 'class-validator';
|
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
|
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
|
||||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
|
||||||
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
|
|
||||||
|
|
||||||
import { BeforeCreateOneField } from 'src/metadata/field-metadata/hooks/before-create-one-field.hook';
|
|
||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
|
||||||
import { IsDefaultValue } from 'src/metadata/field-metadata/validators/is-default-value.validator';
|
|
||||||
import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input';
|
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@BeforeCreateOne(BeforeCreateOneField)
|
export class CreateFieldInput extends OmitType(
|
||||||
export class CreateFieldInput {
|
FieldMetadataDTO,
|
||||||
@IsString()
|
['id', 'createdAt', 'updatedAt'] as const,
|
||||||
@IsNotEmpty()
|
InputType,
|
||||||
@Field()
|
) {
|
||||||
name: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@Field()
|
|
||||||
label: string;
|
|
||||||
|
|
||||||
@IsEnum(FieldMetadataType)
|
|
||||||
@IsNotEmpty()
|
|
||||||
@Field(() => FieldMetadataType)
|
|
||||||
type: FieldMetadataType;
|
|
||||||
|
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
@Field()
|
@Field()
|
||||||
objectMetadataId: string;
|
objectMetadataId: string;
|
||||||
|
}
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
@InputType()
|
||||||
@Field({ nullable: true })
|
export class CreateOneFieldMetadataInput {
|
||||||
description?: string;
|
@Type(() => CreateFieldInput)
|
||||||
|
@ValidateNested()
|
||||||
@IsString()
|
@Field(() => CreateFieldInput, {
|
||||||
@IsOptional()
|
description: 'The record to create',
|
||||||
@Field({ nullable: true })
|
})
|
||||||
icon?: string;
|
field!: CreateFieldInput;
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
isNullable?: boolean;
|
|
||||||
|
|
||||||
@IsDefaultValue({ message: 'Invalid default value for the specified type' })
|
|
||||||
@IsOptional()
|
|
||||||
@Field(() => GraphQLJSON, { nullable: true })
|
|
||||||
defaultValue?: FieldMetadataDefaultValue;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@Type(() => FieldMetadataComplexOptions)
|
|
||||||
@Field(() => GraphQLJSON, { nullable: true })
|
|
||||||
options?: FieldMetadataOptions;
|
|
||||||
|
|
||||||
@HideField()
|
|
||||||
targetColumnMap: FieldMetadataTargetColumnMap;
|
|
||||||
|
|
||||||
@HideField()
|
|
||||||
workspaceId: string;
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsDate,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
IsString,
|
||||||
|
Matches,
|
||||||
|
ValidateIf,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueString {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
value: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueNumber {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsNumber()
|
||||||
|
value: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueBoolean {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsBoolean()
|
||||||
|
value: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueStringArray {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
value: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueDateTime {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsDate()
|
||||||
|
value: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueLink {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
label: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueCurrency {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsNumber()
|
||||||
|
amountMicros: number | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
currencyCode: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDefaultValueFullName {
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
firstName: string | null;
|
||||||
|
|
||||||
|
@ValidateIf((_object, value) => value !== null)
|
||||||
|
@IsString()
|
||||||
|
lastName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDynamicDefaultValueUuid {
|
||||||
|
@Matches('uuid')
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
type: 'uuid';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FieldMetadataDynamicDefaultValueNow {
|
||||||
|
@Matches('now')
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
type: 'now';
|
||||||
|
}
|
@ -15,6 +15,16 @@ import {
|
|||||||
QueryOptions,
|
QueryOptions,
|
||||||
Relation,
|
Relation,
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
} from '@ptc-org/nestjs-query-graphql';
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
IsEnum,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUUID,
|
||||||
|
Validate,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
|
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
|
||||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||||
@ -22,6 +32,8 @@ import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interface
|
|||||||
import { RelationMetadataDTO } from 'src/metadata/relation-metadata/dtos/relation-metadata.dto';
|
import { RelationMetadataDTO } from 'src/metadata/relation-metadata/dtos/relation-metadata.dto';
|
||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
import { BeforeDeleteOneField } from 'src/metadata/field-metadata/hooks/before-delete-one-field.hook';
|
import { BeforeDeleteOneField } from 'src/metadata/field-metadata/hooks/before-delete-one-field.hook';
|
||||||
|
import { IsFieldMetadataDefaultValue } from 'src/metadata/field-metadata/validators/is-field-metadata-default-value.validator';
|
||||||
|
import { IsFieldMetadataOptions } from 'src/metadata/field-metadata/validators/is-field-metadata-options.validator';
|
||||||
|
|
||||||
registerEnumType(FieldMetadataType, {
|
registerEnumType(FieldMetadataType, {
|
||||||
name: 'FieldMetadataType',
|
name: 'FieldMetadataType',
|
||||||
@ -46,52 +58,77 @@ registerEnumType(FieldMetadataType, {
|
|||||||
@Relation('fromRelationMetadata', () => RelationMetadataDTO, {
|
@Relation('fromRelationMetadata', () => RelationMetadataDTO, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
export class FieldMetadataDTO {
|
export class FieldMetadataDTO<
|
||||||
|
T extends FieldMetadataType | 'default' = 'default',
|
||||||
|
> {
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
@IDField(() => ID)
|
@IDField(() => ID)
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@IsEnum(FieldMetadataType)
|
||||||
|
@IsNotEmpty()
|
||||||
@Field(() => FieldMetadataType)
|
@Field(() => FieldMetadataType)
|
||||||
type: FieldMetadataType;
|
type: FieldMetadataType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
@Field()
|
@Field()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
@Field()
|
@Field()
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
description: string;
|
description?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
icon: string;
|
icon?: string;
|
||||||
|
|
||||||
@Field({ nullable: true, deprecationReason: 'Use label name instead' })
|
@IsBoolean()
|
||||||
placeholder?: string;
|
@IsOptional()
|
||||||
|
@FilterableField({ nullable: true })
|
||||||
|
isCustom?: boolean;
|
||||||
|
|
||||||
@FilterableField()
|
@IsBoolean()
|
||||||
isCustom: boolean;
|
@IsOptional()
|
||||||
|
@FilterableField({ nullable: true })
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
@FilterableField()
|
@IsBoolean()
|
||||||
isActive: boolean;
|
@IsOptional()
|
||||||
|
@FilterableField({ nullable: true })
|
||||||
|
isSystem?: boolean;
|
||||||
|
|
||||||
@FilterableField()
|
@IsBoolean()
|
||||||
isSystem: boolean;
|
@IsOptional()
|
||||||
|
@Field({ nullable: true })
|
||||||
@Field()
|
isNullable?: boolean;
|
||||||
isNullable: boolean;
|
|
||||||
|
|
||||||
|
@Validate(IsFieldMetadataDefaultValue)
|
||||||
|
@IsOptional()
|
||||||
@Field(() => GraphQLJSON, { nullable: true })
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
defaultValue?: FieldMetadataDefaultValue;
|
defaultValue?: FieldMetadataDefaultValue<T>;
|
||||||
|
|
||||||
|
@Validate(IsFieldMetadataOptions)
|
||||||
|
@IsOptional()
|
||||||
@Field(() => GraphQLJSON, { nullable: true })
|
@Field(() => GraphQLJSON, { nullable: true })
|
||||||
options?: FieldMetadataOptions;
|
options?: FieldMetadataOptions<T>;
|
||||||
|
|
||||||
@HideField()
|
@HideField()
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
@Field()
|
@Field()
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
@Field()
|
@Field()
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IsString, IsNumber, IsOptional } from 'class-validator';
|
import { IsString, IsNumber, IsOptional, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
export class FieldMetadataDefaultOptions {
|
export class FieldMetadataDefaultOptions {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ -8,15 +8,17 @@ export class FieldMetadataDefaultOptions {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
position: number;
|
position: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FieldMetadataComplexOptions extends FieldMetadataDefaultOptions {
|
export class FieldMetadataComplexOptions extends FieldMetadataDefaultOptions {
|
||||||
@IsOptional()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
@ -1,62 +1,40 @@
|
|||||||
import { Field, HideField, InputType } from '@nestjs/graphql';
|
|
||||||
|
|
||||||
import { BeforeUpdateOne } from '@ptc-org/nestjs-query-graphql';
|
|
||||||
import {
|
import {
|
||||||
IsArray,
|
Field,
|
||||||
IsBoolean,
|
HideField,
|
||||||
IsOptional,
|
ID,
|
||||||
IsString,
|
InputType,
|
||||||
ValidateNested,
|
OmitType,
|
||||||
} from 'class-validator';
|
PartialType,
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
} from '@nestjs/graphql';
|
||||||
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsNotEmpty, IsUUID, ValidateNested } from 'class-validator';
|
||||||
|
|
||||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
|
||||||
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
|
|
||||||
|
|
||||||
import { BeforeUpdateOneField } from 'src/metadata/field-metadata/hooks/before-update-one-field.hook';
|
|
||||||
import { FieldMetadataComplexOptions } from 'src/metadata/field-metadata/dtos/options.input';
|
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@BeforeUpdateOne(BeforeUpdateOneField)
|
export class UpdateFieldInput extends OmitType(
|
||||||
export class UpdateFieldInput {
|
PartialType(FieldMetadataDTO, InputType),
|
||||||
@IsString()
|
['id', 'type', 'createdAt', 'updatedAt'] as const,
|
||||||
@IsOptional()
|
) {
|
||||||
@Field({ nullable: true })
|
@HideField()
|
||||||
label?: string;
|
id: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
name?: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
icon?: string;
|
|
||||||
|
|
||||||
@IsBoolean()
|
|
||||||
@IsOptional()
|
|
||||||
@Field({ nullable: true })
|
|
||||||
isActive?: boolean;
|
|
||||||
|
|
||||||
// TODO: Add validation for this but we don't have the type actually
|
|
||||||
@IsOptional()
|
|
||||||
@Field(() => GraphQLJSON, { nullable: true })
|
|
||||||
defaultValue?: FieldMetadataDefaultValue;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@ValidateNested({ each: true })
|
|
||||||
@Type(() => FieldMetadataComplexOptions)
|
|
||||||
@Field(() => GraphQLJSON, { nullable: true })
|
|
||||||
options?: FieldMetadataOptions;
|
|
||||||
|
|
||||||
@HideField()
|
@HideField()
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class UpdateOneFieldMetadataInput {
|
||||||
|
@IsUUID()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field(() => ID, { description: 'The id of the record to update' })
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Type(() => UpdateFieldInput)
|
||||||
|
@ValidateNested()
|
||||||
|
@Field(() => UpdateFieldInput, {
|
||||||
|
description: 'The record to update',
|
||||||
|
})
|
||||||
|
update!: UpdateFieldInput;
|
||||||
|
}
|
||||||
|
@ -13,12 +13,14 @@ import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metada
|
|||||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
|
import { IsFieldMetadataDefaultValue } from 'src/metadata/field-metadata/validators/is-field-metadata-default-value.validator';
|
||||||
|
import { FieldMetadataResolver } from 'src/metadata/field-metadata/field-metadata.resolver';
|
||||||
|
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
|
||||||
|
|
||||||
import { FieldMetadataService } from './field-metadata.service';
|
import { FieldMetadataService } from './field-metadata.service';
|
||||||
import { FieldMetadataEntity } from './field-metadata.entity';
|
import { FieldMetadataEntity } from './field-metadata.entity';
|
||||||
|
|
||||||
import { CreateFieldInput } from './dtos/create-field.input';
|
import { CreateFieldInput } from './dtos/create-field.input';
|
||||||
import { FieldMetadataDTO } from './dtos/field-metadata.dto';
|
|
||||||
import { UpdateFieldInput } from './dtos/update-field.input';
|
import { UpdateFieldInput } from './dtos/update-field.input';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -32,7 +34,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
|||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
],
|
],
|
||||||
services: [FieldMetadataService],
|
services: [IsFieldMetadataDefaultValue, FieldMetadataService],
|
||||||
resolvers: [
|
resolvers: [
|
||||||
{
|
{
|
||||||
EntityClass: FieldMetadataEntity,
|
EntityClass: FieldMetadataEntity,
|
||||||
@ -46,9 +48,13 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
|||||||
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
defaultSort: [{ field: 'id', direction: SortDirection.DESC }],
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
|
// Manually created because of the async validation
|
||||||
|
one: { disabled: true },
|
||||||
many: { disabled: true },
|
many: { disabled: true },
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
|
// Manually created because of the async validation
|
||||||
|
one: { disabled: true },
|
||||||
many: { disabled: true },
|
many: { disabled: true },
|
||||||
},
|
},
|
||||||
delete: { many: { disabled: true } },
|
delete: { many: { disabled: true } },
|
||||||
@ -57,7 +63,11 @@ import { UpdateFieldInput } from './dtos/update-field.input';
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [FieldMetadataService],
|
providers: [
|
||||||
|
IsFieldMetadataDefaultValue,
|
||||||
|
FieldMetadataService,
|
||||||
|
FieldMetadataResolver,
|
||||||
|
],
|
||||||
exports: [FieldMetadataService],
|
exports: [FieldMetadataService],
|
||||||
})
|
})
|
||||||
export class FieldMetadataModule {}
|
export class FieldMetadataModule {}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { Workspace } from 'src/core/workspace/workspace.entity';
|
||||||
|
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||||
|
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||||
|
import { CreateOneFieldMetadataInput } from 'src/metadata/field-metadata/dtos/create-field.input';
|
||||||
|
import { FieldMetadataDTO } from 'src/metadata/field-metadata/dtos/field-metadata.dto';
|
||||||
|
import { UpdateOneFieldMetadataInput } from 'src/metadata/field-metadata/dtos/update-field.input';
|
||||||
|
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Resolver(() => FieldMetadataDTO)
|
||||||
|
export class FieldMetadataResolver {
|
||||||
|
constructor(private readonly fieldMetadataService: FieldMetadataService) {}
|
||||||
|
|
||||||
|
@Mutation(() => FieldMetadataDTO)
|
||||||
|
createOneField(
|
||||||
|
@Args('input') input: CreateOneFieldMetadataInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.fieldMetadataService.createOne({
|
||||||
|
...input.field,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => FieldMetadataDTO)
|
||||||
|
updateOneField(
|
||||||
|
@Args('input') input: UpdateOneFieldMetadataInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.fieldMetadataService.updateOne(input.id, {
|
||||||
|
...input.update,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -159,6 +159,15 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
throw new NotFoundException('Field does not exist');
|
throw new NotFoundException('Field does not exist');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingFieldMetadata.isCustom === false) {
|
||||||
|
// We can only update the isActive field for standard fields
|
||||||
|
record = {
|
||||||
|
id: record.id,
|
||||||
|
isActive: record.isActive,
|
||||||
|
workspaceId: record.workspaceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const objectMetadata =
|
const objectMetadata =
|
||||||
await this.objectMetadataService.findOneWithinWorkspace(
|
await this.objectMetadataService.findOneWithinWorkspace(
|
||||||
record.workspaceId,
|
record.workspaceId,
|
||||||
@ -208,6 +217,25 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
return updatedFieldMetadata;
|
return updatedFieldMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async findOneOrFail(
|
||||||
|
id: string,
|
||||||
|
options?: FindOneOptions<FieldMetadataEntity>,
|
||||||
|
) {
|
||||||
|
const fieldMetadata = await this.fieldMetadataRepository.findOne({
|
||||||
|
...options,
|
||||||
|
where: {
|
||||||
|
...options?.where,
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fieldMetadata) {
|
||||||
|
throw new NotFoundException('Field does not exist');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
public async findOneWithinWorkspace(
|
public async findOneWithinWorkspace(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
options: FindOneOptions<FieldMetadataEntity>,
|
options: FindOneOptions<FieldMetadataEntity>,
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BeforeCreateOneHook,
|
|
||||||
CreateOneInputType,
|
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
|
||||||
|
|
||||||
import { CreateFieldInput } from 'src/metadata/field-metadata/dtos/create-field.input';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BeforeCreateOneField<T extends CreateFieldInput>
|
|
||||||
implements BeforeCreateOneHook<T, any>
|
|
||||||
{
|
|
||||||
async run(
|
|
||||||
instance: CreateOneInputType<T>,
|
|
||||||
context: any,
|
|
||||||
): Promise<CreateOneInputType<T>> {
|
|
||||||
const workspaceId = context?.req?.user?.workspace?.id;
|
|
||||||
|
|
||||||
if (!workspaceId) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.input.workspaceId = workspaceId;
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
BeforeUpdateOneHook,
|
|
||||||
UpdateOneInputType,
|
|
||||||
} from '@ptc-org/nestjs-query-graphql';
|
|
||||||
|
|
||||||
import { UpdateFieldInput } from 'src/metadata/field-metadata/dtos/update-field.input';
|
|
||||||
import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata.entity';
|
|
||||||
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BeforeUpdateOneField<T extends UpdateFieldInput>
|
|
||||||
implements BeforeUpdateOneHook<T, any>
|
|
||||||
{
|
|
||||||
constructor(readonly fieldMetadataService: FieldMetadataService) {}
|
|
||||||
|
|
||||||
// TODO: this logic could be moved to a policy guard
|
|
||||||
async run(
|
|
||||||
instance: UpdateOneInputType<T>,
|
|
||||||
context: any,
|
|
||||||
): Promise<UpdateOneInputType<T>> {
|
|
||||||
const workspaceId = context?.req?.user?.workspace?.id;
|
|
||||||
|
|
||||||
if (!workspaceId) {
|
|
||||||
throw new UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldMetadata =
|
|
||||||
await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
|
|
||||||
where: {
|
|
||||||
id: instance.id.toString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fieldMetadata) {
|
|
||||||
throw new BadRequestException('Field does not exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fieldMetadata.isCustom) {
|
|
||||||
if (
|
|
||||||
Object.keys(instance.update).length === 1 &&
|
|
||||||
instance.update.hasOwnProperty('isActive') &&
|
|
||||||
instance.update.isActive !== undefined
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
id: instance.id,
|
|
||||||
update: {
|
|
||||||
isActive: instance.update.isActive,
|
|
||||||
} as T,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new BadRequestException(
|
|
||||||
'Only isActive field can be updated for standard fields',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.checkIfFieldIsEditable(instance.update, fieldMetadata);
|
|
||||||
|
|
||||||
instance.update.workspaceId = workspaceId;
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is temporary until we properly use the MigrationRunner to update column names
|
|
||||||
private checkIfFieldIsEditable(
|
|
||||||
update: UpdateFieldInput,
|
|
||||||
fieldMetadata: FieldMetadataEntity,
|
|
||||||
) {
|
|
||||||
if (update.name && update.name !== fieldMetadata.name) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Field's name can't be updated. Please create a new field instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update.label && update.label !== fieldMetadata.label) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Field's label can't be updated. Please create a new field instead",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +1,17 @@
|
|||||||
|
import {
|
||||||
|
FieldMetadataDefaultValueBoolean,
|
||||||
|
FieldMetadataDefaultValueCurrency,
|
||||||
|
FieldMetadataDefaultValueDateTime,
|
||||||
|
FieldMetadataDefaultValueFullName,
|
||||||
|
FieldMetadataDefaultValueLink,
|
||||||
|
FieldMetadataDefaultValueNumber,
|
||||||
|
FieldMetadataDefaultValueString,
|
||||||
|
FieldMetadataDefaultValueStringArray,
|
||||||
|
FieldMetadataDynamicDefaultValueNow,
|
||||||
|
FieldMetadataDynamicDefaultValueUuid,
|
||||||
|
} from 'src/metadata/field-metadata/dtos/default-value.input';
|
||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
export interface FieldMetadataDefaultValueString {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
export interface FieldMetadataDefaultValueNumber {
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
export interface FieldMetadataDefaultValueBoolean {
|
|
||||||
value: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FieldMetadataDefaultValueStringArray {
|
|
||||||
value: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FieldMetadataDefaultValueDateTime {
|
|
||||||
value: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldMetadataScalarDefaultValue =
|
type FieldMetadataScalarDefaultValue =
|
||||||
| FieldMetadataDefaultValueString
|
| FieldMetadataDefaultValueString
|
||||||
| FieldMetadataDefaultValueNumber
|
| FieldMetadataDefaultValueNumber
|
||||||
@ -25,23 +19,8 @@ type FieldMetadataScalarDefaultValue =
|
|||||||
| FieldMetadataDefaultValueDateTime;
|
| FieldMetadataDefaultValueDateTime;
|
||||||
|
|
||||||
export type FieldMetadataDynamicDefaultValue =
|
export type FieldMetadataDynamicDefaultValue =
|
||||||
| { type: 'uuid' }
|
| FieldMetadataDynamicDefaultValueUuid
|
||||||
| { type: 'now' };
|
| FieldMetadataDynamicDefaultValueNow;
|
||||||
|
|
||||||
interface FieldMetadataDefaultValueLink {
|
|
||||||
label: string;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FieldMetadataDefaultValueCurrency {
|
|
||||||
amountMicros: number;
|
|
||||||
currencyCode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FieldMetadataDefaultValueFullName {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AllFieldMetadataDefaultValueTypes =
|
type AllFieldMetadataDefaultValueTypes =
|
||||||
| FieldMetadataScalarDefaultValue
|
| FieldMetadataScalarDefaultValue
|
||||||
@ -51,11 +30,15 @@ type AllFieldMetadataDefaultValueTypes =
|
|||||||
| FieldMetadataDefaultValueFullName;
|
| FieldMetadataDefaultValueFullName;
|
||||||
|
|
||||||
type FieldMetadataDefaultValueMapping = {
|
type FieldMetadataDefaultValueMapping = {
|
||||||
[FieldMetadataType.UUID]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.UUID]:
|
||||||
|
| FieldMetadataDefaultValueString
|
||||||
|
| FieldMetadataDynamicDefaultValueUuid;
|
||||||
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.TEXT]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.PHONE]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.EMAIL]: FieldMetadataDefaultValueString;
|
||||||
[FieldMetadataType.DATE_TIME]: FieldMetadataDefaultValueDateTime;
|
[FieldMetadataType.DATE_TIME]:
|
||||||
|
| FieldMetadataDefaultValueDateTime
|
||||||
|
| FieldMetadataDynamicDefaultValueNow;
|
||||||
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
|
[FieldMetadataType.BOOLEAN]: FieldMetadataDefaultValueBoolean;
|
||||||
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
|
[FieldMetadataType.NUMBER]: FieldMetadataDefaultValueNumber;
|
||||||
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
|
[FieldMetadataType.NUMERIC]: FieldMetadataDefaultValueString;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
import { validateDefaultValueBasedOnType } from 'src/metadata/field-metadata/utils/validate-default-value-based-on-type.util';
|
import { validateDefaultValueForType } from 'src/metadata/field-metadata/utils/validate-default-value-for-type.util';
|
||||||
|
|
||||||
describe('validateDefaultValueBasedOnType', () => {
|
describe('validateDefaultValueForType', () => {
|
||||||
it('should return true for null defaultValue', () => {
|
it('should return true for null defaultValue', () => {
|
||||||
expect(validateDefaultValueBasedOnType(null, FieldMetadataType.TEXT)).toBe(
|
expect(validateDefaultValueForType(FieldMetadataType.TEXT, null)).toBe(
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -11,135 +11,116 @@ describe('validateDefaultValueBasedOnType', () => {
|
|||||||
// Dynamic default values
|
// Dynamic default values
|
||||||
it('should validate uuid dynamic default value for UUID type', () => {
|
it('should validate uuid dynamic default value for UUID type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType({ type: 'uuid' }, FieldMetadataType.UUID),
|
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'uuid' }),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate now dynamic default value for DATE_TIME type', () => {
|
it('should validate now dynamic default value for DATE_TIME type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.DATE_TIME, { type: 'now' }),
|
||||||
{ type: 'now' },
|
|
||||||
FieldMetadataType.DATE_TIME,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for mismatched dynamic default value', () => {
|
it('should return false for mismatched dynamic default value', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType({ type: 'now' }, FieldMetadataType.UUID),
|
validateDefaultValueForType(FieldMetadataType.UUID, { type: 'now' }),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Static default values
|
// Static default values
|
||||||
it('should validate string default value for TEXT type', () => {
|
it('should validate string default value for TEXT type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 'test' }),
|
||||||
{ value: 'test' },
|
|
||||||
FieldMetadataType.TEXT,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid string default value for TEXT type', () => {
|
it('should return false for invalid string default value for TEXT type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType({ value: 123 }, FieldMetadataType.TEXT),
|
validateDefaultValueForType(FieldMetadataType.TEXT, { value: 123 }),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate string default value for PHONE type', () => {
|
it('should validate string default value for PHONE type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.PHONE, {
|
||||||
{ value: '+123456789' },
|
value: '+123456789',
|
||||||
FieldMetadataType.PHONE,
|
}),
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid string default value for PHONE type', () => {
|
it('should return false for invalid string default value for PHONE type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType({ value: 123 }, FieldMetadataType.PHONE),
|
validateDefaultValueForType(FieldMetadataType.PHONE, { value: 123 }),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate string default value for EMAIL type', () => {
|
it('should validate string default value for EMAIL type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.EMAIL, {
|
||||||
{ value: 'test@example.com' },
|
value: 'test@example.com',
|
||||||
FieldMetadataType.EMAIL,
|
}),
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid string default value for EMAIL type', () => {
|
it('should return false for invalid string default value for EMAIL type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType({ value: 123 }, FieldMetadataType.EMAIL),
|
validateDefaultValueForType(FieldMetadataType.EMAIL, { value: 123 }),
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate number default value for NUMBER type', () => {
|
it('should validate number default value for NUMBER type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType({ value: 100 }, FieldMetadataType.NUMBER),
|
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: 100 }),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid number default value for NUMBER type', () => {
|
it('should return false for invalid number default value for NUMBER type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.NUMBER, { value: '100' }),
|
||||||
{ value: '100' },
|
|
||||||
FieldMetadataType.NUMBER,
|
|
||||||
),
|
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate number default value for PROBABILITY type', () => {
|
it('should validate number default value for PROBABILITY type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.PROBABILITY, {
|
||||||
{ value: 0.5 },
|
value: 0.5,
|
||||||
FieldMetadataType.PROBABILITY,
|
}),
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid number default value for PROBABILITY type', () => {
|
it('should return false for invalid number default value for PROBABILITY type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.PROBABILITY, {
|
||||||
{ value: '50%' },
|
value: '50%',
|
||||||
FieldMetadataType.PROBABILITY,
|
}),
|
||||||
),
|
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate boolean default value for BOOLEAN type', () => {
|
it('should validate boolean default value for BOOLEAN type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: true }),
|
||||||
{ value: true },
|
|
||||||
FieldMetadataType.BOOLEAN,
|
|
||||||
),
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid boolean default value for BOOLEAN type', () => {
|
it('should return false for invalid boolean default value for BOOLEAN type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.BOOLEAN, { value: 'true' }),
|
||||||
{ value: 'true' },
|
|
||||||
FieldMetadataType.BOOLEAN,
|
|
||||||
),
|
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// LINK type
|
// LINK type
|
||||||
it('should validate LINK default value', () => {
|
it('should validate LINK default value', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.LINK, {
|
||||||
{ label: 'http://example.com', url: 'Example' },
|
label: 'http://example.com',
|
||||||
FieldMetadataType.LINK,
|
url: 'Example',
|
||||||
),
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid LINK default value', () => {
|
it('should return false for invalid LINK default value', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error Just for testing purposes
|
// @ts-expect-error Just for testing purposes
|
||||||
{ label: 123, url: {} },
|
{ label: 123, url: {} },
|
||||||
@ -151,16 +132,16 @@ describe('validateDefaultValueBasedOnType', () => {
|
|||||||
// CURRENCY type
|
// CURRENCY type
|
||||||
it('should validate CURRENCY default value', () => {
|
it('should validate CURRENCY default value', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(FieldMetadataType.CURRENCY, {
|
||||||
{ amountMicros: 100, currencyCode: 'USD' },
|
amountMicros: 100,
|
||||||
FieldMetadataType.CURRENCY,
|
currencyCode: 'USD',
|
||||||
),
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid CURRENCY default value', () => {
|
it('should return false for invalid CURRENCY default value', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error Just for testing purposes
|
// @ts-expect-error Just for testing purposes
|
||||||
{ amountMicros: '100', currencyCode: 'USD' },
|
{ amountMicros: '100', currencyCode: 'USD' },
|
||||||
@ -172,10 +153,9 @@ describe('validateDefaultValueBasedOnType', () => {
|
|||||||
// Unknown type
|
// Unknown type
|
||||||
it('should return false for unknown type', () => {
|
it('should return false for unknown type', () => {
|
||||||
expect(
|
expect(
|
||||||
validateDefaultValueBasedOnType(
|
validateDefaultValueForType('unknown' as FieldMetadataType, {
|
||||||
{ value: 'test' },
|
value: 'test',
|
||||||
'unknown' as FieldMetadataType,
|
}),
|
||||||
),
|
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,95 +0,0 @@
|
|||||||
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
|
||||||
|
|
||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
|
||||||
|
|
||||||
export const validateDefaultValueBasedOnType = (
|
|
||||||
defaultValue: FieldMetadataDefaultValue,
|
|
||||||
type: FieldMetadataType,
|
|
||||||
): boolean => {
|
|
||||||
if (defaultValue === null) return true;
|
|
||||||
|
|
||||||
// Dynamic default values
|
|
||||||
if (typeof defaultValue === 'object' && 'type' in defaultValue) {
|
|
||||||
if (type === FieldMetadataType.UUID && defaultValue.type === 'uuid') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (type === FieldMetadataType.DATE_TIME && defaultValue.type === 'now') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static default values
|
|
||||||
switch (type) {
|
|
||||||
case FieldMetadataType.TEXT:
|
|
||||||
case FieldMetadataType.PHONE:
|
|
||||||
case FieldMetadataType.EMAIL:
|
|
||||||
case FieldMetadataType.RATING:
|
|
||||||
case FieldMetadataType.SELECT:
|
|
||||||
case FieldMetadataType.NUMERIC:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'value' in defaultValue &&
|
|
||||||
typeof defaultValue.value === 'string'
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.NUMBER:
|
|
||||||
case FieldMetadataType.PROBABILITY:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'value' in defaultValue &&
|
|
||||||
typeof defaultValue.value === 'number'
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.BOOLEAN:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'value' in defaultValue &&
|
|
||||||
typeof defaultValue.value === 'boolean'
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.DATE_TIME:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'value' in defaultValue &&
|
|
||||||
defaultValue.value instanceof Date
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.LINK:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'label' in defaultValue &&
|
|
||||||
typeof defaultValue.label === 'string' &&
|
|
||||||
'url' in defaultValue &&
|
|
||||||
typeof defaultValue.url === 'string'
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.CURRENCY:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'amountMicros' in defaultValue &&
|
|
||||||
typeof defaultValue.amountMicros === 'number' &&
|
|
||||||
'currencyCode' in defaultValue &&
|
|
||||||
typeof defaultValue.currencyCode === 'string'
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.FULL_NAME:
|
|
||||||
return (
|
|
||||||
typeof defaultValue === 'object' &&
|
|
||||||
'firstName' in defaultValue &&
|
|
||||||
typeof defaultValue.firstName === 'string' &&
|
|
||||||
'lastName' in defaultValue &&
|
|
||||||
typeof defaultValue.lastName === 'string'
|
|
||||||
);
|
|
||||||
|
|
||||||
case FieldMetadataType.MULTI_SELECT:
|
|
||||||
return (
|
|
||||||
Array.isArray(defaultValue) &&
|
|
||||||
defaultValue.every((value) => typeof value === 'string')
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
@ -0,0 +1,64 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { validateSync } from 'class-validator';
|
||||||
|
|
||||||
|
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import {
|
||||||
|
FieldMetadataDefaultValueBoolean,
|
||||||
|
FieldMetadataDefaultValueCurrency,
|
||||||
|
FieldMetadataDefaultValueDateTime,
|
||||||
|
FieldMetadataDefaultValueFullName,
|
||||||
|
FieldMetadataDefaultValueLink,
|
||||||
|
FieldMetadataDefaultValueNumber,
|
||||||
|
FieldMetadataDefaultValueString,
|
||||||
|
FieldMetadataDefaultValueStringArray,
|
||||||
|
FieldMetadataDynamicDefaultValueNow,
|
||||||
|
FieldMetadataDynamicDefaultValueUuid,
|
||||||
|
} from 'src/metadata/field-metadata/dtos/default-value.input';
|
||||||
|
|
||||||
|
export const defaultValueValidatorsMap = {
|
||||||
|
[FieldMetadataType.UUID]: [
|
||||||
|
FieldMetadataDefaultValueString,
|
||||||
|
FieldMetadataDynamicDefaultValueUuid,
|
||||||
|
],
|
||||||
|
[FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString],
|
||||||
|
[FieldMetadataType.PHONE]: [FieldMetadataDefaultValueString],
|
||||||
|
[FieldMetadataType.EMAIL]: [FieldMetadataDefaultValueString],
|
||||||
|
[FieldMetadataType.DATE_TIME]: [
|
||||||
|
FieldMetadataDefaultValueDateTime,
|
||||||
|
FieldMetadataDynamicDefaultValueNow,
|
||||||
|
],
|
||||||
|
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
|
||||||
|
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
|
||||||
|
[FieldMetadataType.NUMERIC]: [FieldMetadataDefaultValueString],
|
||||||
|
[FieldMetadataType.PROBABILITY]: [FieldMetadataDefaultValueNumber],
|
||||||
|
[FieldMetadataType.LINK]: [FieldMetadataDefaultValueLink],
|
||||||
|
[FieldMetadataType.CURRENCY]: [FieldMetadataDefaultValueCurrency],
|
||||||
|
[FieldMetadataType.FULL_NAME]: [FieldMetadataDefaultValueFullName],
|
||||||
|
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
|
||||||
|
[FieldMetadataType.SELECT]: [FieldMetadataDefaultValueString],
|
||||||
|
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataDefaultValueStringArray],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateDefaultValueForType = (
|
||||||
|
type: FieldMetadataType,
|
||||||
|
defaultValue: FieldMetadataDefaultValue,
|
||||||
|
): boolean => {
|
||||||
|
if (defaultValue === null) return true;
|
||||||
|
|
||||||
|
const validators = defaultValueValidatorsMap[type];
|
||||||
|
|
||||||
|
if (!validators) return false;
|
||||||
|
|
||||||
|
const isValid = validators.some((validator) => {
|
||||||
|
const defaultValueInstance = plainToInstance<
|
||||||
|
any,
|
||||||
|
FieldMetadataDefaultValue
|
||||||
|
>(validator, defaultValue);
|
||||||
|
|
||||||
|
return validateSync(defaultValueInstance).length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { validateSync } from 'class-validator';
|
||||||
|
|
||||||
|
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import {
|
||||||
|
FieldMetadataComplexOptions,
|
||||||
|
FieldMetadataDefaultOptions,
|
||||||
|
} from 'src/metadata/field-metadata/dtos/options.input';
|
||||||
|
|
||||||
|
export const optionsValidatorsMap = {
|
||||||
|
[FieldMetadataType.RATING]: [FieldMetadataDefaultOptions],
|
||||||
|
[FieldMetadataType.SELECT]: [FieldMetadataDefaultOptions],
|
||||||
|
[FieldMetadataType.MULTI_SELECT]: [FieldMetadataComplexOptions],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateOptionsForType = (
|
||||||
|
type: FieldMetadataType,
|
||||||
|
options: FieldMetadataOptions,
|
||||||
|
): boolean => {
|
||||||
|
if (options === null) return true;
|
||||||
|
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validators = optionsValidatorsMap[type];
|
||||||
|
|
||||||
|
if (!validators) return false;
|
||||||
|
|
||||||
|
const isValid = options.every((option) => {
|
||||||
|
return validators.some((validator) => {
|
||||||
|
const optionsInstance = plainToInstance<any, FieldMetadataDefaultOptions>(
|
||||||
|
validator,
|
||||||
|
option,
|
||||||
|
);
|
||||||
|
|
||||||
|
return validateSync(optionsInstance).length === 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
};
|
@ -1,27 +0,0 @@
|
|||||||
import {
|
|
||||||
registerDecorator,
|
|
||||||
ValidationOptions,
|
|
||||||
ValidationArguments,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
import { validateDefaultValueBasedOnType } from 'src/metadata/field-metadata/utils/validate-default-value-based-on-type.util';
|
|
||||||
|
|
||||||
export const IsDefaultValue = (validationOptions?: ValidationOptions) => {
|
|
||||||
return function (object: any, propertyName: string) {
|
|
||||||
registerDecorator({
|
|
||||||
name: 'isDefaultValue',
|
|
||||||
target: object.constructor,
|
|
||||||
propertyName: propertyName,
|
|
||||||
constraints: [],
|
|
||||||
options: validationOptions,
|
|
||||||
validator: {
|
|
||||||
validate(value: any, args: ValidationArguments) {
|
|
||||||
// Extract type value from the object
|
|
||||||
const type = (args.object as any)['type'];
|
|
||||||
|
|
||||||
return validateDefaultValueBasedOnType(value, type);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ValidationArguments,
|
||||||
|
ValidatorConstraint,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
|
||||||
|
import {
|
||||||
|
FieldMetadataEntity,
|
||||||
|
FieldMetadataType,
|
||||||
|
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import { validateDefaultValueForType } from 'src/metadata/field-metadata/utils/validate-default-value-for-type.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@ValidatorConstraint({ name: 'isFieldMetadataDefaultValue', async: true })
|
||||||
|
export class IsFieldMetadataDefaultValue
|
||||||
|
implements ValidatorConstraintInterface
|
||||||
|
{
|
||||||
|
constructor(private readonly fieldMetadataService: FieldMetadataService) {}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
value: FieldMetadataDefaultValue,
|
||||||
|
args: ValidationArguments,
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Try to extract type value from the object
|
||||||
|
let type: FieldMetadataType | null = args.object['type'];
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
// Extract id value from the instance, should happen only when updating
|
||||||
|
const id: string | undefined = args.instance?.['id'];
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fieldMetadata: FieldMetadataEntity;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fieldMetadata = await this.fieldMetadataService.findOneOrFail(id);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
type = fieldMetadata.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateDefaultValueForType(type, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage(): string {
|
||||||
|
return 'FieldMetadataDefaultValue is not valid';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ValidationArguments, ValidatorConstraint } from 'class-validator';
|
||||||
|
|
||||||
|
import { FieldMetadataOptions } from 'src/metadata/field-metadata/interfaces/field-metadata-options.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
|
||||||
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
|
import { validateOptionsForType } from 'src/metadata/field-metadata/utils/validate-options-for-type.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
@ValidatorConstraint({ name: 'isFieldMetadataOptions', async: true })
|
||||||
|
export class IsFieldMetadataOptions {
|
||||||
|
constructor(private readonly fieldMetadataService: FieldMetadataService) {}
|
||||||
|
|
||||||
|
async validate(
|
||||||
|
value: FieldMetadataOptions,
|
||||||
|
args: ValidationArguments,
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Try to extract type value from the object
|
||||||
|
let type: FieldMetadataType | null = args.object['type'];
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
// Extract id value from the instance, should happen only when updating
|
||||||
|
const id: string | undefined = args.instance?.['id'];
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldMetadata = await this.fieldMetadataService.findOneOrFail(id);
|
||||||
|
|
||||||
|
type = fieldMetadata.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return validateOptionsForType(type, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMessage(): string {
|
||||||
|
return 'FieldMetadataOptions is not valid';
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface';
|
import { WorkspaceColumnActionOptions } from 'src/metadata/workspace-migration/interfaces/workspace-column-action-options.interface';
|
||||||
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/metadata/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
import { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.interface';
|
||||||
|
|
||||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||||
import {
|
import {
|
||||||
@ -33,7 +34,7 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
|||||||
options?: WorkspaceColumnActionOptions,
|
options?: WorkspaceColumnActionOptions,
|
||||||
): WorkspaceMigrationColumnCreate {
|
): WorkspaceMigrationColumnCreate {
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
fieldMetadata.defaultValue?.value ?? options?.defaultValue;
|
this.getDefaultValue(fieldMetadata.defaultValue) ?? options?.defaultValue;
|
||||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -51,7 +52,8 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
|||||||
options?: WorkspaceColumnActionOptions,
|
options?: WorkspaceColumnActionOptions,
|
||||||
): WorkspaceMigrationColumnAlter {
|
): WorkspaceMigrationColumnAlter {
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
nextFieldMetadata.defaultValue?.value ?? options?.defaultValue;
|
this.getDefaultValue(nextFieldMetadata.defaultValue) ??
|
||||||
|
options?.defaultValue;
|
||||||
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
const serializedDefaultValue = serializeDefaultValue(defaultValue);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -62,4 +64,19 @@ export class BasicColumnActionFactory extends ColumnActionAbstractFactory<BasicF
|
|||||||
defaultValue: serializedDefaultValue,
|
defaultValue: serializedDefaultValue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultValue(
|
||||||
|
defaultValue:
|
||||||
|
| FieldMetadataDefaultValue<BasicFieldMetadataType>
|
||||||
|
| undefined
|
||||||
|
| null,
|
||||||
|
) {
|
||||||
|
if (!defaultValue) return null;
|
||||||
|
|
||||||
|
if ('type' in defaultValue) {
|
||||||
|
return defaultValue;
|
||||||
|
} else {
|
||||||
|
return defaultValue?.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user