Allow app to extend object (#17116)
- exports `extendObject` utils from `twenty-sdk` - define `*.object-extension.ts` filename pattern to extend and object - handle object-extension configs in manifest
This commit is contained in:
+1
@@ -22,6 +22,7 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
|
||||
case ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
|
||||
throw new NotFoundError(exception);
|
||||
case ApplicationExceptionCode.FORBIDDEN:
|
||||
case ApplicationExceptionCode.INVALID_INPUT:
|
||||
throw new UserInputError(exception);
|
||||
default: {
|
||||
assertUnreachable(exception.code);
|
||||
|
||||
+90
@@ -5,6 +5,7 @@ import { parse } from 'path';
|
||||
import {
|
||||
ApplicationManifest,
|
||||
FieldManifest,
|
||||
ObjectExtensionManifest,
|
||||
ObjectManifest,
|
||||
RelationFieldManifest,
|
||||
RoleManifest,
|
||||
@@ -96,6 +97,14 @@ export class ApplicationSyncService {
|
||||
applicationId: application.id,
|
||||
});
|
||||
|
||||
if (manifest.objectExtensions && manifest.objectExtensions.length > 0) {
|
||||
await this.syncObjectExtensionsOrThrow({
|
||||
objectExtensionsToSync: manifest.objectExtensions,
|
||||
workspaceId,
|
||||
applicationId: application.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (manifest.serverlessFunctions.length > 0) {
|
||||
if (!isDefined(application.serverlessFunctionLayerId)) {
|
||||
throw new ApplicationException(
|
||||
@@ -772,6 +781,87 @@ export class ApplicationSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncObjectExtensionsOrThrow({
|
||||
objectExtensionsToSync,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
}: {
|
||||
objectExtensionsToSync: ObjectExtensionManifest[];
|
||||
workspaceId: string;
|
||||
applicationId: string;
|
||||
}) {
|
||||
const { flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const { idByNameSingular: objectIdByNameSingular } =
|
||||
buildObjectIdByNameMaps(flatObjectMetadataMaps);
|
||||
|
||||
for (const objectExtension of objectExtensionsToSync) {
|
||||
const { targetObject, fields } = objectExtension;
|
||||
|
||||
let targetObjectId: string | undefined;
|
||||
|
||||
if (isDefined(targetObject.nameSingular)) {
|
||||
targetObjectId = objectIdByNameSingular[targetObject.nameSingular];
|
||||
|
||||
if (!isDefined(targetObjectId)) {
|
||||
throw new ApplicationException(
|
||||
`Failed to find target object with nameSingular "${targetObject.nameSingular}" for object extension`,
|
||||
ApplicationExceptionCode.OBJECT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
} else if (isDefined(targetObject.universalIdentifier)) {
|
||||
targetObjectId =
|
||||
flatObjectMetadataMaps.idByUniversalIdentifier[
|
||||
targetObject.universalIdentifier
|
||||
];
|
||||
|
||||
if (!isDefined(targetObjectId)) {
|
||||
throw new ApplicationException(
|
||||
`Failed to find target object with universalIdentifier "${targetObject.universalIdentifier}" for object extension`,
|
||||
ApplicationExceptionCode.OBJECT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDefined(targetObjectId)) {
|
||||
throw new ApplicationException(
|
||||
'Object extension must specify either nameSingular or universalIdentifier in targetObject',
|
||||
ApplicationExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
// Sync regular fields for this extension
|
||||
await this.syncFields({
|
||||
objectId: targetObjectId,
|
||||
fieldsToSync: fields,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
});
|
||||
|
||||
// Sync relation fields for this extension
|
||||
const relationFields = fields.filter(
|
||||
(field): field is RelationFieldManifest =>
|
||||
this.isRelationFieldManifest(field),
|
||||
);
|
||||
|
||||
if (relationFields.length > 0) {
|
||||
await this.syncRelationFields({
|
||||
objectId: targetObjectId,
|
||||
relationsToSync: relationFields,
|
||||
workspaceId,
|
||||
applicationId,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async syncServerlessFunctions({
|
||||
serverlessFunctionsToSync,
|
||||
code,
|
||||
|
||||
@@ -11,6 +11,7 @@ export enum ApplicationExceptionCode {
|
||||
ENTITY_NOT_FOUND = 'ENTITY_NOT_FOUND',
|
||||
APPLICATION_NOT_FOUND = 'APPLICATION_NOT_FOUND',
|
||||
FORBIDDEN = 'FORBIDDEN',
|
||||
INVALID_INPUT = 'INVALID_INPUT',
|
||||
}
|
||||
|
||||
const getApplicationExceptionUserFriendlyMessage = (
|
||||
@@ -29,6 +30,8 @@ const getApplicationExceptionUserFriendlyMessage = (
|
||||
return msg`Application not found.`;
|
||||
case ApplicationExceptionCode.FORBIDDEN:
|
||||
return msg`You do not have permission to perform this action.`;
|
||||
case ApplicationExceptionCode.INVALID_INPUT:
|
||||
return msg`Invalid input provided.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user