1895 extensibility v1 application tokens 3 (#16504)

- moves applicationRoleId to application entity
- add new `APPLICATION` FieldActorSource and `APPLICATION`
JwtTokenTypeEnum value
- create a new token with applicationId when executing a function
- when applicationId is in token, check for application.defaultRole
permissions
-use twenty-shared types in `twenty-sdk/application`
- create a new import from generate called "Twenty" that you can use
directly without having to set TWENTY_API_KEY AND TWENTY_API_URL (keep
metadata or core parameter only)
- provide to serverless unique one time BEARER TOKEN to run it

Result
<img width="977" height="566" alt="image"
src="https://github.com/user-attachments/assets/e78428a0-5b13-4975-aa13-58ee3b32450c"
/>

<img width="910" height="596" alt="image"
src="https://github.com/user-attachments/assets/6ec72bf5-7655-4093-a45e-ad269595a324"
/>

<img width="741" height="568" alt="image"
src="https://github.com/user-attachments/assets/7683944c-fd79-4417-8fb2-8e4815cc112f"
/>
This commit is contained in:
martmull
2025-12-15 17:44:23 +01:00
committed by GitHub
parent e33f18bfa8
commit e289f3056e
103 changed files with 1427 additions and 512 deletions
@@ -16,10 +16,10 @@ import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serve
import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
@Module({
imports: [
@@ -37,9 +37,9 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
WorkspaceMigrationV2Module,
PermissionsModule,
RoleModule,
RoleTargetModule,
ObjectPermissionModule,
PermissionFlagModule,
WorkflowCommonModule,
],
providers: [
ApplicationResolver,
@@ -3,11 +3,12 @@ import { Injectable, Logger } from '@nestjs/common';
import { parse } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { Sources } from 'twenty-shared/types';
import { HTTPMethod, Sources } from 'twenty-shared/types';
import {
ApplicationManifest,
FieldManifest,
ObjectManifest,
RoleManifest,
ServerlessFunctionManifest,
ServerlessFunctionTriggerManifest,
} from 'twenty-shared/application';
@@ -39,10 +40,10 @@ import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-f
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
@Injectable()
export class ApplicationSyncService {
@@ -62,7 +63,6 @@ export class ApplicationSyncService {
private readonly routeTriggerV2Service: RouteTriggerV2Service,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly roleService: RoleService,
private readonly roleTargetService: RoleTargetService,
private readonly objectPermissionService: ObjectPermissionService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly permissionService: PermissionFlagService,
@@ -106,6 +106,12 @@ export class ApplicationSyncService {
});
}
await this.syncRoles({
manifest,
workspaceId,
applicationId: application.id,
});
this.logger.log('✅ Application sync from manifest completed');
}
@@ -130,17 +136,12 @@ export class ApplicationSyncService {
version: packageJson.version,
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
serverlessFunctionLayerId: null,
defaultServerlessFunctionRoleId: null,
workspaceId,
}));
let serverlessFunctionLayerId = application.serverlessFunctionLayerId;
await this.syncApplicationRole({
applicationId: application.id,
manifest,
workspaceId,
});
if (manifest.serverlessFunctions.length > 0) {
if (!isDefined(serverlessFunctionLayerId)) {
serverlessFunctionLayerId = (
@@ -175,114 +176,177 @@ export class ApplicationSyncService {
description: manifest.application.description,
version: packageJson.version,
serverlessFunctionLayerId,
defaultServerlessFunctionRoleId: null,
});
}
private async syncApplicationRole({
private async syncRoles({
manifest,
applicationId,
workspaceId,
applicationId,
}: {
manifest: ApplicationManifest;
applicationId: string;
workspaceId: string;
applicationId: string;
}) {
const applicationRole = manifest.application.applicationRole;
let defaultServerlessFunctionRoleId: string | null = null;
if (
isDefined(applicationRole) &&
isDefined(applicationRole.universalIdentifier)
) {
let role = await this.roleService.getRoleByUniversalIdentifier({
universalIdentifier: applicationRole.universalIdentifier,
for (const role of manifest.roles ?? []) {
let existingRole = await this.roleService.getRoleByUniversalIdentifier({
universalIdentifier: role.universalIdentifier,
workspaceId,
});
if (role) {
if (existingRole) {
await this.roleService.updateRole({
input: {
id: role.id,
update: {
...applicationRole,
canAccessAllTools: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
canBeAssignedToApplications: true,
},
id: existingRole.id,
update: role,
},
workspaceId,
});
} else {
role = await this.roleService.createRole({
input: {
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: false,
canSoftDeleteAllObjectRecords: false,
canDestroyAllObjectRecords: false,
...applicationRole,
canAccessAllTools: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
canBeAssignedToApplications: true,
},
workspaceId,
applicationId: applicationId,
});
await this.roleTargetService.create({
createRoleTargetInput: {
roleId: role.id,
applicationId: applicationId,
universalIdentifier: role.universalIdentifier,
targetMetadataForeignKey: 'targetApplicationId',
targetId: applicationId,
},
existingRole = await this.roleService.createRole({
input: role,
workspaceId,
applicationId,
});
}
await this.syncApplicationRolePermissions({
role,
workspaceId,
roleId: existingRole.id,
});
if (
isDefined(applicationRole.objectPermissions) &&
applicationRole.objectPermissions.length > 0
existingRole.universalIdentifier ===
manifest.application.functionRoleUniversalIdentifier
) {
defaultServerlessFunctionRoleId = existingRole.id;
}
}
if (isDefined(defaultServerlessFunctionRoleId)) {
await this.applicationService.update(applicationId, {
defaultServerlessFunctionRoleId: defaultServerlessFunctionRoleId,
});
}
}
private async syncApplicationRolePermissions({
role,
workspaceId,
roleId,
}: {
role: RoleManifest;
workspaceId: string;
roleId: string;
}) {
if (
(role.objectPermissions ?? []).length > 0 ||
(role.fieldPermissions ?? []).length > 0
) {
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'],
},
);
const { idByNameSingular: objectIdByNameSingular } =
buildObjectIdByNameMaps(flatObjectMetadataMaps);
const formattedObjectPermissions = role.objectPermissions
?.map((perm) => ({
...perm,
objectMetadataId: isDefined(perm.objectNameSingular)
? objectIdByNameSingular[perm.objectNameSingular]
: isDefined(perm.objectUniversalIdentifier)
? flatObjectMetadataMaps.idByUniversalIdentifier[
perm.objectUniversalIdentifier
]
: undefined,
}))
.filter((perm): perm is typeof perm & { objectMetadataId: string } =>
isDefined(perm.objectMetadataId),
);
if (isDefined(formattedObjectPermissions)) {
await this.objectPermissionService.upsertObjectPermissions({
workspaceId,
input: {
roleId: role.id,
objectPermissions: applicationRole.objectPermissions,
roleId,
objectPermissions: formattedObjectPermissions,
},
});
}
if (
isDefined(applicationRole.fieldPermissions) &&
applicationRole.fieldPermissions.length > 0
) {
const formattedFieldPermissions = role?.fieldPermissions
?.map((perm) => {
const objectMetadataId = isDefined(perm.objectNameSingular)
? objectIdByNameSingular[perm.objectNameSingular]
: isDefined(perm.objectUniversalIdentifier)
? flatObjectMetadataMaps.idByUniversalIdentifier[
perm.objectUniversalIdentifier
]
: undefined;
const fieldMetadataId = isDefined(objectMetadataId)
? isDefined(perm.fieldName)
? Object.values(flatFieldMetadataMaps.byId).find(
(flatField) =>
isDefined(flatField) &&
flatField.objectMetadataId === objectMetadataId &&
flatField.name === perm.fieldName,
)?.id
: isDefined(perm.fieldUniversalIdentifier)
? Object.values(flatFieldMetadataMaps.byId).find(
(flatField) =>
isDefined(flatField) &&
flatField.objectMetadataId === objectMetadataId &&
flatField.universalIdentifier ===
perm.fieldUniversalIdentifier,
)?.id
: undefined
: undefined;
return {
...perm,
objectMetadataId,
fieldMetadataId,
};
})
.filter(
(
perm,
): perm is typeof perm & {
objectMetadataId: string;
fieldMetadataId: string;
} =>
isDefined(perm.objectMetadataId) && isDefined(perm.fieldMetadataId),
);
if (isDefined(formattedFieldPermissions)) {
await this.fieldPermissionService.upsertFieldPermissions({
workspaceId,
input: {
roleId: role.id,
fieldPermissions: applicationRole.fieldPermissions,
roleId,
fieldPermissions: formattedFieldPermissions,
},
});
}
}
if (
isDefined(applicationRole.permissionFlags) &&
applicationRole.permissionFlags.length > 0
) {
await this.permissionService.upsertPermissionFlags({
workspaceId,
input: {
roleId: role.id,
permissionFlagKeys: applicationRole.permissionFlags,
},
});
}
if (isDefined(role?.permissionFlags) && role.permissionFlags.length > 0) {
await this.permissionService.upsertPermissionFlags({
workspaceId,
input: {
roleId,
permissionFlagKeys: role.permissionFlags,
},
});
}
}
@@ -1036,7 +1100,7 @@ export class ApplicationSyncService {
id: triggerToUpdate.id,
update: {
path: triggerToSync.path,
httpMethod: triggerToSync.httpMethod,
httpMethod: triggerToSync.httpMethod as HTTPMethod,
isAuthRequired: triggerToSync.isAuthRequired,
},
};
@@ -1054,7 +1118,7 @@ export class ApplicationSyncService {
const createRouteTriggerInput = {
path: triggerToCreate.path,
httpMethod: triggerToCreate.httpMethod,
httpMethod: triggerToCreate.httpMethod as HTTPMethod,
isAuthRequired: triggerToCreate.isAuthRequired,
serverlessFunctionId,
};
@@ -1,4 +1,4 @@
import { ObjectType } from '@nestjs/graphql';
import { Field, ObjectType } from '@nestjs/graphql';
import {
Column,
@@ -14,6 +14,7 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
@@ -60,6 +61,12 @@ export class ApplicationEntity {
@Column({ nullable: true, type: 'uuid' })
serverlessFunctionLayerId: string | null;
@Column({ nullable: true, type: 'uuid' })
defaultServerlessFunctionRoleId: string | null;
@Field(() => RoleDTO, { nullable: true })
defaultServerlessFunctionRole: RoleDTO | null;
@Column({ nullable: false, type: 'boolean', default: true })
canBeUninstalled: boolean;
@@ -13,6 +13,7 @@ import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/applicatio
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
@ObjectType('Application')
export class ApplicationDTO {
@@ -43,6 +44,15 @@ export class ApplicationDTO {
@IsBoolean()
canBeUninstalled: boolean;
@IsOptional()
@IsString()
@Field({ nullable: true })
defaultServerlessFunctionRoleId?: string;
@IsOptional()
@Field(() => RoleDTO, { nullable: true })
defaultServerlessFunctionRole?: RoleDTO;
@Field(() => [AgentDTO])
agents?: AgentDTO[];