Etienne
2025-08-04 19:06:40 +02:00
committed by GitHub
parent 932910e71e
commit 3c485ccb37
4 changed files with 190 additions and 22 deletions
@@ -89,19 +89,12 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
): Promise<InsertResult> {
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
const selectedColumns = buildColumnsToSelect({
select: executionArgs.graphqlQuerySelectedFieldsResult.select,
relations: executionArgs.graphqlQuerySelectedFieldsResult.relations,
objectMetadataItemWithFieldMaps,
});
const conflictingFields = this.getConflictingFields(
objectMetadataItemWithFieldMaps,
);
const existingRecords = await this.findExistingRecords(
executionArgs,
conflictingFields,
selectedColumns,
);
const { recordsToUpdate, recordsToInsert } = this.categorizeRecords(
@@ -187,7 +180,6 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
fullPath: string;
column: string;
}[],
selectedColumns: Record<string, boolean>,
): Promise<Partial<ObjectRecord>[]> {
const { objectMetadataItemWithFieldMaps } = executionArgs.options;
const queryBuilder = executionArgs.repository.createQueryBuilder(
@@ -203,12 +195,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
queryBuilder.orWhere(condition);
});
return await queryBuilder
.setFindOptions({
select: selectedColumns,
})
.withDeleted()
.getMany();
return await queryBuilder.withDeleted().getMany();
}
private getValueFromPath(
@@ -271,11 +258,17 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
for (const field of conflictingFields) {
const requestFieldValue = this.getValueFromPath(record, field.fullPath);
const existingRec = existingRecords.find(
(existingRecord) =>
isDefined(existingRecord[field.column]) &&
existingRecord[field.column] === requestFieldValue,
);
const existingRec = existingRecords.find((existingRecord) => {
const existingFieldValue = this.getValueFromPath(
existingRecord,
field.fullPath,
);
return (
isDefined(existingFieldValue) &&
existingFieldValue === requestFieldValue
);
});
if (existingRec) {
existingRecord = { ...record, id: existingRec.id };
@@ -5,12 +5,18 @@ import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-qu
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
interface PostgreSQLError extends QueryFailedError {
detail?: string;
}
export const handleDuplicateKeyError = (
error: QueryFailedError,
error: PostgreSQLError,
context: WorkspaceQueryRunnerOptions,
) => {
const indexNameMatch = error.message.match(/"([^"]+)"/);
const duplicatedValues = error?.detail?.match(/=\(([^)]+)\)/)?.[1];
if (indexNameMatch) {
const indexName = indexNameMatch[1];
@@ -42,9 +48,9 @@ export const handleDuplicateKeyError = (
if (affectedColumns?.length === 1) {
throw new UserInputError(
`Duplicate ${columnNames}. Please set a unique one.`,
`Duplicate ${columnNames} ${duplicatedValues ? `with value ${duplicatedValues}` : ''}. Please set a unique one.`,
{
userFriendlyMessage: `This ${columnNames.toLowerCase()} is already taken. Please choose a different value.`,
userFriendlyMessage: `This ${columnNames.toLowerCase()} ${duplicatedValues ? `with value ${duplicatedValues}` : ''} is already taken. Please choose a different value.`,
},
);
}
@@ -9,6 +9,9 @@ export const PERSON_GQL_FIELDS = `
firstName
lastName
}
emails {
primaryEmail
}
createdAt
deletedAt
`;
@@ -0,0 +1,166 @@
import { PERSON_GQL_FIELDS } from 'test/integration/constants/person-gql-fields.constants';
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util';
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
describe('people resolvers (integration)', () => {
let person2Id: string;
beforeAll(async () => {
await deleteAllRecords('person');
});
it('should create many people', async () => {
const graphqlOperation = createManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
data: [
{
name: {
firstName: 'John',
lastName: 'Doe',
},
jobTitle: 'Just Created',
emails: {
primaryEmail: 'john.doe@example.com',
},
},
{
name: {
firstName: 'Jane',
lastName: 'Smith',
},
jobTitle: 'Just Created',
emails: {
primaryEmail: 'jane.smith@example.com',
},
},
{
name: {
firstName: 'Tim',
lastName: 'Apple',
},
jobTitle: 'Just Created',
emails: {
primaryEmail: 'tim.apple@example.com',
},
},
],
upsert: true,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data.createPeople).toHaveLength(3);
expect(response.body.errors).toBeUndefined();
});
it('should update many people', async () => {
const findOneOperation = findOneOperationFactory({
objectMetadataSingularName: 'person',
gqlFields: PERSON_GQL_FIELDS,
filter: {
emails: {
primaryEmail: {
eq: 'jane.smith@example.com',
},
},
},
});
const findOneResponse = await makeGraphqlAPIRequest(findOneOperation);
person2Id = findOneResponse.body.data.person.id;
const graphqlOperation = createManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
data: [
{
emails: {
primaryEmail: 'john.doe@example.com',
},
jobTitle: 'Just Updated',
},
{
id: person2Id,
jobTitle: 'Just Updated',
},
],
upsert: true,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
const findAllOperation = findManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
});
const findAllResponse = await makeGraphqlAPIRequest(findAllOperation);
expect(findAllResponse.body.data.people.edges.length).toBe(3);
expect(response.body.data.createPeople).toHaveLength(2);
expect(response.body.errors).toBeUndefined();
response.body.data.createPeople.forEach((person: any) => {
expect(person.jobTitle).toEqual('Just Updated');
});
});
it('should update and create many people', async () => {
const graphqlOperation = createManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
data: [
{
emails: {
primaryEmail: 'tim.apple@example.com',
},
jobTitle: 'Just Updated',
},
{
jobTitle: 'Just Created',
emails: {
primaryEmail: 'paul.doe@example.com',
},
},
{
id: person2Id,
jobTitle: 'Email Just Updated',
emails: {
primaryEmail: 'jane.smith@updated.com',
},
},
],
upsert: true,
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
const findAllOperation = findManyOperationFactory({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
});
const findAllResponse = await makeGraphqlAPIRequest(findAllOperation);
expect(findAllResponse.body.data.people.edges.length).toBe(4);
expect(response.body.data.createPeople).toHaveLength(3);
expect(
response.body.data.createPeople.find(
(person: any) => person.id === person2Id,
).emails.primaryEmail,
).toEqual('jane.smith@updated.com');
expect(response.body.errors).toBeUndefined();
});
});