Common Api - createOne/Many (#15083)

closes https://github.com/twentyhq/core-team-issues/issues/1578
This commit is contained in:
Etienne
2025-10-15 14:13:07 +02:00
committed by GitHub
parent f65783f900
commit 4ae299973b
31 changed files with 1573 additions and 45 deletions
@@ -7,14 +7,72 @@ import {
import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-shared/utils';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonCreateManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-many-query-runner/common-create-many-query-runner.service';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
@Injectable()
export class RestApiCreateManyHandler extends RestApiBaseHandler {
constructor(
private readonly commonCreateManyQueryRunnerService: CommonCreateManyQueryRunnerService,
) {
super();
}
async commonHandle(request: AuthenticatedRequest) {
try {
const { data, depth, upsert } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFieldsResult = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const records = await this.commonCreateManyQueryRunnerService.run({
args: { data, selectedFieldsResult, upsert },
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
});
return this.formatRestResponse(
records,
objectMetadataItemWithFieldMaps.namePlural,
);
} catch (error) {
workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(
records: ObjectRecord[],
objectNamePlural: string,
) {
return { data: { [objectNamePlural]: records } };
}
private parseRequestArgs(request: AuthenticatedRequest) {
return {
data: request.body,
depth: parseDepthRestRequest(request),
upsert: parseUpsertRestRequest(request),
};
}
async handle(request: AuthenticatedRequest) {
const { objectMetadata, repository, restrictedFields } =
await this.getRepositoryAndMetadataOrFail(request);
@@ -7,14 +7,69 @@ import {
import isEmpty from 'lodash.isempty';
import { isDefined } from 'twenty-shared/utils';
import { ObjectRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { RestApiBaseHandler } from 'src/engine/api/rest/core/interfaces/rest-api-base.handler';
import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service';
import { parseDepthRestRequest } from 'src/engine/api/rest/input-request-parsers/depth-parser-utils/parse-depth-rest-request.util';
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
import { AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
import { workspaceQueryRunnerRestApiExceptionHandler } from 'src/engine/api/rest/utils/workspace-query-runner-rest-api-exception-handler.util';
import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils';
@Injectable()
export class RestApiCreateOneHandler extends RestApiBaseHandler {
constructor(
private readonly commonCreateOneQueryRunnerService: CommonCreateOneQueryRunnerService,
) {
super();
}
async commonHandle(request: AuthenticatedRequest) {
try {
const { data, depth, upsert } = this.parseRequestArgs(request);
const {
authContext,
objectMetadataItemWithFieldMaps,
objectMetadataMaps,
} = await this.buildCommonOptions(request);
const selectedFieldsResult = await this.computeSelectedFields({
depth,
objectMetadataMapItem: objectMetadataItemWithFieldMaps,
objectMetadataMaps,
authContext,
});
const record = await this.commonCreateOneQueryRunnerService.run({
args: { data, selectedFieldsResult, upsert },
authContext,
objectMetadataMaps,
objectMetadataItemWithFieldMaps,
});
return this.formatRestResponse(
record,
objectMetadataItemWithFieldMaps.nameSingular,
);
} catch (error) {
workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
private formatRestResponse(record: ObjectRecord, objectNameSingular: string) {
return { data: { [objectNameSingular]: record } };
}
private parseRequestArgs(request: AuthenticatedRequest) {
return {
data: request.body,
depth: parseDepthRestRequest(request),
upsert: parseUpsertRestRequest(request),
};
}
async handle(request: AuthenticatedRequest) {
const { objectMetadata, repository, restrictedFields } =
await this.getRepositoryAndMetadataOrFail(request);
@@ -85,7 +85,7 @@ export class RestApiFindOneHandler extends RestApiBaseHandler {
objectMetadataItemWithFieldMaps.nameSingular,
);
} catch (error) {
return workspaceQueryRunnerRestApiExceptionHandler(error);
workspaceQueryRunnerRestApiExceptionHandler(error);
}
}
@@ -39,11 +39,23 @@ export class RestApiCoreService {
}
async createOne(request: AuthenticatedRequest) {
return await this.restApiCreateOneHandler.handle(request);
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
if (isCommonApiEnabled) {
return await this.restApiCreateOneHandler.commonHandle(request);
} else {
return await this.restApiCreateOneHandler.handle(request);
}
}
async createMany(request: AuthenticatedRequest) {
return await this.restApiCreateManyHandler.handle(request);
const isCommonApiEnabled = await this.isCommonApiEnabled(request);
if (isCommonApiEnabled) {
return await this.restApiCreateManyHandler.commonHandle(request);
} else {
return await this.restApiCreateManyHandler.handle(request);
}
}
async findDuplicates(request: AuthenticatedRequest) {
@@ -0,0 +1,61 @@
import { parseUpsertRestRequest } from 'src/engine/api/rest/input-request-parsers/upsert-parser-utils/parse-upsert-rest-request.util';
describe('parseUpsertRestRequest', () => {
it('should return false when upsert query parameter is not defined', () => {
const request = {
query: {},
} as any;
const result = parseUpsertRestRequest(request);
expect(result).toBe(false);
});
it('should return true when upsert query parameter is "true"', () => {
const request = {
query: {
upsert: 'true',
},
} as any;
const result = parseUpsertRestRequest(request);
expect(result).toBe(true);
});
it('should return false when upsert query parameter is "false"', () => {
const request = {
query: {
upsert: 'false',
},
} as any;
const result = parseUpsertRestRequest(request);
expect(result).toBe(false);
});
it('should return false when upsert query parameter is empty string', () => {
const request = {
query: {
upsert: '',
},
} as any;
const result = parseUpsertRestRequest(request);
expect(result).toBe(false);
});
it('should return false when upsert query parameter is a boolean true', () => {
const request = {
query: {
upsert: true,
},
} as any;
const result = parseUpsertRestRequest(request);
expect(result).toBe(false);
});
});
@@ -0,0 +1,13 @@
import { isDefined } from 'twenty-shared/utils';
import { type AuthenticatedRequest } from 'src/engine/api/rest/types/authenticated-request';
export const parseUpsertRestRequest = (
request: AuthenticatedRequest,
): boolean => {
if (!isDefined(request.query.upsert)) {
return false;
}
return request.query.upsert === 'true';
};