Refactor seed to use twenty-standard application (#16598)

# Introduction
In this pull-request we introduce a service dedicated to the
twenty-standard app installation, we will later be able to re-use
existing logic to be more generic and allow any app installation.
For the moment sticking to this usage
https://github.com/twentyhq/core-team-issues/issues/1995

## Encountered issues
- We decided not to migrate deprecated fields ( also they will become
custom field for any existing workspace having them in the future )
- duplicate criteria
- wrong search index declaration
- forgotten isSearchable
- Attachement seed
- Restored standardId

## Note
For the moment we're still searching through standardId for code that
run on both existing and new workspaces.
For code running on new workspace exclusively we're searching using
universalIdentifier
We will standardize universalIdentifier usage later when we've migratred
all the existing workspaces

## Workspace creation
Will handle workspace creation the same way in another PR

Related https://github.com/twentyhq/twenty/pull/15065

## TODO
- [ ] Double all frontend hardcoded queries to not refer to deprecated
fields especially attachments
This commit is contained in:
Paul Rastoin
2025-12-18 17:08:55 +01:00
committed by GitHub
parent d7fc9387a0
commit 38785cd4e9
74 changed files with 897 additions and 726 deletions
@@ -12,12 +12,10 @@ describe('attachmentsResolver (e2e)', () => {
node {
name
fullPath
type
id
createdAt
updatedAt
deletedAt
authorId
taskId
noteId
personId
@@ -54,7 +52,6 @@ describe('attachmentsResolver (e2e)', () => {
expect(attachments).toHaveProperty('name');
expect(attachments).toHaveProperty('fullPath');
expect(attachments).toHaveProperty('type');
expect(attachments).toHaveProperty('id');
expect(attachments).toHaveProperty('createdAt');
expect(attachments).toHaveProperty('updatedAt');
@@ -17,7 +17,7 @@ import { createManyOperationFactory } from 'test/integration/graphql/utils/creat
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils';
import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util';
import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util';
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
import { type ObjectRecord } from 'twenty-shared/types';
@@ -67,10 +67,12 @@ describe('relation connect in workspace createOne/createMany resolvers (e2e)',
}),
);
await performCreateManyOperation('company', 'companies', `id`, [
company1,
company2,
]);
await createManyOperation({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: 'id',
data: [company1, company2],
});
});
beforeEach(async () => {
@@ -20,7 +20,7 @@ import {
TEST_PET_ID_3,
TEST_PET_ID_4,
} from 'test/integration/constants/test-pet-ids.constants';
import { performCreateManyOperation } from 'test/integration/graphql/utils/perform-create-many-operation.utils';
import { createManyOperation } from 'test/integration/graphql/utils/create-many-operation.util';
import { search } from 'test/integration/graphql/utils/search.util';
import { deleteAllRecords } from 'test/integration/utils/delete-all-records';
import {
@@ -113,6 +113,7 @@ describe('SearchResolver', () => {
const [searchInput1Pet, searchInput2Pet, cafePet, naivePet] = pets;
beforeAll(async () => {
// TODO refactor not a good practice, or should at least restore afterwards
await deleteAllRecords('person');
await deleteAllRecords('company');
await deleteAllRecords('opportunity');
@@ -124,33 +125,28 @@ describe('SearchResolver', () => {
await deleteAllRecords('_pet');
await deleteAllRecords('_surveyResult');
await deleteAllRecords('_rocket');
///
try {
await performCreateManyOperation(
'pet',
'pets',
OBJECT_MODEL_COMMON_FIELDS,
pets,
);
await createManyOperation({
objectMetadataSingularName: 'pet',
objectMetadataPluralName: 'pets',
gqlFields: OBJECT_MODEL_COMMON_FIELDS,
data: pets,
});
await performCreateManyOperation(
'person',
'people',
PERSON_GQL_FIELDS,
persons,
);
await createManyOperation({
objectMetadataSingularName: 'person',
objectMetadataPluralName: 'people',
gqlFields: PERSON_GQL_FIELDS,
data: persons,
});
await performCreateManyOperation(
'company',
'companies',
COMPANY_GQL_FIELDS,
companies,
);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
throw new Error('beforeAll failed');
}
await createManyOperation({
objectMetadataSingularName: 'company',
objectMetadataPluralName: 'companies',
gqlFields: COMPANY_GQL_FIELDS,
data: companies,
});
});
const testsUseCases: EachTestingContext<{
@@ -28,6 +28,7 @@ describe('Granular settings permissions', () => {
originalMemberRoleId = memberRole.id;
// Create a custom role with canUpdateAllSettings = false
// canUpdateAllObjectRecords must be true to allow creating records like workflows
const createRoleQuery = {
query: `
mutation CreateOneRole {
@@ -36,7 +37,7 @@ describe('Granular settings permissions', () => {
description: "Role for testing specific setting permissions"
canUpdateAllSettings: false
canReadAllObjectRecords: true
canUpdateAllObjectRecords: false
canUpdateAllObjectRecords: true
canSoftDeleteAllObjectRecords: false
canDestroyAllObjectRecords: false
}) {
@@ -0,0 +1,66 @@
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ObjectRecord } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import { v4 } from 'uuid';
type CreateManyOperationInput = {
objectMetadataSingularName: string;
objectMetadataPluralName: string;
gqlFields?: string;
data: object[];
upsert?: boolean;
expectToFail?: boolean;
token?: string;
};
export const createManyOperation = async ({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields = 'id',
data,
upsert = false,
expectToFail = false,
token,
}: CreateManyOperationInput): CommonResponseBody<{
createdRecords: ObjectRecord[];
}> => {
const graphqlOperation = createManyOperationFactory({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
data: data.map((item) => ({
id: v4(),
...item,
})),
upsert,
});
const response = await makeGraphqlAPIRequest(graphqlOperation, token);
if (expectToFail === true) {
warnIfNoErrorButExpectedToFail({
response,
errorMessage: 'Create many operation should have failed but did not',
});
}
if (expectToFail === false) {
warnIfErrorButNotExpectedToFail({
response,
errorMessage: 'Create many operation failed but should not have',
});
}
return {
data: {
createdRecords:
response.body.data?.[`create${capitalize(objectMetadataPluralName)}`] ??
[],
},
errors: response.body.errors,
};
};
@@ -1,25 +0,0 @@
import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { capitalize } from 'twenty-shared/utils';
import { v4 } from 'uuid';
export const performCreateManyOperation = async (
objectMetadataSingularName: string,
objectMetadataPluralName: string,
gqlFields: string,
data: object[],
) => {
const response = await makeGraphqlAPIRequest(
createManyOperationFactory({
objectMetadataSingularName,
objectMetadataPluralName,
gqlFields,
data: data.map((item) => ({
id: v4(),
...item,
})),
}),
);
return response.body.data[`create${capitalize(objectMetadataPluralName)}`];
};
@@ -190,25 +190,23 @@ describe('Standard field metadata update should succeed', () => {
);
});
it.failing(
'Should deactivate and reactivate standard field successfully',
async () => {
const deletedAtField = opportunityObjectFields.find(
(field) => field.name === 'deletedAt',
);
it('Should deactivate and reactivate standard field successfully', async () => {
const deletedAtField = opportunityObjectFields.find(
(field) => field.name === 'deletedAt',
);
jestExpectToBeDefined(deletedAtField);
expect(deletedAtField.isActive).toBe(true);
jestExpectToBeDefined(deletedAtField);
expect(deletedAtField.isActive).toBe(true);
const { data: firstUpdateData } = await updateOneFieldMetadata({
input: {
idToUpdate: deletedAtField.id,
updatePayload: {
isActive: false,
},
const { data: firstUpdateData } = await updateOneFieldMetadata({
input: {
idToUpdate: deletedAtField.id,
updatePayload: {
isActive: false,
},
expectToFail: false,
gqlFields: `
},
expectToFail: false,
gqlFields: `
id
name
label
@@ -224,19 +222,19 @@ describe('Standard field metadata update should succeed', () => {
icon
}
`,
});
});
expect(firstUpdateData.updateOneField.isActive).toBe(false);
expect(firstUpdateData.updateOneField.isActive).toBe(false);
const { data: secondUpdateData } = await updateOneFieldMetadata({
input: {
idToUpdate: deletedAtField.id,
updatePayload: {
isActive: true,
},
const { data: secondUpdateData } = await updateOneFieldMetadata({
input: {
idToUpdate: deletedAtField.id,
updatePayload: {
isActive: true,
},
expectToFail: false,
gqlFields: `
},
expectToFail: false,
gqlFields: `
id
name
label
@@ -252,11 +250,10 @@ describe('Standard field metadata update should succeed', () => {
icon
}
`,
});
});
expect(secondUpdateData.updateOneField.isActive).toBe(true);
},
);
expect(secondUpdateData.updateOneField.isActive).toBe(true);
});
});
describe('Standard field isUnique update should succeed', () => {
@@ -1,11 +1,7 @@
const TEST_SCHEMA_NAME = 'workspace_1wgvd1injqtife6y4rvfbu3h5';
export const deleteAllRecords = async (objectNameSingular: string) => {
try {
await global.testDataSource.query(
`DELETE from "${TEST_SCHEMA_NAME}"."${objectNameSingular}"`,
);
} catch {
/* empty */
}
await global.testDataSource.query(
`DELETE from "${TEST_SCHEMA_NAME}"."${objectNameSingular}"`,
);
};