Files
twenty/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts
T
Marie 0001c2e7d0 [Apps] Apps marketplace (first draft) (#17562)
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
2026-01-30 17:32:32 +00:00

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;
}
}