Basic data enrichment (#3023)
* Add Enrich to frontend * Naive backend implementation * Add work email check * Rename Enrich to Quick Action * Refactor logic to a separate service * Refacto to separate IntelligenceService * Small fixes * Missing Break statement * Address PR comments * Create company interface * Improve edge case handling * Use httpService instead of Axios * Fix server tests
This commit is contained in:
@ -80,8 +80,14 @@ import OptionTable from '@site/src/theme/OptionTable'
|
|||||||
['SENTRY_DSN', 'https://xxx@xxx.ingest.sentry.io/xxx', 'The sentry logging endpoint used if sentry logging driver is selected'],
|
['SENTRY_DSN', 'https://xxx@xxx.ingest.sentry.io/xxx', 'The sentry logging endpoint used if sentry logging driver is selected'],
|
||||||
]}></OptionTable>
|
]}></OptionTable>
|
||||||
|
|
||||||
### Support
|
|
||||||
|
|
||||||
|
### Data enrichment and AI
|
||||||
|
<OptionTable options={[
|
||||||
|
['OPENROUTER_API_KEY', '', "The API key for openrouter.ai, an abstraction layer over models from Mistral, OpenAI and more"]
|
||||||
|
]}></OptionTable>
|
||||||
|
|
||||||
|
|
||||||
|
### Support Chat
|
||||||
|
|
||||||
<OptionTable options={[
|
<OptionTable options={[
|
||||||
['SUPPORT_DRIVER', 'front', "Support driver ('front' or 'none')"],
|
['SUPPORT_DRIVER', 'front', "Support driver ('front' or 'none')"],
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
|||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||||
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
import { useGenerateCreateManyRecordMutation } from '@/object-record/hooks/useGenerateCreateManyRecordMutation';
|
||||||
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
import { useGenerateCreateOneRecordMutation } from '@/object-record/hooks/useGenerateCreateOneRecordMutation';
|
||||||
|
import { useGenerateExecuteQuickActionOnOneRecordMutation } from '@/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation';
|
||||||
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
|
import { useGenerateFindManyRecordsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsQuery';
|
||||||
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
import { useGenerateFindOneRecordQuery } from '@/object-record/hooks/useGenerateFindOneRecordQuery';
|
||||||
import { useGenerateUpdateOneRecordMutation } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
import { useGenerateUpdateOneRecordMutation } from '@/object-record/hooks/useGenerateUpdateOneRecordMutation';
|
||||||
@ -106,6 +107,11 @@ export const useObjectMetadataItem = (
|
|||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const executeQuickActionOnOneRecordMutation =
|
||||||
|
useGenerateExecuteQuickActionOnOneRecordMutation({
|
||||||
|
objectMetadataItem,
|
||||||
|
});
|
||||||
|
|
||||||
const labelIdentifierFieldMetadataId = objectMetadataItem.fields.find(
|
const labelIdentifierFieldMetadataId = objectMetadataItem.fields.find(
|
||||||
({ name }) => name === 'name',
|
({ name }) => name === 'name',
|
||||||
)?.id;
|
)?.id;
|
||||||
@ -123,6 +129,7 @@ export const useObjectMetadataItem = (
|
|||||||
createOneRecordMutation,
|
createOneRecordMutation,
|
||||||
updateOneRecordMutation,
|
updateOneRecordMutation,
|
||||||
deleteOneRecordMutation,
|
deleteOneRecordMutation,
|
||||||
|
executeQuickActionOnOneRecordMutation,
|
||||||
createManyRecordsMutation,
|
createManyRecordsMutation,
|
||||||
mapToObjectRecordIdentifier,
|
mapToObjectRecordIdentifier,
|
||||||
getObjectOrderByField,
|
getObjectOrderByField,
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
type useExecuteQuickActionOnOneRecordProps = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExecuteQuickActionOnOneRecord = <T>({
|
||||||
|
objectNameSingular,
|
||||||
|
}: useExecuteQuickActionOnOneRecordProps) => {
|
||||||
|
const {
|
||||||
|
objectMetadataItem,
|
||||||
|
executeQuickActionOnOneRecordMutation,
|
||||||
|
findManyRecordsQuery,
|
||||||
|
} = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const executeQuickActionOnOneRecord = useCallback(
|
||||||
|
async (idToExecuteQuickActionOn: string) => {
|
||||||
|
const executeQuickActionOnRecord = await apolloClient.mutate({
|
||||||
|
mutation: executeQuickActionOnOneRecordMutation,
|
||||||
|
variables: {
|
||||||
|
idToExecuteQuickActionOn,
|
||||||
|
},
|
||||||
|
refetchQueries: [getOperationName(findManyRecordsQuery) ?? ''],
|
||||||
|
});
|
||||||
|
|
||||||
|
return executeQuickActionOnRecord.data[
|
||||||
|
`executeQuickActionOn${capitalize(objectMetadataItem.nameSingular)}`
|
||||||
|
] as T;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
apolloClient,
|
||||||
|
executeQuickActionOnOneRecordMutation,
|
||||||
|
findManyRecordsQuery,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeQuickActionOnOneRecord,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
import { useMapFieldMetadataToGraphQLQuery } from '@/object-metadata/hooks/useMapFieldMetadataToGraphQLQuery';
|
||||||
|
import { EMPTY_MUTATION } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getExecuteQuickActionOnOneRecordMutationGraphQLField = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: string;
|
||||||
|
}) => {
|
||||||
|
return `executeQuickActionOn${capitalize(objectNameSingular)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGenerateExecuteQuickActionOnOneRecordMutation = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
}: {
|
||||||
|
objectMetadataItem: ObjectMetadataItem;
|
||||||
|
}) => {
|
||||||
|
const mapFieldMetadataToGraphQLQuery = useMapFieldMetadataToGraphQLQuery();
|
||||||
|
|
||||||
|
if (!objectMetadataItem) {
|
||||||
|
return EMPTY_MUTATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular);
|
||||||
|
|
||||||
|
const graphQLFieldForExecuteQuickActionOnOneRecordMutation =
|
||||||
|
getExecuteQuickActionOnOneRecordMutationGraphQLField({
|
||||||
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
return gql`
|
||||||
|
mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: ID!) {
|
||||||
|
${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) {
|
||||||
|
id
|
||||||
|
${objectMetadataItem.fields
|
||||||
|
.map((field) => mapFieldMetadataToGraphQLQuery(field))
|
||||||
|
.join('\n')}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
@ -5,14 +5,21 @@ import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
|
|||||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||||
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
|
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
|
||||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||||
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
|
||||||
import { selectedRowIdsSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsSelector';
|
import { selectedRowIdsSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsSelector';
|
||||||
import { IconHeart, IconHeartOff, IconTrash } from '@/ui/display/icon';
|
import {
|
||||||
|
IconHeart,
|
||||||
|
IconHeartOff,
|
||||||
|
IconTrash,
|
||||||
|
IconWand,
|
||||||
|
} from '@/ui/display/icon';
|
||||||
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
|
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
|
||||||
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
|
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
|
||||||
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
|
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
|
||||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||||
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
|
||||||
type useRecordTableContextMenuEntriesProps = {
|
type useRecordTableContextMenuEntriesProps = {
|
||||||
recordTableScopeId?: string;
|
recordTableScopeId?: string;
|
||||||
@ -68,6 +75,10 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const handleDeleteClick = useRecoilCallback(
|
const handleDeleteClick = useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
async () => {
|
async () => {
|
||||||
@ -85,6 +96,27 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
[deleteOneRecord, resetTableRowSelection],
|
[deleteOneRecord, resetTableRowSelection],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleExecuteQuickActionOnClick = useRecoilCallback(
|
||||||
|
({ snapshot }) =>
|
||||||
|
async () => {
|
||||||
|
const rowIdsToExecuteQuickActionOn = snapshot
|
||||||
|
.getLoadable(selectedRowIdsSelector)
|
||||||
|
.getValue();
|
||||||
|
|
||||||
|
resetTableRowSelection();
|
||||||
|
await Promise.all(
|
||||||
|
rowIdsToExecuteQuickActionOn.map(async (rowId) => {
|
||||||
|
await executeQuickActionOnOneRecord(rowId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[executeQuickActionOnOneRecord, resetTableRowSelection],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_QUICK_ACTIONS_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setContextMenuEntries: useCallback(() => {
|
setContextMenuEntries: useCallback(() => {
|
||||||
const selectedRowId =
|
const selectedRowId =
|
||||||
@ -143,6 +175,15 @@ export const useRecordTableContextMenuEntries = (
|
|||||||
// Icon: IconNotes,
|
// Icon: IconNotes,
|
||||||
// onClick: () => {},
|
// onClick: () => {},
|
||||||
// },
|
// },
|
||||||
|
...(dataExecuteQuickActionOnmentEnabled
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: 'Quick Action',
|
||||||
|
Icon: IconWand,
|
||||||
|
onClick: () => handleExecuteQuickActionOnClick(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
Icon: IconTrash,
|
Icon: IconTrash,
|
||||||
|
|||||||
@ -107,6 +107,7 @@ export {
|
|||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
IconVideo,
|
IconVideo,
|
||||||
|
IconWand,
|
||||||
IconWorld,
|
IconWorld,
|
||||||
IconX,
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
"@graphql-tools/schema": "^10.0.0",
|
"@graphql-tools/schema": "^10.0.0",
|
||||||
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch",
|
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch",
|
||||||
"@nestjs/apollo": "^11.0.5",
|
"@nestjs/apollo": "^11.0.5",
|
||||||
|
"@nestjs/axios": "^3.0.1",
|
||||||
"@nestjs/common": "^9.0.0",
|
"@nestjs/common": "^9.0.0",
|
||||||
"@nestjs/config": "^2.3.2",
|
"@nestjs/config": "^2.3.2",
|
||||||
"@nestjs/core": "^9.0.0",
|
"@nestjs/core": "^9.0.0",
|
||||||
@ -59,7 +60,7 @@
|
|||||||
"@types/lodash.merge": "^4.6.7",
|
"@types/lodash.merge": "^4.6.7",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"apollo-server-express": "^3.12.0",
|
"apollo-server-express": "^3.12.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.6.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"bullmq": "^4.14.0",
|
"bullmq": "^4.14.0",
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
|
||||||
import { AnalyticsService } from './analytics.service';
|
import { AnalyticsService } from './analytics.service';
|
||||||
import { AnalyticsResolver } from './analytics.resolver';
|
import { AnalyticsResolver } from './analytics.resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [AnalyticsResolver, AnalyticsService],
|
providers: [AnalyticsResolver, AnalyticsService],
|
||||||
|
imports: [
|
||||||
|
HttpModule.register({
|
||||||
|
baseURL: 'https://t.twenty.com/api/v1/s2s',
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AnalyticsModule {}
|
export class AnalyticsModule {}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
@ -17,6 +18,10 @@ describe('AnalyticsResolver', () => {
|
|||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HttpService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
@ -15,6 +16,10 @@ describe('AnalyticsService', () => {
|
|||||||
provide: EnvironmentService,
|
provide: EnvironmentService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HttpService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
|
||||||
|
|
||||||
import { anonymize } from 'src/utils/anonymize';
|
import { anonymize } from 'src/utils/anonymize';
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
@ -11,13 +10,10 @@ import { CreateAnalyticsInput } from './dto/create-analytics.input';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AnalyticsService {
|
export class AnalyticsService {
|
||||||
private readonly httpService: AxiosInstance;
|
constructor(
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
constructor(private readonly environmentService: EnvironmentService) {
|
private readonly httpService: HttpService,
|
||||||
this.httpService = axios.create({
|
) {}
|
||||||
baseURL: 'https://t.twenty.com/api/v1/s2s',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
createEventInput: CreateAnalyticsInput,
|
createEventInput: CreateAnalyticsInput,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
|
||||||
import { ApiRestController } from 'src/core/api-rest/api-rest.controller';
|
import { ApiRestController } from 'src/core/api-rest/api-rest.controller';
|
||||||
import { ApiRestService } from 'src/core/api-rest/api-rest.service';
|
import { ApiRestService } from 'src/core/api-rest/api-rest.service';
|
||||||
@ -6,7 +7,7 @@ import { ApiRestQueryBuilderModule } from 'src/core/api-rest/api-rest-query-buil
|
|||||||
import { AuthModule } from 'src/core/auth/auth.module';
|
import { AuthModule } from 'src/core/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ApiRestQueryBuilderModule, AuthModule],
|
imports: [ApiRestQueryBuilderModule, AuthModule, HttpModule],
|
||||||
controllers: [ApiRestController],
|
controllers: [ApiRestController],
|
||||||
providers: [ApiRestService],
|
providers: [ApiRestService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
|
||||||
import { ApiRestService } from 'src/core/api-rest/api-rest.service';
|
import { ApiRestService } from 'src/core/api-rest/api-rest.service';
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
@ -24,6 +25,10 @@ describe('ApiRestService', () => {
|
|||||||
provide: TokenService,
|
provide: TokenService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HttpService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
@ -15,6 +15,7 @@ export class ApiRestService {
|
|||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly apiRestQueryBuilderFactory: ApiRestQueryBuilderFactory,
|
private readonly apiRestQueryBuilderFactory: ApiRestQueryBuilderFactory,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async callGraphql(
|
async callGraphql(
|
||||||
@ -26,7 +27,7 @@ export class ApiRestService {
|
|||||||
`${request.protocol}://${request.get('host')}`;
|
`${request.protocol}://${request.get('host')}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await axios.post(`${baseUrl}/graphql`, data, {
|
return await this.httpService.axiosRef.post(`${baseUrl}/graphql`, data, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: request.headers.authorization,
|
Authorization: request.headers.authorization,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
|
||||||
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
import { FileModule } from 'src/core/file/file.module';
|
import { FileModule } from 'src/core/file/file.module';
|
||||||
@ -43,6 +44,7 @@ const jwtModule = JwtModule.registerAsync({
|
|||||||
WorkspaceManagerModule,
|
WorkspaceManagerModule,
|
||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
TypeOrmModule.forFeature([Workspace, User, RefreshToken], 'core'),
|
TypeOrmModule.forFeature([Workspace, User, RefreshToken], 'core'),
|
||||||
|
HttpModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
GoogleAuthController,
|
GoogleAuthController,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
|
||||||
import { UserService } from 'src/core/user/services/user.service';
|
import { UserService } from 'src/core/user/services/user.service';
|
||||||
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
|
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
|
||||||
@ -33,6 +34,10 @@ describe('AuthService', () => {
|
|||||||
provide: FileUploadService,
|
provide: FileUploadService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HttpService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(Workspace, 'core'),
|
provide: getRepositoryToken(Workspace, 'core'),
|
||||||
useValue: {},
|
useValue: {},
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
|
||||||
import FileType from 'file-type';
|
import FileType from 'file-type';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
@ -48,6 +49,7 @@ export class AuthService {
|
|||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectRepository(User, 'core')
|
@InjectRepository(User, 'core')
|
||||||
private readonly userRepository: Repository<User>,
|
private readonly userRepository: Repository<User>,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async challenge(challengeInput: ChallengeInput) {
|
async challenge(challengeInput: ChallengeInput) {
|
||||||
@ -135,7 +137,10 @@ export class AuthService {
|
|||||||
let imagePath: string | undefined = undefined;
|
let imagePath: string | undefined = undefined;
|
||||||
|
|
||||||
if (picture) {
|
if (picture) {
|
||||||
const buffer = await getImageBufferFromUrl(picture);
|
const buffer = await getImageBufferFromUrl(
|
||||||
|
picture,
|
||||||
|
this.httpService.axiosRef,
|
||||||
|
);
|
||||||
|
|
||||||
const type = await FileType.fromBuffer(buffer);
|
const type = await FileType.fromBuffer(buffer);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,52 @@
|
|||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { CompanyInteface } from 'src/core/quick-actions/interfaces/company.interface';
|
||||||
|
|
||||||
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IntelligenceService {
|
||||||
|
constructor(
|
||||||
|
private readonly environmentService: EnvironmentService,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async enrichCompany(domainName: string): Promise<CompanyInteface> {
|
||||||
|
const enrichedCompany = await this.httpService.axiosRef.get(
|
||||||
|
`https://companies.twenty.com/${domainName}`,
|
||||||
|
{
|
||||||
|
validateStatus: function () {
|
||||||
|
// This ensures the promise is always resolved, preventing axios from throwing an error
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (enrichedCompany.status !== 200) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
linkedinLinkUrl: `https://linkedin.com/` + enrichedCompany.data.handle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async completeWithAi(content: string) {
|
||||||
|
return this.httpService.axiosRef.post(
|
||||||
|
'https://openrouter.ai/api/v1/chat/completions',
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.environmentService.getOpenRouterApiKey()}`,
|
||||||
|
'HTTP-Referer': `https://twenty.com`,
|
||||||
|
'X-Title': `Twenty CRM`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'mistralai/mixtral-8x7b-instruct',
|
||||||
|
messages: [{ role: 'user', content: content }],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export interface CompanyInteface {
|
||||||
|
linkedinLinkUrl?: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
|
||||||
|
import { IntelligenceService } from 'src/core/quick-actions/intelligence.service';
|
||||||
|
import { QuickActionsService } from 'src/core/quick-actions/quick-actions.service';
|
||||||
|
import { WorkspaceQueryRunnerModule } from 'src/workspace/workspace-query-runner/workspace-query-runner.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [WorkspaceQueryRunnerModule, HttpModule],
|
||||||
|
controllers: [],
|
||||||
|
providers: [QuickActionsService, IntelligenceService],
|
||||||
|
exports: [QuickActionsService, IntelligenceService],
|
||||||
|
})
|
||||||
|
export class QuickActionsModule {}
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||||
|
|
||||||
|
import { isWorkEmail } from 'src/utils/is-work-email';
|
||||||
|
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||||
|
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||||
|
import { IntelligenceService } from 'src/core/quick-actions/intelligence.service';
|
||||||
|
import { capitalize } from 'src/utils/capitalize';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class QuickActionsService {
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceQueryRunnunerService: WorkspaceQueryRunnerService,
|
||||||
|
private readonly intelligenceService: IntelligenceService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createCompanyFromPerson(id: string, workspaceId: string) {
|
||||||
|
const personRequest =
|
||||||
|
await this.workspaceQueryRunnunerService.executeAndParse<IRecord>(
|
||||||
|
`query {
|
||||||
|
personCollection(filter: {id: {eq: "${id}"}}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
companyId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'person',
|
||||||
|
'',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
const person = personRequest.edges?.[0]?.node;
|
||||||
|
|
||||||
|
if (!person) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!person.companyId && person.email && isWorkEmail(person.email)) {
|
||||||
|
const companyDomainName = person.email.split('@')?.[1].toLowerCase();
|
||||||
|
const companyName = capitalize(companyDomainName.split('.')[0]);
|
||||||
|
let relatedCompanyId = uuidv4();
|
||||||
|
|
||||||
|
const existingCompany =
|
||||||
|
await this.workspaceQueryRunnunerService.executeAndParse<IRecord>(
|
||||||
|
`query {companyCollection(filter: {domainName: {eq: "${companyDomainName}"}}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
'company',
|
||||||
|
'',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingCompany.edges?.length) {
|
||||||
|
relatedCompanyId = existingCompany.edges[0].node.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workspaceQueryRunnunerService.execute(
|
||||||
|
`mutation {
|
||||||
|
insertIntocompanyCollection(objects: ${stringifyWithoutKeyQuote([
|
||||||
|
{
|
||||||
|
id: relatedCompanyId,
|
||||||
|
name: companyName,
|
||||||
|
domainName: companyDomainName,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
])}) {
|
||||||
|
affectedCount
|
||||||
|
records {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceQueryRunnunerService.execute(
|
||||||
|
`mutation {
|
||||||
|
updatepersonCollection(set: ${stringifyWithoutKeyQuote({
|
||||||
|
companyId: relatedCompanyId,
|
||||||
|
})}, filter: { id: { eq: "${person.id}" } }) {
|
||||||
|
affectedCount
|
||||||
|
records {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeQuickActionOnCompany(id: string, workspaceId: string) {
|
||||||
|
const companyQuery = `query {
|
||||||
|
companyCollection(filter: {id: {eq: "${id}"}}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
domainName
|
||||||
|
createdAt
|
||||||
|
linkedinLinkUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const companyRequest =
|
||||||
|
await this.workspaceQueryRunnunerService.executeAndParse<IRecord>(
|
||||||
|
companyQuery,
|
||||||
|
'company',
|
||||||
|
'',
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
const company = companyRequest.edges?.[0]?.node;
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedData = await this.intelligenceService.enrichCompany(
|
||||||
|
company.domainName,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.workspaceQueryRunnunerService.execute(
|
||||||
|
`mutation {
|
||||||
|
updatecompanyCollection(set: ${stringifyWithoutKeyQuote(
|
||||||
|
enrichedData,
|
||||||
|
)}, filter: { id: { eq: "${id}" } }) {
|
||||||
|
affectedCount
|
||||||
|
records {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -210,10 +210,14 @@ export class EnvironmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSentryDSN(): string | undefined {
|
getSentryDSN(): string | undefined {
|
||||||
return this.configService.get<string>('SENTRY_DSN');
|
return this.configService.get<string | undefined>('SENTRY_DSN');
|
||||||
}
|
}
|
||||||
|
|
||||||
getDemoWorkspaceIds(): string[] {
|
getDemoWorkspaceIds(): string[] {
|
||||||
return this.configService.get<string[]>('DEMO_WORKSPACE_IDS') ?? [];
|
return this.configService.get<string[]>('DEMO_WORKSPACE_IDS') ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOpenRouterApiKey(): string | undefined {
|
||||||
|
return this.configService.get<string | undefined>('OPENROUTER_API_KEY');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16348
packages/twenty-server/src/utils/email-providers.ts
Normal file
16348
packages/twenty-server/src/utils/email-providers.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
|||||||
import axios from 'axios';
|
import { Axios } from 'axios';
|
||||||
|
|
||||||
const cropRegex = /([w|h])([0-9]+)/;
|
const cropRegex = /([w|h])([0-9]+)/;
|
||||||
|
|
||||||
@ -22,8 +22,11 @@ export const getCropSize = (value: ShortCropSize): CropSize | null => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getImageBufferFromUrl = async (url: string): Promise<Buffer> => {
|
export const getImageBufferFromUrl = async (
|
||||||
const response = await axios.get(url, {
|
url: string,
|
||||||
|
axiosInstance: Axios,
|
||||||
|
): Promise<Buffer> => {
|
||||||
|
const response = await axiosInstance.get(url, {
|
||||||
responseType: 'arraybuffer',
|
responseType: 'arraybuffer',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
21
packages/twenty-server/src/utils/is-work-email.ts
Normal file
21
packages/twenty-server/src/utils/is-work-email.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { emailProvidersSet } from 'src/utils/email-providers';
|
||||||
|
|
||||||
|
export const isWorkEmail = (email: string) => {
|
||||||
|
if (!email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = email.split('@');
|
||||||
|
|
||||||
|
if (fields.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = fields[1];
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !emailProvidersSet.has(domain);
|
||||||
|
};
|
||||||
@ -15,6 +15,7 @@ describe('getResolverName', () => {
|
|||||||
['createOne', 'createEntity'],
|
['createOne', 'createEntity'],
|
||||||
['updateOne', 'updateEntity'],
|
['updateOne', 'updateEntity'],
|
||||||
['deleteOne', 'deleteEntity'],
|
['deleteOne', 'deleteEntity'],
|
||||||
|
['executeQuickActionOnOne', 'executeQuickActionOnEntity'],
|
||||||
])('should return correct name for %s resolver', (type, expectedResult) => {
|
])('should return correct name for %s resolver', (type, expectedResult) => {
|
||||||
expect(
|
expect(
|
||||||
getResolverName(metadata, type as WorkspaceResolverBuilderMethodNames),
|
getResolverName(metadata, type as WorkspaceResolverBuilderMethodNames),
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export const getResolverName = (
|
|||||||
return `update${pascalCase(objectMetadata.nameSingular)}`;
|
return `update${pascalCase(objectMetadata.nameSingular)}`;
|
||||||
case 'deleteOne':
|
case 'deleteOne':
|
||||||
return `delete${pascalCase(objectMetadata.nameSingular)}`;
|
return `delete${pascalCase(objectMetadata.nameSingular)}`;
|
||||||
|
case 'executeQuickActionOnOne':
|
||||||
|
return `executeQuickActionOn${pascalCase(objectMetadata.nameSingular)}`;
|
||||||
case 'updateMany':
|
case 'updateMany':
|
||||||
return `update${pascalCase(objectMetadata.namePlural)}`;
|
return `update${pascalCase(objectMetadata.namePlural)}`;
|
||||||
case 'deleteMany':
|
case 'deleteMany':
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
||||||
|
import { FieldsStringFactory } from 'src/workspace/workspace-query-builder/factories/fields-string.factory';
|
||||||
|
|
||||||
import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
|
import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
|
||||||
|
|
||||||
@ -9,6 +10,6 @@ import { workspaceQueryBuilderFactories } from './factories/factories';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [ObjectMetadataModule],
|
imports: [ObjectMetadataModule],
|
||||||
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
|
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
|
||||||
exports: [WorkspaceQueryBuilderFactory],
|
exports: [WorkspaceQueryBuilderFactory, FieldsStringFactory],
|
||||||
})
|
})
|
||||||
export class WorkspaceQueryBuilderModule {}
|
export class WorkspaceQueryBuilderModule {}
|
||||||
|
|||||||
@ -181,7 +181,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
)?.records;
|
)?.records;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async execute(
|
async execute(
|
||||||
query: string,
|
query: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
): Promise<PGGraphQLResult | undefined> {
|
): Promise<PGGraphQLResult | undefined> {
|
||||||
@ -215,7 +215,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
const errors = graphqlResult?.[0]?.resolve?.errors;
|
const errors = graphqlResult?.[0]?.resolve?.errors;
|
||||||
|
|
||||||
if (Array.isArray(errors) && errors.length > 0) {
|
if (Array.isArray(errors) && errors.length > 0) {
|
||||||
console.error('GraphQL errors', errors);
|
console.error(`GraphQL errors on ${command}${targetTableName}`, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@ -224,4 +224,15 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
return parseResult(result);
|
return parseResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async executeAndParse<Result>(
|
||||||
|
query: string,
|
||||||
|
targetTableName: string,
|
||||||
|
command: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<Result> {
|
||||||
|
const result = await this.execute(query, workspaceId);
|
||||||
|
|
||||||
|
return this.parseResult(result, targetTableName, command);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Resolver,
|
||||||
|
FindOneResolverArgs,
|
||||||
|
ExecuteQuickActionOnOneResolverArgs,
|
||||||
|
DeleteOneResolverArgs,
|
||||||
|
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||||
|
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||||
|
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||||
|
import { WorkspaceQueryRunnerOptions } from 'src/workspace/workspace-query-runner/interfaces/query-runner-optionts.interface';
|
||||||
|
|
||||||
|
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||||
|
import { QuickActionsService } from 'src/core/quick-actions/quick-actions.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExecuteQuickActionOnOneResolverFactory
|
||||||
|
implements WorkspaceResolverBuilderFactoryInterface
|
||||||
|
{
|
||||||
|
public static methodName = 'executeQuickActionOnOne' as const;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||||
|
private readonly quickActionsService: QuickActionsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(
|
||||||
|
context: WorkspaceSchemaBuilderContext,
|
||||||
|
): Resolver<ExecuteQuickActionOnOneResolverArgs> {
|
||||||
|
const internalContext = context;
|
||||||
|
|
||||||
|
return (_source, args, context, info) => {
|
||||||
|
return this.executeQuickActionOnOne(args, {
|
||||||
|
targetTableName: internalContext.targetTableName,
|
||||||
|
workspaceId: internalContext.workspaceId,
|
||||||
|
info,
|
||||||
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeQuickActionOnOne<Record extends IRecord = IRecord>(
|
||||||
|
args: DeleteOneResolverArgs,
|
||||||
|
options: WorkspaceQueryRunnerOptions,
|
||||||
|
): Promise<Record | undefined> {
|
||||||
|
switch (options.targetTableName) {
|
||||||
|
case 'company': {
|
||||||
|
await this.quickActionsService.executeQuickActionOnCompany(
|
||||||
|
args.id,
|
||||||
|
options.workspaceId,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'person': {
|
||||||
|
await this.quickActionsService.createCompanyFromPerson(
|
||||||
|
args.id,
|
||||||
|
options.workspaceId,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// TODO: different quick actions per object on frontend
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.workspaceQueryRunnerService.findOne(
|
||||||
|
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import { CreateOneResolverFactory } from './create-one-resolver.factory';
|
|||||||
import { UpdateOneResolverFactory } from './update-one-resolver.factory';
|
import { UpdateOneResolverFactory } from './update-one-resolver.factory';
|
||||||
import { DeleteOneResolverFactory } from './delete-one-resolver.factory';
|
import { DeleteOneResolverFactory } from './delete-one-resolver.factory';
|
||||||
import { DeleteManyResolverFactory } from './delete-many-resolver.factory';
|
import { DeleteManyResolverFactory } from './delete-many-resolver.factory';
|
||||||
|
import { ExecuteQuickActionOnOneResolverFactory } from './execute-quick-action-on-one-resolver.factory';
|
||||||
|
|
||||||
export const workspaceResolverBuilderFactories = [
|
export const workspaceResolverBuilderFactories = [
|
||||||
FindManyResolverFactory,
|
FindManyResolverFactory,
|
||||||
@ -15,6 +16,7 @@ export const workspaceResolverBuilderFactories = [
|
|||||||
CreateOneResolverFactory,
|
CreateOneResolverFactory,
|
||||||
UpdateOneResolverFactory,
|
UpdateOneResolverFactory,
|
||||||
DeleteOneResolverFactory,
|
DeleteOneResolverFactory,
|
||||||
|
ExecuteQuickActionOnOneResolverFactory,
|
||||||
UpdateManyResolverFactory,
|
UpdateManyResolverFactory,
|
||||||
DeleteManyResolverFactory,
|
DeleteManyResolverFactory,
|
||||||
];
|
];
|
||||||
@ -29,6 +31,7 @@ export const workspaceResolverBuilderMethodNames = {
|
|||||||
CreateOneResolverFactory.methodName,
|
CreateOneResolverFactory.methodName,
|
||||||
UpdateOneResolverFactory.methodName,
|
UpdateOneResolverFactory.methodName,
|
||||||
DeleteOneResolverFactory.methodName,
|
DeleteOneResolverFactory.methodName,
|
||||||
|
ExecuteQuickActionOnOneResolverFactory.methodName,
|
||||||
UpdateManyResolverFactory.methodName,
|
UpdateManyResolverFactory.methodName,
|
||||||
DeleteManyResolverFactory.methodName,
|
DeleteManyResolverFactory.methodName,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -51,6 +51,10 @@ export interface DeleteOneResolverArgs {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecuteQuickActionOnOneResolverArgs {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeleteManyResolverArgs<Filter = any> {
|
export interface DeleteManyResolverArgs<Filter = any> {
|
||||||
filter: Filter;
|
filter: Filter;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { WorkspaceQueryRunnerModule } from 'src/workspace/workspace-query-runner/workspace-query-runner.module';
|
import { WorkspaceQueryRunnerModule } from 'src/workspace/workspace-query-runner/workspace-query-runner.module';
|
||||||
|
import { QuickActionsModule } from 'src/core/quick-actions/quick-actions.module';
|
||||||
|
|
||||||
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
||||||
|
|
||||||
import { workspaceResolverBuilderFactories } from './factories/factories';
|
import { workspaceResolverBuilderFactories } from './factories/factories';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [WorkspaceQueryRunnerModule],
|
imports: [WorkspaceQueryRunnerModule, QuickActionsModule],
|
||||||
providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory],
|
providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory],
|
||||||
exports: [WorkspaceResolverFactory],
|
exports: [WorkspaceResolverFactory],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { ObjectMetadataInterface } from 'src/metadata/field-metadata/interfaces/
|
|||||||
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
|
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
|
||||||
import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
import { DeleteManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
import { DeleteManyResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||||
|
import { ExecuteQuickActionOnOneResolverFactory } from 'src/workspace/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory';
|
||||||
|
|
||||||
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
||||||
import { FindOneResolverFactory } from './factories/find-one-resolver.factory';
|
import { FindOneResolverFactory } from './factories/find-one-resolver.factory';
|
||||||
@ -31,6 +32,7 @@ export class WorkspaceResolverFactory {
|
|||||||
private readonly createOneResolverFactory: CreateOneResolverFactory,
|
private readonly createOneResolverFactory: CreateOneResolverFactory,
|
||||||
private readonly updateOneResolverFactory: UpdateOneResolverFactory,
|
private readonly updateOneResolverFactory: UpdateOneResolverFactory,
|
||||||
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
||||||
|
private readonly executeQuickActionOnOneResolverFactory: ExecuteQuickActionOnOneResolverFactory,
|
||||||
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
|
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
|
||||||
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
||||||
) {}
|
) {}
|
||||||
@ -50,6 +52,7 @@ export class WorkspaceResolverFactory {
|
|||||||
['createOne', this.createOneResolverFactory],
|
['createOne', this.createOneResolverFactory],
|
||||||
['updateOne', this.updateOneResolverFactory],
|
['updateOne', this.updateOneResolverFactory],
|
||||||
['deleteOne', this.deleteOneResolverFactory],
|
['deleteOne', this.deleteOneResolverFactory],
|
||||||
|
['executeQuickActionOnOne', this.executeQuickActionOnOneResolverFactory],
|
||||||
['updateMany', this.updateManyResolverFactory],
|
['updateMany', this.updateManyResolverFactory],
|
||||||
['deleteMany', this.deleteManyResolverFactory],
|
['deleteMany', this.deleteManyResolverFactory],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -34,6 +34,9 @@ describe('getResolverArgs', () => {
|
|||||||
deleteOne: {
|
deleteOne: {
|
||||||
id: { type: FieldMetadataType.UUID, isNullable: false },
|
id: { type: FieldMetadataType.UUID, isNullable: false },
|
||||||
},
|
},
|
||||||
|
executeQuickActionOnOne: {
|
||||||
|
id: { type: FieldMetadataType.UUID, isNullable: false },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test each resolver type
|
// Test each resolver type
|
||||||
|
|||||||
@ -76,6 +76,13 @@ export const getResolverArgs = (
|
|||||||
isNullable: false,
|
isNullable: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
case 'executeQuickActionOnOne':
|
||||||
|
return {
|
||||||
|
id: {
|
||||||
|
type: FieldMetadataType.UUID,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
case 'updateMany':
|
case 'updateMany':
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
17
yarn.lock
17
yarn.lock
@ -7015,6 +7015,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@nestjs/axios@npm:^3.0.1":
|
||||||
|
version: 3.0.1
|
||||||
|
resolution: "@nestjs/axios@npm:3.0.1"
|
||||||
|
peerDependencies:
|
||||||
|
"@nestjs/common": ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0
|
||||||
|
axios: ^1.3.1
|
||||||
|
reflect-metadata: ^0.1.12
|
||||||
|
rxjs: ^6.0.0 || ^7.0.0
|
||||||
|
checksum: 07f36e260a21fbc52be8f1dcc55d71f4de741264d467adaaf4b1c3926a5c9367bc77a2dac4462e638db3a39ec11051ccad36fe958f8513f54bc3e57fba6329a2
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@nestjs/cli@npm:^9.0.0":
|
"@nestjs/cli@npm:^9.0.0":
|
||||||
version: 9.5.0
|
version: 9.5.0
|
||||||
resolution: "@nestjs/cli@npm:9.5.0"
|
resolution: "@nestjs/cli@npm:9.5.0"
|
||||||
@ -16313,7 +16325,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"axios@npm:^1.4.0, axios@npm:^1.5.1, axios@npm:^1.6.1":
|
"axios@npm:^1.5.1, axios@npm:^1.6.1, axios@npm:^1.6.2":
|
||||||
version: 1.6.2
|
version: 1.6.2
|
||||||
resolution: "axios@npm:1.6.2"
|
resolution: "axios@npm:1.6.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -40988,6 +41000,7 @@ __metadata:
|
|||||||
"@graphql-tools/schema": "npm:^10.0.0"
|
"@graphql-tools/schema": "npm:^10.0.0"
|
||||||
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch"
|
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch"
|
||||||
"@nestjs/apollo": "npm:^11.0.5"
|
"@nestjs/apollo": "npm:^11.0.5"
|
||||||
|
"@nestjs/axios": "npm:^3.0.1"
|
||||||
"@nestjs/cli": "npm:^9.0.0"
|
"@nestjs/cli": "npm:^9.0.0"
|
||||||
"@nestjs/common": "npm:^9.0.0"
|
"@nestjs/common": "npm:^9.0.0"
|
||||||
"@nestjs/config": "npm:^2.3.2"
|
"@nestjs/config": "npm:^2.3.2"
|
||||||
@ -41032,7 +41045,7 @@ __metadata:
|
|||||||
"@typescript-eslint/parser": "npm:^5.0.0"
|
"@typescript-eslint/parser": "npm:^5.0.0"
|
||||||
add: "npm:^2.0.6"
|
add: "npm:^2.0.6"
|
||||||
apollo-server-express: "npm:^3.12.0"
|
apollo-server-express: "npm:^3.12.0"
|
||||||
axios: "npm:^1.4.0"
|
axios: "npm:^1.6.2"
|
||||||
bcrypt: "npm:^5.1.1"
|
bcrypt: "npm:^5.1.1"
|
||||||
body-parser: "npm:^1.20.2"
|
body-parser: "npm:^1.20.2"
|
||||||
bullmq: "npm:^4.14.0"
|
bullmq: "npm:^4.14.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user