0001c2e7d0
https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4 How it works - `manifest.json` files are now committed when apps are published to our repo - to display available apps, from the server, we read into our github repo, using a pod-scoped cache - feature flagged - app installation will be behind permission gate MARKETPLACE_APPS Limitations and what is yet to develop - content and settings tabs - installed apps tab - app installation - test and potentially fix reading from .manifest.json once [Reshape manifest structure](https://github.com/twentyhq/core-team-issues/issues/2183) is done. additional work is expected on assets notably. (couldnt properly do it here as manifest.json will only be committed after this pr) - we only read in community/ folder for now - we may want tochange that - the cache is rather artisanal for now and scoped by pod - we may want to change that
428 lines
15 KiB
TypeScript
428 lines
15 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { type FindOneOptions, type Repository } from 'typeorm';
|
|
|
|
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
|
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
|
import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
|
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import {
|
|
FieldMetadataException,
|
|
FieldMetadataExceptionCode,
|
|
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
|
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
|
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
|
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
|
import { fromCreateFieldInputToFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-create-field-input-to-flat-field-metadatas-to-create.util';
|
|
import { fromDeleteFieldInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-delete-field-input-to-flat-field-metadatas-to-delete.util';
|
|
import { fromUpdateFieldInputToFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-update-field-input-to-flat-field-metadata.util';
|
|
import { throwOnFieldInputTranspilationsError } from 'src/engine/metadata-modules/flat-field-metadata/utils/throw-on-field-input-transpilations-error.util';
|
|
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
|
import { EMPTY_ORCHESTRATOR_FAILURE_REPORT } from 'src/engine/workspace-manager/workspace-migration/constant/empty-orchestrator-failure-report.constant';
|
|
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
|
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
|
|
|
@Injectable()
|
|
export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntity> {
|
|
constructor(
|
|
@InjectRepository(FieldMetadataEntity)
|
|
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
|
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
|
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
|
private readonly applicationService: ApplicationService,
|
|
private readonly workspaceCacheService: WorkspaceCacheService,
|
|
) {
|
|
super(fieldMetadataRepository);
|
|
}
|
|
|
|
async createOneField({
|
|
createFieldInput,
|
|
workspaceId,
|
|
applicationId,
|
|
}: {
|
|
createFieldInput: Omit<CreateFieldInput, 'workspaceId'>;
|
|
workspaceId: string;
|
|
/**
|
|
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
|
|
* when interacting with another application than workspace custom one
|
|
* */
|
|
applicationId?: string;
|
|
}): Promise<FlatFieldMetadata> {
|
|
const [createdFieldMetadata] = await this.createManyFields({
|
|
workspaceId,
|
|
createFieldInputs: [createFieldInput],
|
|
applicationId,
|
|
});
|
|
|
|
if (!isDefined(createdFieldMetadata)) {
|
|
throw new FieldMetadataException(
|
|
'Failed to create field metadata',
|
|
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
|
|
return createdFieldMetadata;
|
|
}
|
|
|
|
async deleteOneField({
|
|
deleteOneFieldInput,
|
|
workspaceId,
|
|
isSystemBuild = false,
|
|
}: {
|
|
deleteOneFieldInput: DeleteOneFieldInput;
|
|
workspaceId: string;
|
|
isSystemBuild?: boolean;
|
|
}): Promise<FlatFieldMetadata> {
|
|
const {
|
|
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
|
flatIndexMaps: existingFlatIndexMaps,
|
|
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
|
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
|
{
|
|
workspaceId,
|
|
flatMapsKeys: [
|
|
'flatObjectMetadataMaps',
|
|
'flatIndexMaps',
|
|
'flatFieldMetadataMaps',
|
|
],
|
|
},
|
|
);
|
|
|
|
const {
|
|
flatFieldMetadatasToDelete,
|
|
flatIndexesToDelete,
|
|
flatIndexesToUpdate,
|
|
} = fromDeleteFieldInputToFlatFieldMetadatasToDelete({
|
|
deleteOneFieldInput,
|
|
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
|
flatIndexMaps: existingFlatIndexMaps,
|
|
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
|
});
|
|
|
|
const validateAndBuildResult =
|
|
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
|
{
|
|
allFlatEntityOperationByMetadataName: {
|
|
fieldMetadata: {
|
|
flatEntityToCreate: [],
|
|
flatEntityToDelete: flatFieldMetadatasToDelete,
|
|
flatEntityToUpdate: [],
|
|
},
|
|
index: {
|
|
flatEntityToCreate: [],
|
|
flatEntityToDelete: flatIndexesToDelete,
|
|
flatEntityToUpdate: flatIndexesToUpdate,
|
|
},
|
|
},
|
|
workspaceId,
|
|
isSystemBuild,
|
|
},
|
|
);
|
|
|
|
if (isDefined(validateAndBuildResult)) {
|
|
throw new WorkspaceMigrationBuilderException(
|
|
validateAndBuildResult,
|
|
'Multiple validation errors occurred while deleting field',
|
|
);
|
|
}
|
|
|
|
return flatFieldMetadatasToDelete[0];
|
|
}
|
|
|
|
async updateOneField({
|
|
updateFieldInput,
|
|
workspaceId,
|
|
isSystemBuild = false,
|
|
}: {
|
|
updateFieldInput: Omit<UpdateFieldInput, 'workspaceId'>;
|
|
workspaceId: string;
|
|
isSystemBuild?: boolean;
|
|
}): Promise<FlatFieldMetadata> {
|
|
const {
|
|
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
|
flatIndexMaps: existingFlatIndexMaps,
|
|
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
|
flatViewFilterMaps: existingFlatViewFilterMaps,
|
|
flatViewGroupMaps: existingFlatViewGroupMaps,
|
|
flatViewMaps: existingFlatViewMaps,
|
|
flatViewFieldMaps: existingFlatViewFieldMaps,
|
|
} = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
|
{
|
|
workspaceId,
|
|
flatMapsKeys: [
|
|
'flatObjectMetadataMaps',
|
|
'flatIndexMaps',
|
|
'flatFieldMetadataMaps',
|
|
'flatViewFilterMaps',
|
|
'flatViewGroupMaps',
|
|
'flatViewMaps',
|
|
'flatViewFieldMaps',
|
|
],
|
|
},
|
|
);
|
|
|
|
const { workspaceCustomFlatApplication } =
|
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
const inputTranspilationResult = fromUpdateFieldInputToFlatFieldMetadata({
|
|
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
|
flatIndexMaps: existingFlatIndexMaps,
|
|
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
|
updateFieldInput: { ...updateFieldInput, workspaceId },
|
|
flatViewFilterMaps: existingFlatViewFilterMaps,
|
|
flatViewGroupMaps: existingFlatViewGroupMaps,
|
|
flatViewMaps: existingFlatViewMaps,
|
|
flatViewFieldMaps: existingFlatViewFieldMaps,
|
|
flatApplication: workspaceCustomFlatApplication,
|
|
isSystemBuild,
|
|
});
|
|
|
|
if (inputTranspilationResult.status === 'fail') {
|
|
throw new WorkspaceMigrationBuilderException(
|
|
{
|
|
report: {
|
|
...EMPTY_ORCHESTRATOR_FAILURE_REPORT(),
|
|
fieldMetadata: [
|
|
{
|
|
errors: inputTranspilationResult.errors,
|
|
type: 'update',
|
|
metadataName: 'fieldMetadata',
|
|
flatEntityMinimalInformation: {
|
|
id: '',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
status: 'fail',
|
|
},
|
|
'Validation errors occurred while updating field',
|
|
);
|
|
}
|
|
|
|
const {
|
|
flatFieldMetadatasToUpdate,
|
|
flatFieldMetadatasToCreate,
|
|
flatIndexMetadatasToUpdate,
|
|
flatIndexMetadatasToDelete,
|
|
flatIndexMetadatasToCreate,
|
|
flatViewGroupsToCreate,
|
|
flatViewGroupsToDelete,
|
|
flatViewGroupsToUpdate,
|
|
flatViewFiltersToDelete,
|
|
flatViewFiltersToUpdate,
|
|
flatViewFieldsToDelete,
|
|
flatViewsToUpdate,
|
|
flatViewsToDelete,
|
|
} = inputTranspilationResult.result;
|
|
|
|
const validateAndBuildResult =
|
|
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
|
{
|
|
allFlatEntityOperationByMetadataName: {
|
|
fieldMetadata: {
|
|
flatEntityToCreate: flatFieldMetadatasToCreate,
|
|
flatEntityToDelete: [],
|
|
flatEntityToUpdate: flatFieldMetadatasToUpdate,
|
|
},
|
|
index: {
|
|
flatEntityToCreate: flatIndexMetadatasToCreate,
|
|
flatEntityToDelete: flatIndexMetadatasToDelete,
|
|
flatEntityToUpdate: flatIndexMetadatasToUpdate,
|
|
},
|
|
viewFilter: {
|
|
flatEntityToCreate: [],
|
|
flatEntityToDelete: flatViewFiltersToDelete,
|
|
flatEntityToUpdate: flatViewFiltersToUpdate,
|
|
},
|
|
viewGroup: {
|
|
flatEntityToCreate: flatViewGroupsToCreate,
|
|
flatEntityToDelete: flatViewGroupsToDelete,
|
|
flatEntityToUpdate: flatViewGroupsToUpdate,
|
|
},
|
|
view: {
|
|
flatEntityToCreate: [],
|
|
flatEntityToDelete: flatViewsToDelete,
|
|
flatEntityToUpdate: flatViewsToUpdate,
|
|
},
|
|
viewField: {
|
|
flatEntityToCreate: [],
|
|
flatEntityToDelete: flatViewFieldsToDelete,
|
|
flatEntityToUpdate: [],
|
|
},
|
|
},
|
|
workspaceId,
|
|
isSystemBuild,
|
|
},
|
|
);
|
|
|
|
if (isDefined(validateAndBuildResult)) {
|
|
throw new WorkspaceMigrationBuilderException(
|
|
validateAndBuildResult,
|
|
'Multiple validation errors occurred while updating field',
|
|
);
|
|
}
|
|
|
|
const { flatFieldMetadataMaps: recomputedFlatFieldMetadataMaps } =
|
|
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
|
{
|
|
workspaceId,
|
|
flatMapsKeys: ['flatFieldMetadataMaps'],
|
|
},
|
|
);
|
|
|
|
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
|
flatEntityId: flatFieldMetadatasToUpdate[0].id,
|
|
flatEntityMaps: recomputedFlatFieldMetadataMaps,
|
|
});
|
|
}
|
|
|
|
async createManyFields({
|
|
createFieldInputs,
|
|
workspaceId,
|
|
applicationId,
|
|
isSystemBuild = false,
|
|
}: {
|
|
createFieldInputs: Omit<CreateFieldInput, 'workspaceId'>[];
|
|
workspaceId: string;
|
|
/**
|
|
* @deprecated do not use call validateBuildAndRunWorkspaceMigration contextually
|
|
* when interacting with another application than workspace custom one
|
|
* */
|
|
applicationId?: string;
|
|
isSystemBuild?: boolean;
|
|
}): Promise<FlatFieldMetadata[]> {
|
|
if (createFieldInputs.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const { workspaceCustomFlatApplication } =
|
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
|
{
|
|
workspaceId,
|
|
},
|
|
);
|
|
|
|
const {
|
|
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
|
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
|
flatApplicationMaps,
|
|
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
|
'flatObjectMetadataMaps',
|
|
'flatFieldMetadataMaps',
|
|
'flatApplicationMaps',
|
|
]);
|
|
|
|
const ownerFlatApplication = isDefined(applicationId)
|
|
? flatApplicationMaps.byId[applicationId]
|
|
: workspaceCustomFlatApplication;
|
|
|
|
if (!isDefined(ownerFlatApplication)) {
|
|
throw new FieldMetadataException(
|
|
`Could not find related application ${applicationId}`,
|
|
FieldMetadataExceptionCode.APPLICATION_NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const allTranspiledTranspilationInputs: Awaited<
|
|
ReturnType<typeof fromCreateFieldInputToFlatFieldMetadatasToCreate>
|
|
>[] = [];
|
|
|
|
for (const createFieldInput of createFieldInputs) {
|
|
allTranspiledTranspilationInputs.push(
|
|
await fromCreateFieldInputToFlatFieldMetadatasToCreate({
|
|
flatObjectMetadataMaps: existingFlatObjectMetadataMaps,
|
|
flatFieldMetadataMaps: existingFlatFieldMetadataMaps,
|
|
createFieldInput,
|
|
workspaceId,
|
|
flatApplication: ownerFlatApplication,
|
|
}),
|
|
);
|
|
}
|
|
|
|
throwOnFieldInputTranspilationsError(
|
|
allTranspiledTranspilationInputs,
|
|
'Multiple validation errors occurred while creating field',
|
|
);
|
|
|
|
const {
|
|
flatFieldMetadatas: flatFieldMetadatasToCreate,
|
|
indexMetadatas: flatIndexMetadatasToCreate,
|
|
} = allTranspiledTranspilationInputs.reduce(
|
|
(acc, { result }) => ({
|
|
flatFieldMetadatas: [
|
|
...acc.flatFieldMetadatas,
|
|
...result.flatFieldMetadatas,
|
|
],
|
|
indexMetadatas: [...acc.indexMetadatas, ...result.indexMetadatas],
|
|
}),
|
|
{ flatFieldMetadatas: [], indexMetadatas: [] },
|
|
);
|
|
|
|
const validateAndBuildResult =
|
|
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
|
{
|
|
allFlatEntityOperationByMetadataName: {
|
|
fieldMetadata: {
|
|
flatEntityToCreate: flatFieldMetadatasToCreate,
|
|
flatEntityToDelete: [],
|
|
flatEntityToUpdate: [],
|
|
},
|
|
index: {
|
|
flatEntityToCreate: flatIndexMetadatasToCreate,
|
|
flatEntityToDelete: [],
|
|
flatEntityToUpdate: [],
|
|
},
|
|
},
|
|
workspaceId,
|
|
isSystemBuild,
|
|
},
|
|
);
|
|
|
|
if (isDefined(validateAndBuildResult)) {
|
|
throw new WorkspaceMigrationBuilderException(
|
|
validateAndBuildResult,
|
|
'Multiple validation errors occurred while creating fields',
|
|
);
|
|
}
|
|
|
|
const { flatFieldMetadataMaps: recomputedFlatFieldMetadataMaps } =
|
|
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
|
{
|
|
workspaceId,
|
|
flatMapsKeys: ['flatFieldMetadataMaps'],
|
|
},
|
|
);
|
|
|
|
return findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
|
flatEntityIds: allTranspiledTranspilationInputs.map(
|
|
({ result: { flatFieldMetadatas } }) => flatFieldMetadatas[0].id,
|
|
),
|
|
flatEntityMaps: recomputedFlatFieldMetadataMaps,
|
|
});
|
|
}
|
|
|
|
public async findOneWithinWorkspace(
|
|
workspaceId: string,
|
|
options: FindOneOptions<FieldMetadataEntity>,
|
|
) {
|
|
const [fieldMetadata] = await this.fieldMetadataRepository.find({
|
|
...options,
|
|
where: {
|
|
...options.where,
|
|
workspaceId,
|
|
},
|
|
});
|
|
|
|
return fieldMetadata;
|
|
}
|
|
}
|