mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 12:02:10 +03:00
Add crud actions (#8500)
Adding update / delete / find actions Update and delete are not really different than creation. Find uses the same logique as for graphql filters. Expected formats are: Filter ``` { "and": [ { "name": { "eq": "salut" } }, { "employees": { "eq": "0" } } ] } ``` Order `[ { "name": 'AscNullsFirst' } ]`
This commit is contained in:
parent
736635a94b
commit
a2a272fed4
@ -54,7 +54,7 @@ export class GraphqlQueryParser {
|
||||
|
||||
public applyDeletedAtToBuilder(
|
||||
queryBuilder: SelectQueryBuilder<any>,
|
||||
recordFilter: ObjectRecordFilter,
|
||||
recordFilter: Partial<ObjectRecordFilter>,
|
||||
): SelectQueryBuilder<any> {
|
||||
if (this.checkForDeletedAtFilter(recordFilter)) {
|
||||
queryBuilder.withDeleted();
|
||||
|
@ -10,10 +10,12 @@ import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql
|
||||
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||
import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service';
|
||||
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record';
|
||||
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/utils/generate-fake-object-record-event';
|
||||
import { WorkflowSendEmailStepOutputSchema } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/send-email.workflow-action';
|
||||
import { WorkflowRecordCRUDType } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
import {
|
||||
WorkflowAction,
|
||||
WorkflowActionType,
|
||||
@ -44,7 +46,7 @@ export class WorkflowBuilderWorkspaceService {
|
||||
|
||||
switch (stepType) {
|
||||
case WorkflowTriggerType.DATABASE_EVENT: {
|
||||
return await this.computeDatabaseEventTriggerOutputSchema({
|
||||
return this.computeDatabaseEventTriggerOutputSchema({
|
||||
eventName: step.settings.eventName,
|
||||
workspaceId,
|
||||
objectMetadataRepository: this.objectMetadataRepository,
|
||||
@ -57,7 +59,7 @@ export class WorkflowBuilderWorkspaceService {
|
||||
return {};
|
||||
}
|
||||
|
||||
return await this.computeRecordOutputSchema({
|
||||
return this.computeRecordOutputSchema({
|
||||
objectType,
|
||||
workspaceId,
|
||||
objectMetadataRepository: this.objectMetadataRepository,
|
||||
@ -70,7 +72,7 @@ export class WorkflowBuilderWorkspaceService {
|
||||
const { serverlessFunctionId, serverlessFunctionVersion } =
|
||||
step.settings.input;
|
||||
|
||||
return await this.computeCodeActionOutputSchema({
|
||||
return this.computeCodeActionOutputSchema({
|
||||
serverlessFunctionId,
|
||||
serverlessFunctionVersion,
|
||||
workspaceId,
|
||||
@ -79,8 +81,9 @@ export class WorkflowBuilderWorkspaceService {
|
||||
});
|
||||
}
|
||||
case WorkflowActionType.RECORD_CRUD:
|
||||
return await this.computeRecordOutputSchema({
|
||||
return this.computeRecordCrudOutputSchema({
|
||||
objectType: step.settings.input.objectName,
|
||||
operationType: step.settings.input.type,
|
||||
workspaceId,
|
||||
objectMetadataRepository: this.objectMetadataRepository,
|
||||
});
|
||||
@ -122,6 +125,34 @@ export class WorkflowBuilderWorkspaceService {
|
||||
);
|
||||
}
|
||||
|
||||
private async computeRecordCrudOutputSchema<Entity>({
|
||||
objectType,
|
||||
operationType,
|
||||
workspaceId,
|
||||
objectMetadataRepository,
|
||||
}: {
|
||||
objectType: string;
|
||||
operationType: string;
|
||||
workspaceId: string;
|
||||
objectMetadataRepository: Repository<ObjectMetadataEntity>;
|
||||
}) {
|
||||
const recordOutputSchema = await this.computeRecordOutputSchema<Entity>({
|
||||
objectType,
|
||||
workspaceId,
|
||||
objectMetadataRepository,
|
||||
});
|
||||
|
||||
if (operationType === WorkflowRecordCRUDType.READ) {
|
||||
return {
|
||||
first: recordOutputSchema,
|
||||
last: recordOutputSchema,
|
||||
totalCount: generateFakeValue('number'),
|
||||
};
|
||||
}
|
||||
|
||||
return recordOutputSchema;
|
||||
}
|
||||
|
||||
private async computeRecordOutputSchema<Entity>({
|
||||
objectType,
|
||||
workspaceId,
|
||||
|
@ -0,0 +1,13 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class RecordCRUDActionException extends CustomException {
|
||||
code: RecordCRUDActionExceptionCode;
|
||||
constructor(message: string, code: RecordCRUDActionExceptionCode) {
|
||||
super(message, code);
|
||||
}
|
||||
}
|
||||
|
||||
export enum RecordCRUDActionExceptionCode {
|
||||
INVALID_REQUEST = 'INVALID_REQUEST',
|
||||
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { RecordCRUDWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/record-crud.workflow-action';
|
||||
|
||||
@Module({
|
||||
providers: [RecordCRUDWorkflowAction],
|
||||
imports: [WorkspaceCacheStorageModule],
|
||||
providers: [RecordCRUDWorkflowAction, ScopedWorkspaceContextFactory],
|
||||
exports: [RecordCRUDWorkflowAction],
|
||||
})
|
||||
export class RecordCRUDActionModule {}
|
||||
|
@ -1,18 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Entity } from '@microsoft/microsoft-graph-types';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
import {
|
||||
ObjectRecordFilter,
|
||||
ObjectRecordOrderBy,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant';
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import {
|
||||
RecordCRUDActionException,
|
||||
RecordCRUDActionExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception';
|
||||
import {
|
||||
WorkflowCreateRecordActionInput,
|
||||
WorkflowDeleteRecordActionInput,
|
||||
WorkflowReadRecordActionInput,
|
||||
WorkflowRecordCRUDActionInput,
|
||||
WorkflowRecordCRUDType,
|
||||
WorkflowUpdateRecordActionInput,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-result.type';
|
||||
|
||||
@Injectable()
|
||||
export class RecordCRUDWorkflowAction implements WorkflowAction {
|
||||
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
workflowActionInput: WorkflowRecordCRUDActionInput,
|
||||
@ -20,9 +47,16 @@ export class RecordCRUDWorkflowAction implements WorkflowAction {
|
||||
switch (workflowActionInput.type) {
|
||||
case WorkflowRecordCRUDType.CREATE:
|
||||
return this.createRecord(workflowActionInput);
|
||||
case WorkflowRecordCRUDType.DELETE:
|
||||
return this.deleteRecord(workflowActionInput);
|
||||
case WorkflowRecordCRUDType.UPDATE:
|
||||
return this.updateRecord(workflowActionInput);
|
||||
case WorkflowRecordCRUDType.READ:
|
||||
return this.findRecords(workflowActionInput);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown record operation type: ${workflowActionInput.type}`,
|
||||
throw new RecordCRUDActionException(
|
||||
`Unknown record operation type`,
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -38,8 +72,213 @@ export class RecordCRUDWorkflowAction implements WorkflowAction {
|
||||
workflowActionInput.objectRecord,
|
||||
);
|
||||
|
||||
const createdObjectRecord = await repository.save(objectRecord);
|
||||
await repository.save(objectRecord);
|
||||
|
||||
return { result: createdObjectRecord };
|
||||
return {
|
||||
result: objectRecord,
|
||||
};
|
||||
}
|
||||
|
||||
private async updateRecord(
|
||||
workflowActionInput: WorkflowUpdateRecordActionInput,
|
||||
): Promise<WorkflowActionResult> {
|
||||
const repository = await this.twentyORMManager.getRepository(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
const objectRecord = await repository.findOne({
|
||||
where: {
|
||||
id: workflowActionInput.objectRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectRecord) {
|
||||
throw new RecordCRUDActionException(
|
||||
`Failed to update: Record ${workflowActionInput.objectName} with id ${workflowActionInput.objectRecordId} not found`,
|
||||
RecordCRUDActionExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await repository.update(workflowActionInput.objectRecordId, {
|
||||
...workflowActionInput.objectRecord,
|
||||
});
|
||||
|
||||
return {
|
||||
result: {
|
||||
...objectRecord,
|
||||
...workflowActionInput.objectRecord,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async deleteRecord(
|
||||
workflowActionInput: WorkflowDeleteRecordActionInput,
|
||||
): Promise<WorkflowActionResult> {
|
||||
const repository = await this.twentyORMManager.getRepository(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
const objectRecord = await repository.findOne({
|
||||
where: {
|
||||
id: workflowActionInput.objectRecordId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!objectRecord) {
|
||||
throw new RecordCRUDActionException(
|
||||
`Failed to delete: Record ${workflowActionInput.objectName} with id ${workflowActionInput.objectRecordId} not found`,
|
||||
RecordCRUDActionExceptionCode.RECORD_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await repository.update(workflowActionInput.objectRecordId, {
|
||||
deletedAt: new Date(),
|
||||
});
|
||||
|
||||
return {
|
||||
result: objectRecord,
|
||||
};
|
||||
}
|
||||
|
||||
private async findRecords(
|
||||
workflowActionInput: WorkflowReadRecordActionInput,
|
||||
): Promise<WorkflowActionResult> {
|
||||
const repository = await this.twentyORMManager.getRepository(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to read: Workspace ID is required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const currentCacheVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
||||
|
||||
if (currentCacheVersion === undefined) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to read: Metadata cache version not found',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
workspaceId,
|
||||
currentCacheVersion,
|
||||
);
|
||||
|
||||
if (!objectMetadataMaps) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to read: Object metadata collection not found',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
objectMetadataMaps.byNameSingular[workflowActionInput.objectName];
|
||||
|
||||
if (!objectMetadataItemWithFieldsMaps) {
|
||||
throw new RecordCRUDActionException(
|
||||
`Failed to read: Object ${workflowActionInput.objectName} not found`,
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldsMaps.fieldsByName,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
const objectRecords = await this.getObjectRecords(
|
||||
workflowActionInput,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadataMaps,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
);
|
||||
|
||||
const totalCount = await this.getTotalCount(
|
||||
workflowActionInput,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
);
|
||||
|
||||
return {
|
||||
result: {
|
||||
first: objectRecords[0],
|
||||
last: objectRecords[objectRecords.length - 1],
|
||||
totalCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getObjectRecords<T extends ObjectLiteral>(
|
||||
workflowActionInput: WorkflowReadRecordActionInput,
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps,
|
||||
objectMetadataMaps: ObjectMetadataMaps,
|
||||
repository: WorkspaceRepository<T>,
|
||||
graphqlQueryParser: GraphqlQueryParser,
|
||||
) {
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
workflowActionInput.objectName,
|
||||
workflowActionInput.filter ?? ({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
const orderByWithIdCondition = [
|
||||
...(workflowActionInput.orderBy ?? []),
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
] as ObjectRecordOrderBy;
|
||||
|
||||
const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
|
||||
withFilterQueryBuilder,
|
||||
orderByWithIdCondition,
|
||||
workflowActionInput.objectName,
|
||||
false,
|
||||
);
|
||||
|
||||
const nonFormattedObjectRecords = await withOrderByQueryBuilder
|
||||
.take(workflowActionInput.limit ?? QUERY_MAX_RECORDS)
|
||||
.getMany();
|
||||
|
||||
return formatResult(
|
||||
nonFormattedObjectRecords,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
}
|
||||
|
||||
private async getTotalCount(
|
||||
workflowActionInput: WorkflowReadRecordActionInput,
|
||||
repository: WorkspaceRepository<Entity>,
|
||||
graphqlQueryParser: GraphqlQueryParser,
|
||||
) {
|
||||
const countQueryBuilder = repository.createQueryBuilder(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
countQueryBuilder,
|
||||
workflowActionInput.objectName,
|
||||
workflowActionInput.filter ?? ({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
const withDeletedCountQueryBuilder =
|
||||
graphqlQueryParser.applyDeletedAtToBuilder(
|
||||
withFilterCountQueryBuilder,
|
||||
workflowActionInput.filter
|
||||
? workflowActionInput.filter
|
||||
: ({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
return withDeletedCountQueryBuilder.getCount();
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,15 @@
|
||||
import {
|
||||
ObjectRecordFilter,
|
||||
ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
type ObjectRecord = Record<string, any>;
|
||||
|
||||
export enum WorkflowRecordCRUDType {
|
||||
CREATE = 'create',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
READ = 'read',
|
||||
}
|
||||
|
||||
export type WorkflowCreateRecordActionInput = {
|
||||
@ -25,7 +31,16 @@ export type WorkflowDeleteRecordActionInput = {
|
||||
objectRecordId: string;
|
||||
};
|
||||
|
||||
export type WorkflowReadRecordActionInput = {
|
||||
type: WorkflowRecordCRUDType.READ;
|
||||
objectName: string;
|
||||
filter?: Partial<ObjectRecordFilter>;
|
||||
orderBy?: Partial<ObjectRecordOrderBy>;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type WorkflowRecordCRUDActionInput =
|
||||
| WorkflowCreateRecordActionInput
|
||||
| WorkflowUpdateRecordActionInput
|
||||
| WorkflowDeleteRecordActionInput;
|
||||
| WorkflowDeleteRecordActionInput
|
||||
| WorkflowReadRecordActionInput;
|
||||
|
Loading…
Reference in New Issue
Block a user