1895 extensibility v1 application tokens (#16365)

First PR to implement application tokens
- add new application role in twenty-server
- move duplicated constants and types to twenty-shared
- will  add role configuration utils into twenty-sdk in another PR
This commit is contained in:
martmull
2025-12-08 10:42:46 +01:00
committed by GitHub
parent 1f9a6b067a
commit 7f1e69740a
188 changed files with 977 additions and 457 deletions
@@ -16,6 +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';
@Module({
imports: [
@@ -32,6 +36,10 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
RouteTriggerModule,
WorkspaceMigrationV2Module,
PermissionsModule,
RoleModule,
RoleTargetModule,
ObjectPermissionModule,
PermissionFlagModule,
],
providers: [
ApplicationResolver,
@@ -3,6 +3,14 @@ import { Injectable, Logger } from '@nestjs/common';
import { parse } from 'path';
import { isDefined } from 'twenty-shared/utils';
import { Sources } from 'twenty-shared/types';
import {
ApplicationManifest,
FieldManifest,
ObjectManifest,
ServerlessFunctionManifest,
ServerlessFunctionTriggerManifest,
} from 'twenty-shared/application';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import {
@@ -11,14 +19,7 @@ import {
} from 'src/engine/core-modules/application/application.exception';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import {
FieldManifest,
ObjectManifest,
ServerlessFunctionManifest,
ServerlessFunctionTriggerManifest,
} from 'src/engine/core-modules/application/types/application.types';
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
import { Sources } from 'src/engine/core-modules/file-storage/types/source.type';
import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service';
import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@@ -37,6 +38,11 @@ import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverl
import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
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';
@Injectable()
export class ApplicationSyncService {
@@ -55,6 +61,11 @@ export class ApplicationSyncService {
private readonly cronTriggerV2Service: CronTriggerV2Service,
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,
) {}
public async synchronizeFromManifest({
@@ -124,6 +135,12 @@ export class ApplicationSyncService {
let serverlessFunctionLayerId = application.serverlessFunctionLayerId;
await this.syncApplicationRole({
applicationId: application.id,
manifest,
workspaceId,
});
if (manifest.serverlessFunctions.length > 0) {
if (!isDefined(serverlessFunctionLayerId)) {
serverlessFunctionLayerId = (
@@ -161,6 +178,114 @@ export class ApplicationSyncService {
});
}
private async syncApplicationRole({
manifest,
applicationId,
workspaceId,
}: {
manifest: ApplicationManifest;
applicationId: string;
workspaceId: string;
}) {
const applicationRole = manifest.application.applicationRole;
if (
isDefined(applicationRole) &&
isDefined(applicationRole.universalIdentifier)
) {
let role = await this.roleService.getRoleByUniversalIdentifier({
universalIdentifier: applicationRole.universalIdentifier,
workspaceId,
});
if (role) {
await this.roleService.updateRole({
input: {
id: role.id,
update: {
...applicationRole,
canAccessAllTools: false,
canUpdateAllSettings: false,
canBeAssignedToAgents: false,
canBeAssignedToUsers: false,
canBeAssignedToApiKeys: false,
canBeAssignedToApplications: true,
},
},
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,
},
workspaceId,
});
}
if (
isDefined(applicationRole.objectPermissions) &&
applicationRole.objectPermissions.length > 0
) {
await this.objectPermissionService.upsertObjectPermissions({
workspaceId,
input: {
roleId: role.id,
objectPermissions: applicationRole.objectPermissions,
},
});
}
if (
isDefined(applicationRole.fieldPermissions) &&
applicationRole.fieldPermissions.length > 0
) {
await this.fieldPermissionService.upsertFieldPermissions({
workspaceId,
input: {
roleId: role.id,
fieldPermissions: applicationRole.fieldPermissions,
},
});
}
if (
isDefined(applicationRole.permissionFlags) &&
applicationRole.permissionFlags.length > 0
) {
await this.permissionService.upsertPermissionFlags({
workspaceId,
input: {
roleId: role.id,
permissionFlagKeys: applicationRole.permissionFlags,
},
});
}
}
}
private async syncFields({
objectId,
fieldsToSync,
@@ -1,6 +1,8 @@
import { UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
@@ -14,7 +16,6 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
@UseGuards(
@@ -1,16 +1,12 @@
import { ArgsType, Field } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import {
AppManifest,
PackageJson,
} from 'src/engine/core-modules/application/types/application.types';
import { ApplicationManifest, PackageJson } from 'twenty-shared/application';
@ArgsType()
export class ApplicationInput {
@Field(() => GraphQLJSON, { nullable: false })
manifest: AppManifest;
manifest: ApplicationManifest;
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
@@ -1,113 +0,0 @@
import { type FieldMetadataType } from 'twenty-shared/types';
import { type HTTPMethod } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity';
export type PackageJson = {
name: string;
license: string;
engines: {
node: string;
npm: string;
yarn: string;
};
packageManager: string;
version: string;
dependencies?: object;
devDependencies?: object;
};
type ApplicationVariable = {
universalIdentifier: string;
value?: string;
description?: string;
isSecret?: boolean;
};
type Sources = { [key: string]: string | Sources };
type Application = {
universalIdentifier: string;
displayName?: string;
description?: string;
icon?: string;
applicationVariables?: Record<string, ApplicationVariable>;
};
export type AppManifest = {
application: Application;
objects: ObjectManifest[];
serverlessFunctions: ServerlessFunctionManifest[];
sources: Sources;
};
export type ServerlessFunctionManifest = {
universalIdentifier: string;
name?: string;
description?: string;
timeoutSeconds?: number;
handlerPath: string;
handlerName: string;
triggers: ServerlessFunctionTriggerManifest[];
};
export type ServerlessFunctionTriggerManifest = (
| {
type: 'cron';
pattern: string;
}
| {
type: 'databaseEvent';
eventName: string;
}
| {
type: 'route';
path: string;
httpMethod: HTTPMethod;
isAuthRequired: boolean;
}
) & {
universalIdentifier: string;
};
export type FieldManifest = {
universalIdentifier: string;
type: FieldMetadataType;
label: string;
description?: string;
icon?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultValue?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
settings?: any;
isNullable?: boolean;
isFieldUiReadOnly?: boolean;
};
export type ObjectManifest = {
universalIdentifier: string;
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
icon?: string;
fields?: FieldManifest[];
};
interface AgentResponseFormat {
type: 'json' | 'text';
schema?: Record<string, unknown>;
}
export type AgentManifest = {
$schema?: string;
standardId: string;
name: string;
label: string;
description?: string;
icon?: string;
prompt: string;
modelId: string;
responseFormat?: AgentResponseFormat;
};