Common - Field validation (#15491)

Closes : https://github.com/twentyhq/core-team-issues/issues/1622

To do in other PR : 
- Add migration command for non nullable text, raw_json & array fields
- Add null transformation

---------

Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Etienne
2025-11-13 15:45:35 +01:00
committed by GitHub
parent f6ed1e8295
commit 261cba40ef
114 changed files with 3524 additions and 1189 deletions
@@ -6,7 +6,8 @@ import { Omit } from 'zod/v4/core/util.cjs';
import { WorkspaceAuthContext } from 'src/engine/api/common/interfaces/workspace-auth-context.interface';
import { QueryResultFieldValue } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/interfaces/query-result-field-value';
import { CommonSelectedFieldsHandler } from 'src/engine/api/common/common-args-handlers/common-query-selected-fields/common-selected-fields.handler';
import { DataArgProcessor } from 'src/engine/api/common/common-args-processors/data-arg-processor/data-arg.processor';
import { QueryRunnerArgsFactory } from 'src/engine/api/common/common-args-processors/query-runner-args.factory';
import {
CommonQueryRunnerException,
CommonQueryRunnerExceptionCode,
@@ -25,8 +26,6 @@ import { isWorkspaceAuthContext } from 'src/engine/api/common/utils/is-workspace
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters/query-result-getters.factory';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service';
@@ -62,7 +61,7 @@ export abstract class CommonBaseQueryRunnerService<
@Inject()
protected readonly queryRunnerArgsFactory: QueryRunnerArgsFactory;
@Inject()
protected readonly queryResultGettersFactory: QueryResultGettersFactory;
protected readonly dataArgProcessor: DataArgProcessor;
@Inject()
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager;
@Inject()
@@ -76,8 +75,6 @@ export abstract class CommonBaseQueryRunnerService<
@Inject()
protected readonly apiKeyRoleService: ApiKeyRoleService;
@Inject()
protected readonly selectedFieldsHandler: CommonSelectedFieldsHandler;
@Inject()
protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService;
@Inject()
protected readonly commonResultGettersService: CommonResultGettersService;
@@ -191,18 +188,19 @@ export abstract class CommonBaseQueryRunnerService<
);
const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext;
const computedArgs = await this.computeArgs(args, queryRunnerContext);
const hookedArgs =
(await this.workspaceQueryHookService.executePreQueryHooks(
authContext,
objectMetadataItemWithFieldMaps.nameSingular,
operationName,
args as WorkspacePreQueryHookPayload<CommonQueryNames>,
computedArgs as WorkspacePreQueryHookPayload<CommonQueryNames>,
)) as CommonInput<Args>;
const computedArgs = await this.computeArgs(hookedArgs, queryRunnerContext);
return {
...computedArgs,
...hookedArgs,
selectedFieldsResult,
};
}
@@ -140,7 +140,7 @@ export class CommonCreateManyQueryRunnerService extends CommonBaseQueryRunnerSer
return {
...args,
data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
data: await this.dataArgProcessor.process({
partialRecordInputs: args.data,
authContext,
objectMetadataItemWithFieldMaps,
@@ -53,15 +53,15 @@ export class CommonCreateOneQueryRunnerService extends CommonBaseQueryRunnerServ
): Promise<CommonInput<CreateOneQueryArgs>> {
const { authContext, objectMetadataItemWithFieldMaps } = queryRunnerContext;
const coercedData = await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
});
return {
...args,
data: (
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
})
)[0],
data: coercedData[0],
};
}
@@ -171,7 +171,7 @@ export class CommonFindDuplicatesQueryRunnerService extends CommonBaseQueryRunne
),
) ?? [],
),
data: await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
data: await this.dataArgProcessor.process({
partialRecordInputs: args.data,
authContext,
objectMetadataItemWithFieldMaps,
@@ -29,10 +29,6 @@ import {
CommonQueryNames,
MergeManyQueryArgs,
} from 'src/engine/api/common/types/common-query-args.type';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { buildColumnsToReturn } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-return';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { hasRecordFieldValue } from 'src/engine/api/graphql/graphql-query-runner/utils/has-record-field-value.util';
@@ -174,9 +170,9 @@ export class CommonMergeManyQueryRunnerService extends CommonBaseQueryRunnerServ
);
if (!priorityRecord) {
throw new GraphqlQueryRunnerException(
throw new CommonQueryRunnerException(
'Priority record not found',
GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND,
CommonQueryRunnerExceptionCode.RECORD_NOT_FOUND,
);
}
@@ -106,7 +106,7 @@ export class CommonUpdateManyQueryRunnerService extends CommonBaseQueryRunnerSer
objectMetadataItemWithFieldMaps,
) || {},
data: (
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
@@ -65,7 +65,7 @@ export class CommonUpdateOneQueryRunnerService extends CommonBaseQueryRunnerServ
return {
...args,
data: (
await this.queryRunnerArgsFactory.overrideDataByFieldMetadata({
await this.dataArgProcessor.process({
partialRecordInputs: [args.data],
authContext,
objectMetadataItemWithFieldMaps,
@@ -7,6 +7,7 @@ export enum CommonQueryRunnerExceptionCode {
INVALID_QUERY_INPUT = 'INVALID_QUERY_INPUT',
INVALID_AUTH_CONTEXT = 'INVALID_AUTH_CONTEXT',
ARGS_CONFLICT = 'ARGS_CONFLICT',
INVALID_ARGS_DATA = 'INVALID_ARGS_DATA',
INVALID_ARGS_FIRST = 'INVALID_ARGS_FIRST',
INVALID_ARGS_LAST = 'INVALID_ARGS_LAST',
UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT',
@@ -20,6 +20,7 @@ export const commonQueryRunnerToGraphqlApiExceptionHandler = (
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA:
case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT:
case CommonQueryRunnerExceptionCode.INVALID_CURSOR:
case CommonQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED:
@@ -19,6 +19,7 @@ export const commonQueryRunnerToRestApiExceptionHandler = (
case CommonQueryRunnerExceptionCode.INVALID_ARGS_FIRST:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_LAST:
case CommonQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
case CommonQueryRunnerExceptionCode.INVALID_ARGS_DATA:
case CommonQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT:
case CommonQueryRunnerExceptionCode.INVALID_CURSOR:
case CommonQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED: