[Apps] App misc - fixes + settings permissions for apps + uploadFile (#17167)

In this PR

- handle settings permission check for applications. Until then this was
unhandled and applications could not perform actions requiring settings
permissions, even if they were granted them

- fix ties to attachment, noteTarget etc: 
When an object had their fields synchronized in the app, the system
fields created as a side-effect of the object creation (relations to
noteTarget, attachment, taskTarget, favorites, timelineActivities -
created with `isCustom: true`, not sure that is correct btw) were then
deleted because they are not declared in the app, and identified as
deletable because of `isCustom: true`.
Updating the logic to exclude system fields from the logic that detects
fields to delete.
I think this outline the confusion we have around isCustom, isSystem
etc.

- introduce uploadFile util in generated twenty client as it cannot be
handled by the client's query / mutation. I had to use this for my
invoicing app
This commit is contained in:
Marie
2026-01-18 18:41:48 +00:00
committed by GitHub
parent 6070dbf16a
commit 2c596e7b1e
9 changed files with 124 additions and 37 deletions
@@ -369,12 +369,16 @@ export class ApplicationSyncService {
private isRelationFieldManifest(
field: FieldManifest | RelationFieldManifest,
): field is RelationFieldManifest {
return field.type === FieldMetadataType.RELATION;
return this.isFieldTypeRelation(field.type);
}
private async syncFields({
private isFieldTypeRelation(type: FieldMetadataType): boolean {
return type === FieldMetadataType.RELATION;
}
private async syncFieldsWithoutRelations({
objectId,
fieldsToSync,
fieldsToSync: allFieldsToSync,
workspaceId,
applicationId,
}: {
@@ -383,33 +387,14 @@ export class ApplicationSyncService {
applicationId: string;
fieldsToSync?: (FieldManifest | RelationFieldManifest)[];
}) {
if (!isDefined(fieldsToSync)) {
if (!isDefined(allFieldsToSync)) {
return;
}
const regularFields = fieldsToSync.filter(
const fieldsToSync = allFieldsToSync.filter(
(field): field is FieldManifest => !this.isRelationFieldManifest(field),
);
await this.syncRegularFields({
objectId,
fieldsToSync: regularFields,
workspaceId,
applicationId,
});
}
private async syncRegularFields({
objectId,
fieldsToSync,
workspaceId,
applicationId,
}: {
objectId: string;
workspaceId: string;
applicationId: string;
fieldsToSync: FieldManifest[];
}) {
if (fieldsToSync.length === 0) {
return;
}
@@ -425,7 +410,10 @@ export class ApplicationSyncService {
const existingFields = Object.values(
existingFlatFieldMetadataMaps.byId,
).filter(
(field) => isDefined(field) && field.objectMetadataId === objectId,
(field) =>
isDefined(field) &&
field.objectMetadataId === objectId &&
!this.isFieldTypeRelation(field.type),
) as FlatFieldMetadata[];
const fieldsToSyncUniversalIds = fieldsToSync.map(
@@ -440,7 +428,8 @@ export class ApplicationSyncService {
(field) =>
isDefined(field.universalIdentifier) &&
!fieldsToSyncUniversalIds.includes(field.universalIdentifier) &&
field.isCustom === true,
field.isCustom === true &&
field.isSystem === false,
);
const fieldsToUpdate = existingFields.filter(
@@ -690,7 +679,7 @@ export class ApplicationSyncService {
workspaceId,
});
await this.syncFields({
await this.syncFieldsWithoutRelations({
fieldsToSync: objectToSync.fields,
objectId: objectToUpdate.id,
workspaceId,
@@ -712,8 +701,8 @@ export class ApplicationSyncService {
icon: objectToCreate.icon || undefined,
description: objectToCreate.description || undefined,
standardId: objectToCreate.universalIdentifier,
universalIdentifier: objectToCreate.universalIdentifier,
dataSourceId: dataSourceMetadata.id,
universalIdentifier: objectToCreate.universalIdentifier,
applicationId,
};
@@ -723,7 +712,7 @@ export class ApplicationSyncService {
workspaceId,
});
await this.syncFields({
await this.syncFieldsWithoutRelations({
fieldsToSync: objectToCreate.fields,
objectId: createdObject.id,
workspaceId,
@@ -837,7 +826,7 @@ export class ApplicationSyncService {
}
// Sync regular fields for this extension
await this.syncFields({
await this.syncFieldsWithoutRelations({
objectId: targetObjectId,
fieldsToSync: fields,
workspaceId,
@@ -25,6 +25,27 @@ export class ApplicationService {
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
async findApplicationRoleId(
applicationId: string,
workspaceId: string,
): Promise<string> {
const application = await this.applicationRepository.findOne({
where: { id: applicationId, workspaceId },
});
if (
!isDefined(application) ||
!isDefined(application.defaultServerlessFunctionRoleId)
) {
throw new ApplicationException(
`Could not find application ${applicationId}`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
return application.defaultServerlessFunctionRoleId;
}
async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({
workspace: workspaceInput,
workspaceId,