Update what is being audit logged (#11833)
No need to audit log workflow runs as it's already a form of audit log. Add more audit log for other objects Rename MessagingTelemetry to MessagingMonitoring Merge Analytics and Audit in one (Audit) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@ -625,11 +625,8 @@ export type FeatureFlagDto = {
|
|||||||
|
|
||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
|
||||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
|
||||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||||
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
|
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
|
||||||
IsEventObjectEnabled = 'IsEventObjectEnabled',
|
|
||||||
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
|
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
|
||||||
IsNewRelationEnabled = 'IsNewRelationEnabled',
|
IsNewRelationEnabled = 'IsNewRelationEnabled',
|
||||||
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',
|
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
@ -575,11 +575,8 @@ export type FeatureFlagDto = {
|
|||||||
|
|
||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
|
||||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
|
||||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||||
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
|
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
|
||||||
IsEventObjectEnabled = 'IsEventObjectEnabled',
|
|
||||||
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
|
IsJsonFilterEnabled = 'IsJsonFilterEnabled',
|
||||||
IsNewRelationEnabled = 'IsNewRelationEnabled',
|
IsNewRelationEnabled = 'IsNewRelationEnabled',
|
||||||
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',
|
IsPermissionsV2Enabled = 'IsPermissionsV2Enabled',
|
||||||
|
|||||||
@ -9,13 +9,11 @@ import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/compo
|
|||||||
import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
|
import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { FeatureFlagKey } from '~/generated/graphql';
|
import { Tag } from 'twenty-ui/components';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
|
||||||
import { H2Title, IconLock } from 'twenty-ui/display';
|
import { H2Title, IconLock } from 'twenty-ui/display';
|
||||||
import { Section } from 'twenty-ui/layout';
|
import { Section } from 'twenty-ui/layout';
|
||||||
import { Tag } from 'twenty-ui/components';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -36,9 +34,6 @@ export const SettingsSecurity = () => {
|
|||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
|
||||||
const IsApprovedAccessDomainsEnabled = useIsFeatureEnabled(
|
|
||||||
FeatureFlagKey.IsApprovedAccessDomainsEnabled,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer
|
<SubMenuTopBarContainer
|
||||||
@ -68,15 +63,13 @@ export const SettingsSecurity = () => {
|
|||||||
/>
|
/>
|
||||||
<SettingsSSOIdentitiesProvidersListCard />
|
<SettingsSSOIdentitiesProvidersListCard />
|
||||||
</StyledSection>
|
</StyledSection>
|
||||||
{IsApprovedAccessDomainsEnabled && (
|
<StyledSection>
|
||||||
<StyledSection>
|
<H2Title
|
||||||
<H2Title
|
title={t`Approved Domains`}
|
||||||
title={t`Approved Domains`}
|
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
|
||||||
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
|
/>
|
||||||
/>
|
<SettingsApprovedAccessDomainsListCard />
|
||||||
<SettingsApprovedAccessDomainsListCard />
|
</StyledSection>
|
||||||
</StyledSection>
|
|
||||||
)}
|
|
||||||
<Section>
|
<Section>
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<H2Title
|
<H2Title
|
||||||
|
|||||||
@ -208,14 +208,14 @@
|
|||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "packages/twenty-server",
|
"cwd": "packages/twenty-server",
|
||||||
"command": "nx ts-node-no-deps -- src/database/clickhouse/migrations/run-migrations.ts"
|
"command": "nx ts-node-no-deps -- src/database/clickHouse/migrations/run-migrations.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clickhouse:seed": {
|
"clickhouse:seed": {
|
||||||
"executor": "nx:run-commands",
|
"executor": "nx:run-commands",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "packages/twenty-server",
|
"cwd": "packages/twenty-server",
|
||||||
"command": "nx ts-node-no-deps -- src/database/clickhouse/seeds/run-seeds.ts"
|
"command": "nx ts-node-no-deps -- src/database/clickHouse/seeds/run-seeds.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lingui:extract": {
|
"lingui:extract": {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
|||||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||||
import { ModulesModule } from 'src/modules/modules.module';
|
import { ModulesModule } from 'src/modules/modules.module';
|
||||||
|
|
||||||
|
import { ClickHouseModule } from './database/clickHouse/clickHouse.module';
|
||||||
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
|
import { CoreEngineModule } from './engine/core-modules/core-engine.module';
|
||||||
import { I18nModule } from './engine/core-modules/i18n/i18n.module';
|
import { I18nModule } from './engine/core-modules/i18n/i18n.module';
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ const MIGRATED_REST_METHODS = [
|
|||||||
useClass: GraphQLConfigService,
|
useClass: GraphQLConfigService,
|
||||||
}),
|
}),
|
||||||
TwentyORMModule,
|
TwentyORMModule,
|
||||||
|
ClickHouseModule,
|
||||||
// Core engine module, contains all the core modules
|
// Core engine module, contains all the core modules
|
||||||
CoreEngineModule,
|
CoreEngineModule,
|
||||||
// Modules module, contains all business logic modules
|
// Modules module, contains all business logic modules
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||||
|
|
||||||
|
import { ClickHouseService } from './clickHouse.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TwentyConfigModule],
|
||||||
|
providers: [ClickHouseService],
|
||||||
|
exports: [ClickHouseService],
|
||||||
|
})
|
||||||
|
export class ClickHouseModule {}
|
||||||
@ -0,0 +1,264 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
import { ClickHouseClient } from '@clickhouse/client';
|
||||||
|
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
|
import { ClickHouseService } from './clickHouse.service';
|
||||||
|
|
||||||
|
// Mock the createClient function from @clickhouse/client
|
||||||
|
jest.mock('@clickhouse/client', () => ({
|
||||||
|
createClient: jest.fn().mockReturnValue({
|
||||||
|
insert: jest.fn().mockResolvedValue({}),
|
||||||
|
query: jest.fn().mockResolvedValue({
|
||||||
|
json: jest.fn().mockResolvedValue([{ test: 'data' }]),
|
||||||
|
}),
|
||||||
|
ping: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
close: jest.fn().mockResolvedValue({}),
|
||||||
|
exec: jest.fn().mockResolvedValue({}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('ClickHouseService', () => {
|
||||||
|
let service: ClickHouseService;
|
||||||
|
let twentyConfigService: TwentyConfigService;
|
||||||
|
let mockClickHouseClient: jest.Mocked<ClickHouseClient>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
mockClickHouseClient = {
|
||||||
|
insert: jest.fn().mockResolvedValue({}),
|
||||||
|
query: jest.fn().mockResolvedValue({
|
||||||
|
json: jest.fn().mockResolvedValue([{ test: 'data' }]),
|
||||||
|
}),
|
||||||
|
ping: jest.fn().mockResolvedValue({ success: true }),
|
||||||
|
close: jest.fn().mockResolvedValue({}),
|
||||||
|
exec: jest.fn().mockResolvedValue({}),
|
||||||
|
} as unknown as jest.Mocked<ClickHouseClient>;
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ClickHouseService,
|
||||||
|
{
|
||||||
|
provide: TwentyConfigService,
|
||||||
|
useValue: {
|
||||||
|
get: jest.fn((key) => {
|
||||||
|
if (key === 'CLICKHOUSE_URL') return 'http://localhost:8123';
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ClickHouseService>(ClickHouseService);
|
||||||
|
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
||||||
|
|
||||||
|
// Set the mock client
|
||||||
|
(service as any).mainClient = mockClickHouseClient;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should not initialize clickhouse client when clickhouse is disabled', async () => {
|
||||||
|
jest.spyOn(twentyConfigService, 'get').mockImplementation((key) => {
|
||||||
|
if (key === 'CLICKHOUSE_URL') return '';
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newModule: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ClickHouseService,
|
||||||
|
{
|
||||||
|
provide: TwentyConfigService,
|
||||||
|
useValue: twentyConfigService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
const newService = newModule.get<ClickHouseService>(ClickHouseService);
|
||||||
|
|
||||||
|
expect((newService as any).mainClient).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('insert', () => {
|
||||||
|
it('should insert data into clickhouse and return success', async () => {
|
||||||
|
const testData = [{ id: 1, name: 'test' }];
|
||||||
|
const result = await service.insert('test_table', testData);
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: true });
|
||||||
|
expect(mockClickHouseClient.insert).toHaveBeenCalledWith({
|
||||||
|
table: 'test_table',
|
||||||
|
values: testData,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return failure when clickhouse client is not defined', async () => {
|
||||||
|
(service as any).mainClient = undefined;
|
||||||
|
|
||||||
|
const testData = [{ id: 1, name: 'test' }];
|
||||||
|
const result = await service.insert('test_table', testData);
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors and return failure', async () => {
|
||||||
|
const testError = new Error('Test error');
|
||||||
|
|
||||||
|
mockClickHouseClient.insert.mockRejectedValueOnce(testError);
|
||||||
|
|
||||||
|
const testData = [{ id: 1, name: 'test' }];
|
||||||
|
const result = await service.insert('test_table', testData);
|
||||||
|
|
||||||
|
expect(result).toEqual({ success: false });
|
||||||
|
// Since the service uses logger.error instead of exceptionHandlerService.captureExceptions,
|
||||||
|
// we don't need to assert on exceptionHandlerService
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('select', () => {
|
||||||
|
it('should execute a query and return results', async () => {
|
||||||
|
const query = 'SELECT * FROM test_table WHERE id = {id:Int32}';
|
||||||
|
const params = { id: 1 };
|
||||||
|
|
||||||
|
mockClickHouseClient.query.mockResolvedValueOnce({
|
||||||
|
json: jest.fn().mockResolvedValueOnce([{ id: 1, name: 'test' }]),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const result = await service.select(query, params);
|
||||||
|
|
||||||
|
expect(result).toEqual([{ id: 1, name: 'test' }]);
|
||||||
|
expect(mockClickHouseClient.query).toHaveBeenCalledWith({
|
||||||
|
query,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
query_params: params,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array when clickhouse client is not defined', async () => {
|
||||||
|
(service as any).mainClient = undefined;
|
||||||
|
|
||||||
|
const query = 'SELECT * FROM test_table';
|
||||||
|
const result = await service.select(query);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors and return empty array', async () => {
|
||||||
|
const testError = new Error('Test error');
|
||||||
|
|
||||||
|
mockClickHouseClient.query.mockRejectedValueOnce(testError);
|
||||||
|
|
||||||
|
const query = 'SELECT * FROM test_table';
|
||||||
|
const result = await service.select(query);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
// Since the service uses logger.error instead of exceptionHandlerService.captureExceptions,
|
||||||
|
// we don't need to assert on exceptionHandlerService
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createDatabase', () => {
|
||||||
|
it('should create a database and return true', async () => {
|
||||||
|
const result = await service.createDatabase('test_db');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockClickHouseClient.exec).toHaveBeenCalledWith({
|
||||||
|
query: 'CREATE DATABASE IF NOT EXISTS test_db',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when clickhouse client is not defined', async () => {
|
||||||
|
(service as any).mainClient = undefined;
|
||||||
|
|
||||||
|
const result = await service.createDatabase('test_db');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('dropDatabase', () => {
|
||||||
|
it('should drop a database and return true', async () => {
|
||||||
|
const result = await service.dropDatabase('test_db');
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(mockClickHouseClient.exec).toHaveBeenCalledWith({
|
||||||
|
query: 'DROP DATABASE IF EXISTS test_db',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false when clickhouse client is not defined', async () => {
|
||||||
|
(service as any).mainClient = undefined;
|
||||||
|
|
||||||
|
const result = await service.dropDatabase('test_db');
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('connectToClient', () => {
|
||||||
|
it('should connect to a client and return it', async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(service, 'connectToClient')
|
||||||
|
.mockResolvedValueOnce(mockClickHouseClient);
|
||||||
|
|
||||||
|
const result = await service.connectToClient('test-client');
|
||||||
|
|
||||||
|
expect(result).toBe(mockClickHouseClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reuse an existing client if available', async () => {
|
||||||
|
// Set up a client in the map
|
||||||
|
(service as any).clients.set('test-client', mockClickHouseClient);
|
||||||
|
|
||||||
|
const result = await service.connectToClient('test-client');
|
||||||
|
|
||||||
|
expect(result).toBe(mockClickHouseClient);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('disconnectFromClient', () => {
|
||||||
|
it('should disconnect from a client', async () => {
|
||||||
|
// Set up a client in the map
|
||||||
|
(service as any).clients.set('test-client', mockClickHouseClient);
|
||||||
|
|
||||||
|
await service.disconnectFromClient('test-client');
|
||||||
|
|
||||||
|
expect(mockClickHouseClient.close).toHaveBeenCalled();
|
||||||
|
expect((service as any).clients.has('test-client')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing if client does not exist', async () => {
|
||||||
|
await service.disconnectFromClient('non-existent-client');
|
||||||
|
|
||||||
|
expect(mockClickHouseClient.close).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lifecycle hooks', () => {
|
||||||
|
it('should ping server on module init', async () => {
|
||||||
|
await service.onModuleInit();
|
||||||
|
|
||||||
|
expect(mockClickHouseClient.ping).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close all clients on module destroy', async () => {
|
||||||
|
// Set up a couple of clients
|
||||||
|
(service as any).clients.set('client1', mockClickHouseClient);
|
||||||
|
(service as any).clients.set('client2', mockClickHouseClient);
|
||||||
|
|
||||||
|
await service.onModuleDestroy();
|
||||||
|
|
||||||
|
// One for mainClient, and two for the clients in the map
|
||||||
|
expect(mockClickHouseClient.close).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,230 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
Logger,
|
||||||
|
OnModuleDestroy,
|
||||||
|
OnModuleInit,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ClickHouseClient, createClient } from '@clickhouse/client';
|
||||||
|
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private mainClient: ClickHouseClient | undefined;
|
||||||
|
private clients: Map<string, ClickHouseClient> = new Map();
|
||||||
|
private isClientInitializing: Map<string, boolean> = new Map();
|
||||||
|
private readonly logger = new Logger(ClickHouseService.name);
|
||||||
|
|
||||||
|
constructor(private readonly twentyConfigService: TwentyConfigService) {
|
||||||
|
if (this.twentyConfigService.get('CLICKHOUSE_URL')) {
|
||||||
|
this.mainClient = createClient({
|
||||||
|
url: this.twentyConfigService.get('CLICKHOUSE_URL'),
|
||||||
|
compression: {
|
||||||
|
response: true,
|
||||||
|
request: true,
|
||||||
|
},
|
||||||
|
clickhouse_settings: {
|
||||||
|
async_insert: 1,
|
||||||
|
wait_for_async_insert: 1,
|
||||||
|
},
|
||||||
|
application: 'twenty',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMainClient(): ClickHouseClient | undefined {
|
||||||
|
return this.mainClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async connectToClient(
|
||||||
|
clientId: string,
|
||||||
|
url?: string,
|
||||||
|
): Promise<ClickHouseClient | undefined> {
|
||||||
|
if (!this.twentyConfigService.get('CLICKHOUSE_URL')) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a bit before trying again if another initialization is in progress
|
||||||
|
while (this.isClientInitializing.get(clientId)) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clients.has(clientId)) {
|
||||||
|
return this.clients.get(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isClientInitializing.set(clientId, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientInstance = await this.createAndInitializeClient(url);
|
||||||
|
|
||||||
|
this.clients.set(clientId, clientInstance);
|
||||||
|
|
||||||
|
return clientInstance;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(
|
||||||
|
`Error connecting to ClickHouse client ${clientId}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
} finally {
|
||||||
|
this.isClientInitializing.delete(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createAndInitializeClient(
|
||||||
|
url?: string,
|
||||||
|
): Promise<ClickHouseClient> {
|
||||||
|
const client = createClient({
|
||||||
|
url: url ?? this.twentyConfigService.get('CLICKHOUSE_URL'),
|
||||||
|
compression: {
|
||||||
|
response: true,
|
||||||
|
request: true,
|
||||||
|
},
|
||||||
|
clickhouse_settings: {
|
||||||
|
async_insert: 1,
|
||||||
|
wait_for_async_insert: 1,
|
||||||
|
},
|
||||||
|
application: 'twenty',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ping to check connection
|
||||||
|
await client.ping();
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disconnectFromClient(clientId: string) {
|
||||||
|
if (!this.clients.has(clientId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = this.clients.get(clientId);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clients.delete(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
if (this.mainClient) {
|
||||||
|
// Just ping to verify the connection
|
||||||
|
try {
|
||||||
|
await this.mainClient.ping();
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Error connecting to ClickHouse', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
// Close main client
|
||||||
|
if (this.mainClient) {
|
||||||
|
await this.mainClient.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all other clients
|
||||||
|
for (const [, client] of this.clients) {
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async insert<T extends Record<string, any>>(
|
||||||
|
table: string,
|
||||||
|
values: T[],
|
||||||
|
clientId?: string,
|
||||||
|
): Promise<{ success: boolean }> {
|
||||||
|
try {
|
||||||
|
const client = clientId
|
||||||
|
? await this.connectToClient(clientId)
|
||||||
|
: this.mainClient;
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.insert({
|
||||||
|
table,
|
||||||
|
values,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Error inserting data into ClickHouse', err);
|
||||||
|
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to execute a select query
|
||||||
|
public async select<T>(
|
||||||
|
query: string,
|
||||||
|
params?: Record<string, any>,
|
||||||
|
clientId?: string,
|
||||||
|
): Promise<T[]> {
|
||||||
|
try {
|
||||||
|
const client = clientId
|
||||||
|
? await this.connectToClient(clientId)
|
||||||
|
: this.mainClient;
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultSet = await client.query({
|
||||||
|
query,
|
||||||
|
format: 'JSONEachRow',
|
||||||
|
query_params: params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resultSet.json<T>();
|
||||||
|
|
||||||
|
return Array.isArray(result) ? result : [];
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Error executing select query in ClickHouse', err);
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createDatabase(databaseName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!this.mainClient) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.mainClient.exec({
|
||||||
|
query: `CREATE DATABASE IF NOT EXISTS ${databaseName}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Error creating database in ClickHouse', err);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async dropDatabase(databaseName: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!this.mainClient) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.mainClient.exec({
|
||||||
|
query: `DROP DATABASE IF EXISTS ${databaseName}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Error dropping database in ClickHouse', err);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
CREATE TABLE IF NOT EXISTS events
|
CREATE TABLE IF NOT EXISTS auditEvent
|
||||||
(
|
(
|
||||||
`event` LowCardinality(String),
|
`event` LowCardinality(String),
|
||||||
`timestamp` DateTime64(3),
|
`timestamp` DateTime64(3),
|
||||||
@ -7,4 +7,4 @@ CREATE TABLE IF NOT EXISTS events
|
|||||||
`properties` JSON
|
`properties` JSON
|
||||||
)
|
)
|
||||||
ENGINE = MergeTree
|
ENGINE = MergeTree
|
||||||
ORDER BY (event, workspaceId, timestamp);
|
ORDER BY (event, workspaceId, userId, timestamp);
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS externalEvent
|
||||||
|
(
|
||||||
|
`event` LowCardinality(String) NOT NULL,
|
||||||
|
`timestamp` DateTime64(3) NOT NULL,
|
||||||
|
`userId` String DEFAULT '',
|
||||||
|
`workspaceId` String NOT NULL,
|
||||||
|
`objectId` String NOT NULL,
|
||||||
|
`objectType` LowCardinality(String), -- TBC if it should really be a LowCardinality given custom objects
|
||||||
|
`properties` JSON
|
||||||
|
)
|
||||||
|
ENGINE = MergeTree
|
||||||
|
ORDER BY (event, workspaceId, userId, timestamp);
|
||||||
@ -36,7 +36,7 @@ async function ensureDatabaseExists() {
|
|||||||
async function ensureMigrationTable(client: ClickHouseClient) {
|
async function ensureMigrationTable(client: ClickHouseClient) {
|
||||||
await client.command({
|
await client.command({
|
||||||
query: `
|
query: `
|
||||||
CREATE TABLE IF NOT EXISTS migrations (
|
CREATE TABLE IF NOT EXISTS _migration (
|
||||||
filename String,
|
filename String,
|
||||||
applied_at DateTime DEFAULT now()
|
applied_at DateTime DEFAULT now()
|
||||||
) ENGINE = MergeTree()
|
) ENGINE = MergeTree()
|
||||||
@ -50,7 +50,7 @@ async function hasMigrationBeenRun(
|
|||||||
client: ClickHouseClient,
|
client: ClickHouseClient,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const resultSet = await client.query({
|
const resultSet = await client.query({
|
||||||
query: `SELECT count() as count FROM migrations WHERE filename = {filename:String}`,
|
query: `SELECT count() as count FROM _migration WHERE filename = {filename:String}`,
|
||||||
query_params: { filename },
|
query_params: { filename },
|
||||||
format: 'JSON',
|
format: 'JSON',
|
||||||
});
|
});
|
||||||
@ -61,7 +61,7 @@ async function hasMigrationBeenRun(
|
|||||||
|
|
||||||
async function recordMigration(filename: string, client: ClickHouseClient) {
|
async function recordMigration(filename: string, client: ClickHouseClient) {
|
||||||
await client.insert({
|
await client.insert({
|
||||||
table: 'migrations',
|
table: '_migration',
|
||||||
values: [{ filename }],
|
values: [{ filename }],
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
});
|
});
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { GenericTrackEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
||||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
|
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
|
||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
||||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-deactivated';
|
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
|
||||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-updated';
|
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
|
||||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-delete';
|
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const fixtures: Array<GenericTrackEvent> = [
|
export const fixtures: Array<GenericTrackEvent> = [
|
||||||
{
|
{
|
||||||
@ -2,7 +2,7 @@
|
|||||||
import { createClient } from '@clickhouse/client';
|
import { createClient } from '@clickhouse/client';
|
||||||
import { config } from 'dotenv';
|
import { config } from 'dotenv';
|
||||||
|
|
||||||
import { fixtures } from 'src/engine/core-modules/analytics/utils/fixtures/fixtures';
|
import { fixtures } from './fixtures';
|
||||||
|
|
||||||
config({
|
config({
|
||||||
path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
|
path: process.env.NODE_ENV === 'test' ? '.env.test' : '.env',
|
||||||
@ -18,7 +18,7 @@ async function seedEvents() {
|
|||||||
console.log(`⚡ Seeding ${fixtures.length} events...`);
|
console.log(`⚡ Seeding ${fixtures.length} events...`);
|
||||||
|
|
||||||
await client.insert({
|
await client.insert({
|
||||||
table: 'events',
|
table: 'auditEvent',
|
||||||
values: fixtures,
|
values: fixtures,
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
});
|
});
|
||||||
@ -25,11 +25,6 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: FeatureFlagKey.IsEventObjectEnabled,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IsStripeIntegrationEnabled,
|
key: FeatureFlagKey.IsStripeIntegrationEnabled,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
@ -40,21 +35,11 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: FeatureFlagKey.IsAnalyticsV2Enabled,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IsCustomDomainEnabled,
|
key: FeatureFlagKey.IsCustomDomainEnabled,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: FeatureFlagKey.IsApprovedAccessDomainsEnabled,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
value: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IsUniqueIndexesEnabled,
|
key: FeatureFlagKey.IsUniqueIndexesEnabled,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { CreateAuditLogFromInternalEvent } from 'src/engine/core-modules/audit/jobs/create-audit-log-from-internal-event';
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
@ -12,11 +13,10 @@ import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/t
|
|||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||||
|
import { SubscriptionsJob } from 'src/engine/subscriptions/subscriptions.job';
|
||||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event';
|
|
||||||
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
|
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
|
||||||
import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job';
|
import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job';
|
||||||
import { SubscriptionsJob } from 'src/engine/subscriptions/subscriptions.job';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EntityEventsToDbListener {
|
export class EntityEventsToDbListener {
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
||||||
|
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
import { USER_SIGNUP_EVENT } from 'src/engine/core-modules/audit/utils/events/track/user/user-signup';
|
||||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
|
import { TelemetryService } from 'src/engine/core-modules/telemetry/telemetry.service';
|
||||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
import { OnCustomBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-custom-batch-event.decorator';
|
|
||||||
import { USER_SIGNUP_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/user/user-signup';
|
|
||||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TelemetryListener {
|
export class TelemetryListener {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly auditService: AuditService,
|
||||||
private readonly telemetryService: TelemetryService,
|
private readonly telemetryService: TelemetryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -21,8 +21,8 @@ export class TelemetryListener {
|
|||||||
) {
|
) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
payload.events.map(async (eventPayload) => {
|
payload.events.map(async (eventPayload) => {
|
||||||
this.analyticsService
|
this.auditService
|
||||||
.createAnalyticsContext({
|
.createContext({
|
||||||
userId: eventPayload.userId,
|
userId: eventPayload.userId,
|
||||||
workspaceId: payload.workspaceId,
|
workspaceId: payload.workspaceId,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -5,15 +5,15 @@ import { WorkspaceQueryBuilderModule } from 'src/engine/api/graphql/workspace-qu
|
|||||||
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
|
import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace-query-runner/factories';
|
||||||
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
|
import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener';
|
||||||
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||||
|
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||||
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module';
|
||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
|
||||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen
|
|||||||
WorkspaceQueryHookModule,
|
WorkspaceQueryHookModule,
|
||||||
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||||
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
TelemetryModule,
|
TelemetryModule,
|
||||||
FileModule,
|
FileModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
|
||||||
import { ClickhouseService } from 'src/engine/core-modules/analytics/services/clickhouse.service';
|
|
||||||
|
|
||||||
import { AnalyticsResolver } from './analytics.resolver';
|
|
||||||
|
|
||||||
import { AnalyticsService } from './services/analytics.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [AnalyticsResolver, AnalyticsService, ClickhouseService],
|
|
||||||
imports: [JwtModule],
|
|
||||||
exports: [AnalyticsService],
|
|
||||||
})
|
|
||||||
export class AnalyticsModule {}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import {
|
|
||||||
makePageview,
|
|
||||||
makeTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/analytics.utils';
|
|
||||||
import { ClickhouseService } from 'src/engine/core-modules/analytics/services/clickhouse.service';
|
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
|
||||||
import {
|
|
||||||
TrackEventName,
|
|
||||||
TrackEventProperties,
|
|
||||||
} from 'src/engine/core-modules/analytics/types/events.type';
|
|
||||||
import { PageviewProperties } from 'src/engine/core-modules/analytics/utils/events/pageview/pageview';
|
|
||||||
import {
|
|
||||||
AnalyticsException,
|
|
||||||
AnalyticsExceptionCode,
|
|
||||||
} from 'src/engine/core-modules/analytics/analytics.exception';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AnalyticsService {
|
|
||||||
constructor(
|
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
|
||||||
private readonly clickhouseService: ClickhouseService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
createAnalyticsContext(context?: {
|
|
||||||
workspaceId?: string | null | undefined;
|
|
||||||
userId?: string | null | undefined;
|
|
||||||
}) {
|
|
||||||
const userIdAndWorkspaceId = context
|
|
||||||
? {
|
|
||||||
...(context.userId ? { userId: context.userId } : {}),
|
|
||||||
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
track: <T extends TrackEventName>(
|
|
||||||
event: T,
|
|
||||||
properties: TrackEventProperties<T>,
|
|
||||||
) =>
|
|
||||||
this.preventAnalyticsIfDisabled(() =>
|
|
||||||
this.clickhouseService.pushEvent({
|
|
||||||
...userIdAndWorkspaceId,
|
|
||||||
...makeTrackEvent(event, properties),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
pageview: (name: string, properties: Partial<PageviewProperties>) =>
|
|
||||||
this.preventAnalyticsIfDisabled(() =>
|
|
||||||
this.clickhouseService.pushEvent({
|
|
||||||
...userIdAndWorkspaceId,
|
|
||||||
...makePageview(name, properties),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private preventAnalyticsIfDisabled(
|
|
||||||
sendEventOrPageviewFunction: () => Promise<{ success: boolean }>,
|
|
||||||
) {
|
|
||||||
if (!this.twentyConfigService.get('ANALYTICS_ENABLED')) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return sendEventOrPageviewFunction();
|
|
||||||
} catch (err) {
|
|
||||||
return new AnalyticsException(err, AnalyticsExceptionCode.INVALID_INPUT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
|
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
|
||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
|
||||||
import {
|
|
||||||
makePageview,
|
|
||||||
makeTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/analytics.utils';
|
|
||||||
|
|
||||||
import { ClickhouseService } from './clickhouse.service';
|
|
||||||
|
|
||||||
// Mock the createClient function from @clickhouse/client
|
|
||||||
jest.mock('@clickhouse/client', () => ({
|
|
||||||
createClient: jest.fn().mockReturnValue({
|
|
||||||
insert: jest.fn().mockResolvedValue({}),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ClickhouseService', () => {
|
|
||||||
let service: ClickhouseService;
|
|
||||||
let twentyConfigService: TwentyConfigService;
|
|
||||||
let exceptionHandlerService: ExceptionHandlerService;
|
|
||||||
let mockClickhouseClient: { insert: jest.Mock };
|
|
||||||
|
|
||||||
const mockPageview = makePageview('Home', {
|
|
||||||
href: 'https://example.com/test',
|
|
||||||
locale: 'en-US',
|
|
||||||
pathname: '/test',
|
|
||||||
referrer: 'https://example.com',
|
|
||||||
sessionId: 'test-session-id',
|
|
||||||
timeZone: 'UTC',
|
|
||||||
userAgent: 'test-user-agent',
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockEvent = makeTrackEvent(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
mockClickhouseClient = {
|
|
||||||
insert: jest.fn().mockResolvedValue({}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
ClickhouseService,
|
|
||||||
{
|
|
||||||
provide: TwentyConfigService,
|
|
||||||
useValue: {
|
|
||||||
get: jest.fn((key) => {
|
|
||||||
if (key === 'ANALYTICS_ENABLED') return true;
|
|
||||||
if (key === 'CLICKHOUSE_URL') return 'http://localhost:8123';
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ExceptionHandlerService,
|
|
||||||
useValue: {
|
|
||||||
captureExceptions: jest.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<ClickhouseService>(ClickhouseService);
|
|
||||||
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
|
||||||
exceptionHandlerService = module.get<ExceptionHandlerService>(
|
|
||||||
ExceptionHandlerService,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set the mock client
|
|
||||||
// @ts-expect-error accessing private property for testing
|
|
||||||
service.clickhouseClient = mockClickhouseClient;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(service).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('constructor', () => {
|
|
||||||
it('should not initialize clickhouse client when analytics is disabled', async () => {
|
|
||||||
jest.spyOn(twentyConfigService, 'get').mockImplementation((key) => {
|
|
||||||
if (key === 'ANALYTICS_ENABLED') return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const newModule: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [
|
|
||||||
ClickhouseService,
|
|
||||||
{
|
|
||||||
provide: TwentyConfigService,
|
|
||||||
useValue: twentyConfigService,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ExceptionHandlerService,
|
|
||||||
useValue: exceptionHandlerService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
const newService = newModule.get<ClickhouseService>(ClickhouseService);
|
|
||||||
|
|
||||||
// @ts-expect-error accessing private property for testing
|
|
||||||
expect(newService.clickhouseClient).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('pushEvent', () => {
|
|
||||||
it('should insert event into clickhouse and return success', async () => {
|
|
||||||
const result = await service.pushEvent(mockEvent);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
const { type: _type, ...rest } = mockEvent;
|
|
||||||
|
|
||||||
expect(mockClickhouseClient.insert).toHaveBeenCalledWith({
|
|
||||||
table: 'events',
|
|
||||||
values: [rest],
|
|
||||||
format: 'JSONEachRow',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should insert pageview into clickhouse and return success', async () => {
|
|
||||||
const result = await service.pushEvent(mockPageview);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
const { type: _type, ...rest } = mockPageview;
|
|
||||||
|
|
||||||
expect(mockClickhouseClient.insert).toHaveBeenCalledWith({
|
|
||||||
table: 'pageview',
|
|
||||||
values: [rest],
|
|
||||||
format: 'JSONEachRow',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return success when clickhouse client is not defined', async () => {
|
|
||||||
// @ts-expect-error accessing private property for testing
|
|
||||||
service.clickhouseClient = undefined;
|
|
||||||
|
|
||||||
const result = await service.pushEvent(mockEvent);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle errors and return failure', async () => {
|
|
||||||
const testError = new Error('Test error');
|
|
||||||
|
|
||||||
mockClickhouseClient.insert.mockRejectedValueOnce(testError);
|
|
||||||
|
|
||||||
const result = await service.pushEvent(mockEvent);
|
|
||||||
|
|
||||||
expect(result).toEqual({ success: false });
|
|
||||||
expect(exceptionHandlerService.captureExceptions).toHaveBeenCalledWith([
|
|
||||||
testError,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { ClickHouseClient, createClient } from '@clickhouse/client';
|
|
||||||
|
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
|
||||||
import {
|
|
||||||
makePageview,
|
|
||||||
makeTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/analytics.utils';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ClickhouseService {
|
|
||||||
private clickhouseClient: ClickHouseClient | undefined;
|
|
||||||
constructor(
|
|
||||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
|
||||||
) {
|
|
||||||
if (twentyConfigService.get('ANALYTICS_ENABLED')) {
|
|
||||||
this.clickhouseClient = createClient({
|
|
||||||
url: twentyConfigService.get('CLICKHOUSE_URL'),
|
|
||||||
compression: {
|
|
||||||
response: true,
|
|
||||||
request: true,
|
|
||||||
},
|
|
||||||
clickhouse_settings: {
|
|
||||||
async_insert: 1,
|
|
||||||
wait_for_async_insert: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async pushEvent(
|
|
||||||
data: (
|
|
||||||
| ReturnType<typeof makeTrackEvent>
|
|
||||||
| ReturnType<typeof makePageview>
|
|
||||||
) & { userId?: string | null; workspaceId?: string | null },
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (!this.clickhouseClient) {
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type, ...rest } = data;
|
|
||||||
|
|
||||||
await this.clickhouseClient.insert({
|
|
||||||
table: type === 'page' ? 'pageview' : 'events',
|
|
||||||
values: [rest],
|
|
||||||
format: 'JSONEachRow',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (err) {
|
|
||||||
this.exceptionHandlerService.captureExceptions([err]);
|
|
||||||
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export type AnalyticsCommonPropertiesType = 'timestamp' | 'version';
|
|
||||||
export type IdentifierType = 'workspaceId' | 'userId';
|
|
||||||
@ -6,20 +6,20 @@ This module provides analytics tracking functionality for the Twenty application
|
|||||||
|
|
||||||
### Tracking Events
|
### Tracking Events
|
||||||
|
|
||||||
The `AnalyticsService` provides a `createAnalyticsContext` method that returns an object with a `track` method. The `track` method is used to track events.
|
The `AuditService` provides a `createContext` method that returns an object with a `track` method. The `track` method is used to track events.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MyService {
|
export class MyService {
|
||||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
constructor(private readonly auditService: AuditService) {}
|
||||||
|
|
||||||
async doSomething() {
|
async doSomething() {
|
||||||
// Create an analytics context
|
// Create an analytics context
|
||||||
const analytics = this.analyticsService.createAnalyticsContext({
|
const analytics = this.auditService.createContext({
|
||||||
workspaceId: 'workspace-id',
|
workspaceId: 'workspace-id',
|
||||||
userId: 'user-id',
|
userId: 'user-id',
|
||||||
});
|
});
|
||||||
@ -87,9 +87,9 @@ export type TrackEventProperties<T extends TrackEventName> = T extends keyof Tra
|
|||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### AnalyticsService
|
### AuditService
|
||||||
|
|
||||||
#### createAnalyticsContext(context?)
|
#### createContext(context?)
|
||||||
|
|
||||||
Creates an analytics context with the given user ID and workspace ID.
|
Creates an analytics context with the given user ID and workspace ID.
|
||||||
|
|
||||||
@ -1,12 +1,12 @@
|
|||||||
import { CustomException } from 'src/utils/custom-exception';
|
import { CustomException } from 'src/utils/custom-exception';
|
||||||
|
|
||||||
export class AnalyticsException extends CustomException {
|
export class AuditException extends CustomException {
|
||||||
constructor(message: string, code: AnalyticsExceptionCode) {
|
constructor(message: string, code: AuditExceptionCode) {
|
||||||
super(message, code);
|
super(message, code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AnalyticsExceptionCode {
|
export enum AuditExceptionCode {
|
||||||
INVALID_TYPE = 'INVALID_TYPE',
|
INVALID_TYPE = 'INVALID_TYPE',
|
||||||
INVALID_INPUT = 'INVALID_INPUT',
|
INVALID_INPUT = 'INVALID_INPUT',
|
||||||
}
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ClickHouseModule } from 'src/database/clickHouse/clickHouse.module';
|
||||||
|
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
|
||||||
|
|
||||||
|
import { AuditResolver } from './audit.resolver';
|
||||||
|
|
||||||
|
import { AuditService } from './services/audit.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [AuditResolver, AuditService],
|
||||||
|
imports: [JwtModule, ClickHouseModule],
|
||||||
|
exports: [AuditService],
|
||||||
|
})
|
||||||
|
export class AuditModule {}
|
||||||
@ -1,36 +1,36 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
import {
|
import {
|
||||||
AnalyticsException,
|
AuditException,
|
||||||
AnalyticsExceptionCode,
|
AuditExceptionCode,
|
||||||
} from 'src/engine/core-modules/analytics/analytics.exception';
|
} from 'src/engine/core-modules/audit/audit.exception';
|
||||||
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
import { AnalyticsResolver } from './analytics.resolver';
|
import { AuditResolver } from './audit.resolver';
|
||||||
|
|
||||||
import { AnalyticsService } from './services/analytics.service';
|
import { AuditService } from './services/audit.service';
|
||||||
|
|
||||||
describe('AnalyticsResolver', () => {
|
describe('AuditResolver', () => {
|
||||||
let resolver: AnalyticsResolver;
|
let resolver: AuditResolver;
|
||||||
let analyticsService: jest.Mocked<AnalyticsService>;
|
let auditService: jest.Mocked<AuditService>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
analyticsService = {
|
auditService = {
|
||||||
createAnalyticsContext: jest.fn(),
|
createContext: jest.fn(),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
AnalyticsResolver,
|
AuditResolver,
|
||||||
{
|
{
|
||||||
provide: AnalyticsService,
|
provide: AuditService,
|
||||||
useValue: analyticsService,
|
useValue: auditService,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
resolver = module.get<AnalyticsResolver>(AnalyticsResolver);
|
resolver = module.get<AuditResolver>(AuditResolver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@ -40,7 +40,7 @@ describe('AnalyticsResolver', () => {
|
|||||||
it('should handle a valid pageview input', async () => {
|
it('should handle a valid pageview input', async () => {
|
||||||
const mockPageview = jest.fn().mockResolvedValue('Pageview created');
|
const mockPageview = jest.fn().mockResolvedValue('Pageview created');
|
||||||
|
|
||||||
analyticsService.createAnalyticsContext.mockReturnValue({
|
auditService.createContext.mockReturnValue({
|
||||||
pageview: mockPageview,
|
pageview: mockPageview,
|
||||||
track: jest.fn(),
|
track: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -56,7 +56,7 @@ describe('AnalyticsResolver', () => {
|
|||||||
{ id: 'user-1' } as User,
|
{ id: 'user-1' } as User,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(analyticsService.createAnalyticsContext).toHaveBeenCalledWith({
|
expect(auditService.createContext).toHaveBeenCalledWith({
|
||||||
workspaceId: 'workspace-1',
|
workspaceId: 'workspace-1',
|
||||||
userId: 'user-1',
|
userId: 'user-1',
|
||||||
});
|
});
|
||||||
@ -67,7 +67,7 @@ describe('AnalyticsResolver', () => {
|
|||||||
it('should handle a valid track input', async () => {
|
it('should handle a valid track input', async () => {
|
||||||
const mockTrack = jest.fn().mockResolvedValue('Track created');
|
const mockTrack = jest.fn().mockResolvedValue('Track created');
|
||||||
|
|
||||||
analyticsService.createAnalyticsContext.mockReturnValue({
|
auditService.createContext.mockReturnValue({
|
||||||
track: mockTrack,
|
track: mockTrack,
|
||||||
pageview: jest.fn(),
|
pageview: jest.fn(),
|
||||||
});
|
});
|
||||||
@ -83,7 +83,7 @@ describe('AnalyticsResolver', () => {
|
|||||||
{ id: 'user-2' } as User,
|
{ id: 'user-2' } as User,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(analyticsService.createAnalyticsContext).toHaveBeenCalledWith({
|
expect(auditService.createContext).toHaveBeenCalledWith({
|
||||||
workspaceId: 'workspace-2',
|
workspaceId: 'workspace-2',
|
||||||
userId: 'user-2',
|
userId: 'user-2',
|
||||||
});
|
});
|
||||||
@ -91,15 +91,15 @@ describe('AnalyticsResolver', () => {
|
|||||||
expect(result).toBe('Track created');
|
expect(result).toBe('Track created');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an AnalyticsException for invalid input', async () => {
|
it('should throw an AuditException for invalid input', async () => {
|
||||||
const invalidInput = { type: 'invalid' };
|
const invalidInput = { type: 'invalid' };
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
resolver.trackAnalytics(invalidInput as any, undefined, undefined),
|
resolver.trackAnalytics(invalidInput as any, undefined, undefined),
|
||||||
).rejects.toThrowError(
|
).rejects.toThrowError(
|
||||||
new AnalyticsException(
|
new AuditException(
|
||||||
'Invalid analytics input',
|
'Invalid analytics input',
|
||||||
AnalyticsExceptionCode.INVALID_TYPE,
|
AuditExceptionCode.INVALID_TYPE,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1,35 +1,34 @@
|
|||||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuditException,
|
||||||
|
AuditExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/audit/audit.exception';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import {
|
|
||||||
AnalyticsException,
|
|
||||||
AnalyticsExceptionCode,
|
|
||||||
} from 'src/engine/core-modules/analytics/analytics.exception';
|
|
||||||
|
|
||||||
import { AnalyticsService } from './services/analytics.service';
|
|
||||||
import {
|
import {
|
||||||
CreateAnalyticsInput,
|
|
||||||
CreateAnalyticsInputV2,
|
CreateAnalyticsInputV2,
|
||||||
isPageviewAnalyticsInput,
|
isPageviewAnalyticsInput,
|
||||||
isTrackAnalyticsInput,
|
isTrackAnalyticsInput,
|
||||||
} from './dtos/create-analytics.input';
|
} from './dtos/create-analytics.input';
|
||||||
import { Analytics } from './entities/analytics.entity';
|
import { Analytics } from './entities/analytics.entity';
|
||||||
|
import { AuditService } from './services/audit.service';
|
||||||
|
|
||||||
@Resolver(() => Analytics)
|
@Resolver(() => Analytics)
|
||||||
export class AnalyticsResolver {
|
export class AuditResolver {
|
||||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
constructor(private readonly auditService: AuditService) {}
|
||||||
|
|
||||||
// deprecated
|
// preparing for new name
|
||||||
@Mutation(() => Analytics)
|
async auditTrack(
|
||||||
track(
|
@Args()
|
||||||
@Args() _createAnalyticsInput: CreateAnalyticsInput,
|
createAnalyticsInput: CreateAnalyticsInputV2,
|
||||||
@AuthWorkspace() _workspace: Workspace | undefined,
|
@AuthWorkspace() workspace: Workspace | undefined,
|
||||||
@AuthUser({ allowUndefined: true }) _user: User | undefined,
|
@AuthUser({ allowUndefined: true }) user: User | undefined,
|
||||||
) {
|
) {
|
||||||
return { success: true };
|
return this.trackAnalytics(createAnalyticsInput, workspace, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Analytics)
|
@Mutation(() => Analytics)
|
||||||
@ -39,7 +38,7 @@ export class AnalyticsResolver {
|
|||||||
@AuthWorkspace() workspace: Workspace | undefined,
|
@AuthWorkspace() workspace: Workspace | undefined,
|
||||||
@AuthUser({ allowUndefined: true }) user: User | undefined,
|
@AuthUser({ allowUndefined: true }) user: User | undefined,
|
||||||
) {
|
) {
|
||||||
const analyticsContext = this.analyticsService.createAnalyticsContext({
|
const analyticsContext = this.auditService.createContext({
|
||||||
workspaceId: workspace?.id,
|
workspaceId: workspace?.id,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
});
|
});
|
||||||
@ -58,9 +57,9 @@ export class AnalyticsResolver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new AnalyticsException(
|
throw new AuditException(
|
||||||
'Invalid analytics input',
|
'Invalid analytics input',
|
||||||
AnalyticsExceptionCode.INVALID_TYPE,
|
AuditExceptionCode.INVALID_TYPE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -9,8 +9,8 @@ import {
|
|||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import GraphQLJSON from 'graphql-type-json';
|
import GraphQLJSON from 'graphql-type-json';
|
||||||
|
|
||||||
import { TrackEventName } from 'src/engine/core-modules/analytics/types/events.type';
|
import { TrackEventName } from 'src/engine/core-modules/audit/types/events.type';
|
||||||
import { PageviewProperties } from 'src/engine/core-modules/analytics/utils/events/pageview/pageview';
|
import { PageviewProperties } from 'src/engine/core-modules/audit/utils/events/pageview/pageview';
|
||||||
|
|
||||||
enum AnalyticsType {
|
enum AnalyticsType {
|
||||||
PAGEVIEW = 'pageview',
|
PAGEVIEW = 'pageview',
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
|
import { CreateAuditLogFromInternalEvent } from 'src/engine/core-modules/audit/jobs/create-audit-log-from-internal-event';
|
||||||
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
|
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||||
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||||
|
TimelineActivityModule,
|
||||||
|
AuditModule,
|
||||||
|
],
|
||||||
|
providers: [CreateAuditLogFromInternalEvent],
|
||||||
|
})
|
||||||
|
export class AuditJobModule {}
|
||||||
@ -1,26 +1,22 @@
|
|||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
||||||
|
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
|
||||||
|
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
|
||||||
import { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
|
import { ObjectRecordEvent } from 'src/engine/core-modules/event-emitter/types/object-record-event.event';
|
||||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository';
|
|
||||||
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
|
|
||||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-updated';
|
|
||||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
|
|
||||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-delete';
|
|
||||||
|
|
||||||
@Processor(MessageQueue.entityEventsToDbQueue)
|
@Processor(MessageQueue.entityEventsToDbQueue)
|
||||||
export class CreateAuditLogFromInternalEvent {
|
export class CreateAuditLogFromInternalEvent {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
@InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity)
|
||||||
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
private readonly workspaceMemberService: WorkspaceMemberRepository,
|
||||||
@InjectObjectMetadataRepository(AuditLogWorkspaceEntity)
|
private readonly auditService: AuditService,
|
||||||
private readonly auditLogRepository: AuditLogRepository,
|
|
||||||
private readonly analyticsService: AnalyticsService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(CreateAuditLogFromInternalEvent.name)
|
@Process(CreateAuditLogFromInternalEvent.name)
|
||||||
@ -28,43 +24,26 @@ export class CreateAuditLogFromInternalEvent {
|
|||||||
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>,
|
workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
for (const eventData of workspaceEventBatch.events) {
|
for (const eventData of workspaceEventBatch.events) {
|
||||||
let workspaceMemberId: string | null = null;
|
// We remove "before" and "after" property for a cleaner/slimmer event payload
|
||||||
|
const eventProperties =
|
||||||
if (eventData.userId) {
|
|
||||||
const workspaceMember = await this.workspaceMemberService.getByIdOrFail(
|
|
||||||
eventData.userId,
|
|
||||||
workspaceEventBatch.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
workspaceMemberId = workspaceMember.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.auditLogRepository.insert(
|
|
||||||
workspaceEventBatch.name,
|
|
||||||
'diff' in eventData.properties
|
'diff' in eventData.properties
|
||||||
? {
|
? {
|
||||||
// we remove "before" and "after" property for a cleaner/slimmer event payload
|
...eventData.properties,
|
||||||
diff: eventData.properties.diff,
|
diff: eventData.properties.diff,
|
||||||
}
|
}
|
||||||
: eventData.properties,
|
: eventData.properties;
|
||||||
workspaceMemberId,
|
|
||||||
workspaceEventBatch.name.split('.')[0],
|
|
||||||
eventData.objectMetadata.id,
|
|
||||||
eventData.recordId,
|
|
||||||
workspaceEventBatch.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
const analytics = this.analyticsService.createAnalyticsContext({
|
const analytics = this.auditService.createContext({
|
||||||
workspaceId: workspaceEventBatch.workspaceId,
|
workspaceId: workspaceEventBatch.workspaceId,
|
||||||
userId: eventData.userId,
|
userId: eventData.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspaceEventBatch.name.endsWith('.updated')) {
|
if (workspaceEventBatch.name.endsWith('.updated')) {
|
||||||
analytics.track(OBJECT_RECORD_UPDATED_EVENT, eventData.properties);
|
analytics.track(OBJECT_RECORD_UPDATED_EVENT, eventProperties);
|
||||||
} else if (workspaceEventBatch.name.endsWith('.created')) {
|
} else if (workspaceEventBatch.name.endsWith('.created')) {
|
||||||
analytics.track(OBJECT_RECORD_CREATED_EVENT, eventData.properties);
|
analytics.track(OBJECT_RECORD_CREATED_EVENT, eventProperties);
|
||||||
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
|
} else if (workspaceEventBatch.name.endsWith('.deleted')) {
|
||||||
analytics.track(OBJECT_RECORD_DELETED_EVENT, eventData.properties);
|
analytics.track(OBJECT_RECORD_DELETED_EVENT, eventProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,24 +1,24 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
import { AnalyticsContextMock } from 'test/utils/analytics-context.mock';
|
import { AuditContextMock } from 'test/utils/audit-context.mock';
|
||||||
|
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
|
||||||
import { ClickhouseService } from 'src/engine/core-modules/analytics/services/clickhouse.service';
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
||||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
import { AnalyticsService } from './analytics.service';
|
import { AuditService } from './audit.service';
|
||||||
|
|
||||||
describe('AnalyticsService', () => {
|
describe('AuditService', () => {
|
||||||
let service: AnalyticsService;
|
let service: AuditService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: AnalyticsService,
|
provide: AuditService,
|
||||||
useValue: {
|
useValue: {
|
||||||
createAnalyticsContext: AnalyticsContextMock,
|
createContext: AuditContextMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,7 +28,7 @@ describe('AnalyticsService', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: ClickhouseService,
|
provide: ClickHouseService,
|
||||||
useValue: {
|
useValue: {
|
||||||
pushEvent: jest.fn(),
|
pushEvent: jest.fn(),
|
||||||
},
|
},
|
||||||
@ -42,21 +42,21 @@ describe('AnalyticsService', () => {
|
|||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<AnalyticsService>(AnalyticsService);
|
service = module.get<AuditService>(AuditService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createAnalyticsContext', () => {
|
describe('createContext', () => {
|
||||||
const mockUserIdAndWorkspaceId = {
|
const mockUserIdAndWorkspaceId = {
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
workspaceId: 'test-workspace-id',
|
workspaceId: 'test-workspace-id',
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should create a valid context object', () => {
|
it('should create a valid context object', () => {
|
||||||
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
expect(context).toHaveProperty('track');
|
expect(context).toHaveProperty('track');
|
||||||
expect(context).toHaveProperty('pageview');
|
expect(context).toHaveProperty('pageview');
|
||||||
@ -64,15 +64,13 @@ describe('AnalyticsService', () => {
|
|||||||
|
|
||||||
it('should call track with correct parameters', async () => {
|
it('should call track with correct parameters', async () => {
|
||||||
const trackSpy = jest.fn().mockResolvedValue({ success: true });
|
const trackSpy = jest.fn().mockResolvedValue({ success: true });
|
||||||
const mockContext = AnalyticsContextMock({
|
const mockContext = AuditContextMock({
|
||||||
track: trackSpy,
|
track: trackSpy,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest
|
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
|
||||||
.spyOn(service, 'createAnalyticsContext')
|
|
||||||
.mockReturnValue(mockContext);
|
|
||||||
|
|
||||||
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
||||||
|
|
||||||
@ -81,15 +79,13 @@ describe('AnalyticsService', () => {
|
|||||||
|
|
||||||
it('should call pageview with correct parameters', async () => {
|
it('should call pageview with correct parameters', async () => {
|
||||||
const pageviewSpy = jest.fn().mockResolvedValue({ success: true });
|
const pageviewSpy = jest.fn().mockResolvedValue({ success: true });
|
||||||
const mockContext = AnalyticsContextMock({
|
const mockContext = AuditContextMock({
|
||||||
pageview: pageviewSpy,
|
pageview: pageviewSpy,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest
|
jest.spyOn(service, 'createContext').mockReturnValue(mockContext);
|
||||||
.spyOn(service, 'createAnalyticsContext')
|
|
||||||
.mockReturnValue(mockContext);
|
|
||||||
|
|
||||||
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
const testPageviewProperties = {
|
const testPageviewProperties = {
|
||||||
href: '/test-url',
|
href: '/test-url',
|
||||||
locale: '',
|
locale: '',
|
||||||
@ -109,7 +105,7 @@ describe('AnalyticsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return success when track is called', async () => {
|
it('should return success when track is called', async () => {
|
||||||
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
const result = await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
const result = await context.track(CUSTOM_DOMAIN_ACTIVATED_EVENT, {});
|
||||||
|
|
||||||
@ -117,7 +113,7 @@ describe('AnalyticsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return success when pageview is called', async () => {
|
it('should return success when pageview is called', async () => {
|
||||||
const context = service.createAnalyticsContext(mockUserIdAndWorkspaceId);
|
const context = service.createContext(mockUserIdAndWorkspaceId);
|
||||||
|
|
||||||
const result = await context.pageview('page-view', {});
|
const result = await context.pageview('page-view', {});
|
||||||
|
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ClickHouseService } from 'src/database/clickHouse/clickHouse.service';
|
||||||
|
import {
|
||||||
|
AuditException,
|
||||||
|
AuditExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/audit/audit.exception';
|
||||||
|
import {
|
||||||
|
TrackEventName,
|
||||||
|
TrackEventProperties,
|
||||||
|
} from 'src/engine/core-modules/audit/types/events.type';
|
||||||
|
import {
|
||||||
|
makePageview,
|
||||||
|
makeTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/analytics.utils';
|
||||||
|
import { PageviewProperties } from 'src/engine/core-modules/audit/utils/events/pageview/pageview';
|
||||||
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuditService {
|
||||||
|
constructor(
|
||||||
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
|
private readonly clickHouseService: ClickHouseService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
createContext(context?: {
|
||||||
|
workspaceId?: string | null | undefined;
|
||||||
|
userId?: string | null | undefined;
|
||||||
|
}) {
|
||||||
|
const userIdAndWorkspaceId = context
|
||||||
|
? {
|
||||||
|
...(context.userId ? { userId: context.userId } : {}),
|
||||||
|
...(context.workspaceId ? { workspaceId: context.workspaceId } : {}),
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
track: <T extends TrackEventName>(
|
||||||
|
event: T,
|
||||||
|
properties: TrackEventProperties<T>,
|
||||||
|
) =>
|
||||||
|
this.preventIfDisabled(() =>
|
||||||
|
this.clickHouseService.insert('auditEvent', [
|
||||||
|
{ ...userIdAndWorkspaceId, ...makeTrackEvent(event, properties) },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
pageview: (name: string, properties: Partial<PageviewProperties>) =>
|
||||||
|
this.preventIfDisabled(() =>
|
||||||
|
this.clickHouseService.insert('pageview', [
|
||||||
|
{ ...userIdAndWorkspaceId, ...makePageview(name, properties) },
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private preventIfDisabled(
|
||||||
|
sendEventOrPageviewFunction: () => Promise<{ success: boolean }>,
|
||||||
|
) {
|
||||||
|
if (!this.twentyConfigService.get('CLICKHOUSE_URL')) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return sendEventOrPageviewFunction();
|
||||||
|
} catch (err) {
|
||||||
|
return new AuditException(err, AuditExceptionCode.INVALID_INPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export type AuditCommonPropertiesType = 'timestamp' | 'version';
|
||||||
|
export type IdentifierType = 'workspaceId' | 'userId';
|
||||||
@ -1,43 +1,43 @@
|
|||||||
import {
|
|
||||||
WEBHOOK_RESPONSE_EVENT,
|
|
||||||
WebhookResponseTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/webhook/webhook-response';
|
|
||||||
import {
|
|
||||||
SERVERLESS_FUNCTION_EXECUTED_EVENT,
|
|
||||||
ServerlessFunctionExecutedTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/serverless-function/serverless-function-executed';
|
|
||||||
import {
|
|
||||||
CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
|
||||||
CustomDomainDeactivatedTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-deactivated';
|
|
||||||
import {
|
import {
|
||||||
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
CUSTOM_DOMAIN_ACTIVATED_EVENT,
|
||||||
CustomDomainActivatedTrackEvent,
|
CustomDomainActivatedTrackEvent,
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
} from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
||||||
import {
|
import {
|
||||||
WORKSPACE_ENTITY_CREATED_EVENT,
|
CUSTOM_DOMAIN_DEACTIVATED_EVENT,
|
||||||
WorkspaceEntityCreatedTrackEvent,
|
CustomDomainDeactivatedTrackEvent,
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/workspace-entity/workspace-entity-created';
|
} from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
|
||||||
import {
|
|
||||||
USER_SIGNUP_EVENT,
|
|
||||||
UserSignupTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/user/user-signup';
|
|
||||||
import {
|
import {
|
||||||
MONITORING_EVENT,
|
MONITORING_EVENT,
|
||||||
MonitoringTrackEvent,
|
MonitoringTrackEvent,
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/monitoring/monitoring';
|
} from 'src/engine/core-modules/audit/utils/events/track/monitoring/monitoring';
|
||||||
import {
|
import {
|
||||||
OBJECT_RECORD_CREATED_EVENT,
|
OBJECT_RECORD_CREATED_EVENT,
|
||||||
ObjectRecordCreatedTrackEvent,
|
ObjectRecordCreatedTrackEvent,
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
|
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
||||||
import {
|
|
||||||
OBJECT_RECORD_UPDATED_EVENT,
|
|
||||||
ObjectRecordUpdatedTrackEvent,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-updated';
|
|
||||||
import {
|
import {
|
||||||
OBJECT_RECORD_DELETED_EVENT,
|
OBJECT_RECORD_DELETED_EVENT,
|
||||||
ObjectRecordDeletedTrackEvent,
|
ObjectRecordDeletedTrackEvent,
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-delete';
|
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-delete';
|
||||||
|
import {
|
||||||
|
OBJECT_RECORD_UPDATED_EVENT,
|
||||||
|
ObjectRecordUpdatedTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-updated';
|
||||||
|
import {
|
||||||
|
SERVERLESS_FUNCTION_EXECUTED_EVENT,
|
||||||
|
ServerlessFunctionExecutedTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/track/serverless-function/serverless-function-executed';
|
||||||
|
import {
|
||||||
|
USER_SIGNUP_EVENT,
|
||||||
|
UserSignupTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/track/user/user-signup';
|
||||||
|
import {
|
||||||
|
WEBHOOK_RESPONSE_EVENT,
|
||||||
|
WebhookResponseTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/track/webhook/webhook-response';
|
||||||
|
import {
|
||||||
|
WORKSPACE_ENTITY_CREATED_EVENT,
|
||||||
|
WorkspaceEntityCreatedTrackEvent,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/track/workspace-entity/workspace-entity-created';
|
||||||
|
|
||||||
// Define all track event names
|
// Define all track event names
|
||||||
export type TrackEventName =
|
export type TrackEventName =
|
||||||
@ -1,20 +1,20 @@
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
import { AnalyticsCommonPropertiesType } from 'src/engine/core-modules/analytics/types/common.type';
|
import { AuditCommonPropertiesType } from 'src/engine/core-modules/audit/types/common.type';
|
||||||
import {
|
|
||||||
PageviewProperties,
|
|
||||||
pageviewSchema,
|
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/pageview/pageview';
|
|
||||||
import {
|
import {
|
||||||
TrackEventName,
|
TrackEventName,
|
||||||
TrackEventProperties,
|
TrackEventProperties,
|
||||||
} from 'src/engine/core-modules/analytics/types/events.type';
|
} from 'src/engine/core-modules/audit/types/events.type';
|
||||||
|
import {
|
||||||
|
PageviewProperties,
|
||||||
|
pageviewSchema,
|
||||||
|
} from 'src/engine/core-modules/audit/utils/events/pageview/pageview';
|
||||||
import {
|
import {
|
||||||
eventsRegistry,
|
eventsRegistry,
|
||||||
GenericTrackEvent,
|
GenericTrackEvent,
|
||||||
} from 'src/engine/core-modules/analytics/utils/events/track/track';
|
} from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
const common = (): Record<AnalyticsCommonPropertiesType, string> => ({
|
const common = (): Record<AuditCommonPropertiesType, string> => ({
|
||||||
timestamp: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
timestamp: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||||
version: '1',
|
version: '1',
|
||||||
});
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { baseEventSchema } from 'src/engine/core-modules/analytics/utils/events/common/base-schemas';
|
import { baseEventSchema } from 'src/engine/core-modules/audit/utils/events/common/base-schemas';
|
||||||
|
|
||||||
export const pageviewSchema = baseEventSchema.extend({
|
export const pageviewSchema = baseEventSchema.extend({
|
||||||
type: z.literal('page'),
|
type: z.literal('page'),
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
|
export const CUSTOM_DOMAIN_ACTIVATED_EVENT = 'Custom Domain Activated' as const;
|
||||||
export const customDomainActivatedSchema = z
|
export const customDomainActivatedSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
|
export const CUSTOM_DOMAIN_DEACTIVATED_EVENT =
|
||||||
'Custom Domain Deactivated' as const;
|
'Custom Domain Deactivated' as const;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const MONITORING_EVENT = 'Monitoring' as const;
|
export const MONITORING_EVENT = 'Monitoring' as const;
|
||||||
export const monitoringSchema = z
|
export const monitoringSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
|
export const OBJECT_RECORD_CREATED_EVENT = 'Object Record Created' as const;
|
||||||
export const objectRecordCreatedSchema = z.object({
|
export const objectRecordCreatedSchema = z.object({
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
|
export const OBJECT_RECORD_DELETED_EVENT = 'Object Record Deleted' as const;
|
||||||
export const objectRecordDeletedSchema = z.object({
|
export const objectRecordDeletedSchema = z.object({
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
|
export const OBJECT_RECORD_UPDATED_EVENT = 'Object Record Updated' as const;
|
||||||
export const objectRecordUpdatedSchema = z.object({
|
export const objectRecordUpdatedSchema = z.object({
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
|
export const SERVERLESS_FUNCTION_EXECUTED_EVENT =
|
||||||
'Serverless Function Executed' as const;
|
'Serverless Function Executed' as const;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { baseEventSchema } from 'src/engine/core-modules/analytics/utils/events/common/base-schemas';
|
import { baseEventSchema } from 'src/engine/core-modules/audit/utils/events/common/base-schemas';
|
||||||
|
|
||||||
export const genericTrackSchema = baseEventSchema.extend({
|
export const genericTrackSchema = baseEventSchema.extend({
|
||||||
type: z.literal('track'),
|
type: z.literal('track'),
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const USER_SIGNUP_EVENT = 'User Signup' as const;
|
export const USER_SIGNUP_EVENT = 'User Signup' as const;
|
||||||
export const userSignupSchema = z
|
export const userSignupSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
|
export const WEBHOOK_RESPONSE_EVENT = 'Webhook Response' as const;
|
||||||
export const webhookResponseSchema = z
|
export const webhookResponseSchema = z
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { registerEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { registerEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
export const WORKSPACE_ENTITY_CREATED_EVENT =
|
export const WORKSPACE_ENTITY_CREATED_EVENT =
|
||||||
'Workspace Entity Created' as const;
|
'Workspace Entity Created' as const;
|
||||||
@ -51,7 +51,7 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
|||||||
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
||||||
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
|
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
|
||||||
|
|
||||||
import { AnalyticsModule } from './analytics/analytics.module';
|
import { AuditModule } from './audit/audit.module';
|
||||||
import { ClientConfigModule } from './client-config/client-config.module';
|
import { ClientConfigModule } from './client-config/client-config.module';
|
||||||
import { FileModule } from './file/file.module';
|
import { FileModule } from './file/file.module';
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ import { FileModule } from './file/file.module';
|
|||||||
imports: [
|
imports: [
|
||||||
TwentyConfigModule.forRoot(),
|
TwentyConfigModule.forRoot(),
|
||||||
HealthModule,
|
HealthModule,
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
ClientConfigModule,
|
ClientConfigModule,
|
||||||
@ -128,7 +128,7 @@ import { FileModule } from './file/file.module';
|
|||||||
SearchModule,
|
SearchModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
TimelineMessagingModule,
|
TimelineMessagingModule,
|
||||||
|
|||||||
@ -13,8 +13,8 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
||||||
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||||
import {
|
import {
|
||||||
DomainManagerException,
|
DomainManagerException,
|
||||||
@ -36,7 +36,7 @@ export class CloudflareController {
|
|||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly customDomainService: CustomDomainService,
|
private readonly customDomainService: CustomDomainService,
|
||||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly auditService: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare'])
|
@Post(['cloudflare/custom-hostname-webhooks', 'webhooks/cloudflare'])
|
||||||
@ -60,7 +60,7 @@ export class CloudflareController {
|
|||||||
|
|
||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
|
|
||||||
const analytics = this.analyticsService.createAnalyticsContext({
|
const analytics = this.auditService.createContext({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { AuditContextMock } from 'test/utils/audit-context.mock';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { AnalyticsContextMock } from 'test/utils/analytics-context.mock';
|
|
||||||
|
|
||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller';
|
import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller';
|
||||||
import { CustomDomainValidRecords } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records';
|
import { CustomDomainValidRecords } from 'src/engine/core-modules/domain-manager/dtos/custom-domain-valid-records';
|
||||||
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
||||||
@ -13,7 +14,6 @@ import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handl
|
|||||||
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service';
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
|
|
||||||
describe('CloudflareController - customHostnameWebhooks', () => {
|
describe('CloudflareController - customHostnameWebhooks', () => {
|
||||||
let controller: CloudflareController;
|
let controller: CloudflareController;
|
||||||
@ -64,9 +64,9 @@ describe('CloudflareController - customHostnameWebhooks', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AnalyticsService,
|
provide: AuditService,
|
||||||
useValue: {
|
useValue: {
|
||||||
createAnalyticsContext: AnalyticsContextMock,
|
createContext: AuditContextMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller';
|
import { CloudflareController } from 'src/engine/core-modules/domain-manager/controllers/cloudflare.controller';
|
||||||
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AnalyticsModule, TypeOrmModule.forFeature([Workspace], 'core')],
|
imports: [AuditModule, TypeOrmModule.forFeature([Workspace], 'core')],
|
||||||
providers: [DomainManagerService, CustomDomainService],
|
providers: [DomainManagerService, CustomDomainService],
|
||||||
exports: [DomainManagerService, CustomDomainService],
|
exports: [DomainManagerService, CustomDomainService],
|
||||||
controllers: [CloudflareController],
|
controllers: [CloudflareController],
|
||||||
|
|||||||
@ -2,12 +2,12 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
|
|
||||||
import Cloudflare from 'cloudflare';
|
import Cloudflare from 'cloudflare';
|
||||||
import { CustomHostnameCreateResponse } from 'cloudflare/resources/custom-hostnames/custom-hostnames';
|
import { CustomHostnameCreateResponse } from 'cloudflare/resources/custom-hostnames/custom-hostnames';
|
||||||
import { AnalyticsContextMock } from 'test/utils/analytics-context.mock';
|
import { AuditContextMock } from 'test/utils/audit-context.mock';
|
||||||
|
|
||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
import { DomainManagerException } from 'src/engine/core-modules/domain-manager/domain-manager.exception';
|
||||||
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
||||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||||
import { DomainManagerException } from 'src/engine/core-modules/domain-manager/domain-manager.exception';
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||||
|
|
||||||
jest.mock('cloudflare');
|
jest.mock('cloudflare');
|
||||||
@ -28,9 +28,9 @@ describe('CustomDomainService', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AnalyticsService,
|
provide: AuditService,
|
||||||
useValue: {
|
useValue: {
|
||||||
createAnalyticsContext: AnalyticsContextMock,
|
createContext: AuditContextMock,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IsEventObjectEnabled = 'IS_EVENT_OBJECT_ENABLED',
|
|
||||||
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
|
||||||
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
|
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
|
||||||
IsCopilotEnabled = 'IS_COPILOT_ENABLED',
|
IsCopilotEnabled = 'IS_COPILOT_ENABLED',
|
||||||
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
|
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
|
||||||
IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED',
|
|
||||||
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
|
IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED',
|
||||||
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
|
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
|
||||||
IsCustomDomainEnabled = 'IS_CUSTOM_DOMAIN_ENABLED',
|
IsCustomDomainEnabled = 'IS_CUSTOM_DOMAIN_ENABLED',
|
||||||
IsApprovedAccessDomainsEnabled = 'IS_APPROVED_ACCESS_DOMAINS_ENABLED',
|
|
||||||
IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED',
|
IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED',
|
||||||
IsPermissionsV2Enabled = 'IS_PERMISSIONS_V2_ENABLED',
|
IsPermissionsV2Enabled = 'IS_PERMISSIONS_V2_ENABLED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
|
import { AuditJobModule } from 'src/engine/core-modules/audit/jobs/audit-job.module';
|
||||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
@ -19,6 +20,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||||
|
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
||||||
import { CleanOnboardingWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-onboarding-workspaces.job';
|
import { CleanOnboardingWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-onboarding-workspaces.job';
|
||||||
import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job';
|
import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job';
|
||||||
import { CleanWorkspaceDeletionWarningUserVarsJob } from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
|
import { CleanWorkspaceDeletionWarningUserVarsJob } from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job';
|
||||||
@ -32,7 +34,6 @@ import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module
|
|||||||
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||||
import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
|
import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
|
||||||
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
import { WorkflowModule } from 'src/modules/workflow/workflow.module';
|
||||||
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -60,6 +61,7 @@ import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.modu
|
|||||||
FavoriteModule,
|
FavoriteModule,
|
||||||
WorkspaceCleanerModule,
|
WorkspaceCleanerModule,
|
||||||
SubscriptionsModule,
|
SubscriptionsModule,
|
||||||
|
AuditJobModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
CleanSuspendedWorkspacesJob,
|
CleanSuspendedWorkspacesJob,
|
||||||
|
|||||||
@ -16,6 +16,6 @@ export enum ConfigVariablesGroup {
|
|||||||
ServerlessConfig = 'serverless-config',
|
ServerlessConfig = 'serverless-config',
|
||||||
SSL = 'ssl',
|
SSL = 'ssl',
|
||||||
SupportChatConfig = 'support-chat-config',
|
SupportChatConfig = 'support-chat-config',
|
||||||
AnalyticsConfig = 'analytics-config',
|
AnalyticsConfig = 'audit-config',
|
||||||
TokensDuration = 'tokens-duration',
|
TokensDuration = 'tokens-duration',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
|||||||
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
@ -46,7 +46,7 @@ import { UserService } from './services/user.service';
|
|||||||
OnboardingModule,
|
OnboardingModule,
|
||||||
TypeOrmModule.forFeature([KeyValuePair, UserWorkspace], 'core'),
|
TypeOrmModule.forFeature([KeyValuePair, UserWorkspace], 'core'),
|
||||||
UserVarsModule,
|
UserVarsModule,
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
DomainManagerModule,
|
DomainManagerModule,
|
||||||
UserRoleModule,
|
UserRoleModule,
|
||||||
FeatureFlagModule,
|
FeatureFlagModule,
|
||||||
|
|||||||
@ -20,7 +20,6 @@ import { In, Repository } from 'typeorm';
|
|||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
import {
|
import {
|
||||||
AuthException,
|
AuthException,
|
||||||
AuthExceptionCode,
|
AuthExceptionCode,
|
||||||
@ -75,7 +74,6 @@ export class UserResolver {
|
|||||||
private readonly onboardingService: OnboardingService,
|
private readonly onboardingService: OnboardingService,
|
||||||
private readonly userVarService: UserVarsService,
|
private readonly userVarService: UserVarsService,
|
||||||
private readonly fileService: FileService,
|
private readonly fileService: FileService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
|
||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
@InjectRepository(UserWorkspace, 'core')
|
@InjectRepository(UserWorkspace, 'core')
|
||||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||||
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
import { CustomDomainService } from 'src/engine/core-modules/domain-manager/services/custom-domain.service';
|
||||||
@ -25,7 +26,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
|
|
||||||
describe('WorkspaceService', () => {
|
describe('WorkspaceService', () => {
|
||||||
let service: WorkspaceService;
|
let service: WorkspaceService;
|
||||||
@ -76,9 +76,9 @@ describe('WorkspaceService', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AnalyticsService,
|
provide: AuditService,
|
||||||
useValue: {
|
useValue: {
|
||||||
createAnalyticsContext: jest.fn(),
|
createContext: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...[
|
...[
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import { isDefined } from 'twenty-shared/utils';
|
|||||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-activated';
|
||||||
|
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/custom-domain/custom-domain-deactivated';
|
||||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||||
@ -44,9 +47,6 @@ import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage
|
|||||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||||
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags';
|
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags';
|
||||||
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
import { extractVersionMajorMinorPatch } from 'src/utils/version/extract-version-major-minor-patch';
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-activated';
|
|
||||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/custom-domain/custom-domain-deactivated';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
@ -70,7 +70,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
private readonly domainManagerService: DomainManagerService,
|
private readonly domainManagerService: DomainManagerService,
|
||||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||||
private readonly permissionsService: PermissionsService,
|
private readonly permissionsService: PermissionsService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly auditService: AuditService,
|
||||||
private readonly customDomainService: CustomDomainService,
|
private readonly customDomainService: CustomDomainService,
|
||||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||||
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
|
@InjectMessageQueue(MessageQueue.deleteCascadeQueue)
|
||||||
@ -418,7 +418,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
workspace.isCustomDomainEnabled = isCustomDomainWorking;
|
workspace.isCustomDomainEnabled = isCustomDomainWorking;
|
||||||
await this.workspaceRepository.save(workspace);
|
await this.workspaceRepository.save(workspace);
|
||||||
|
|
||||||
const analytics = this.analyticsService.createAnalyticsContext({
|
const analytics = this.auditService.createContext({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
|||||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||||
|
|
||||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||||
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
@ -24,7 +25,6 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
|||||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
|
||||||
|
|
||||||
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
||||||
import { Workspace } from './workspace.entity';
|
import { Workspace } from './workspace.entity';
|
||||||
@ -55,7 +55,7 @@ import { WorkspaceService } from './services/workspace.service';
|
|||||||
TypeORMModule,
|
TypeORMModule,
|
||||||
PermissionsModule,
|
PermissionsModule,
|
||||||
WorkspaceCacheStorageModule,
|
WorkspaceCacheStorageModule,
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
RoleModule,
|
RoleModule,
|
||||||
],
|
],
|
||||||
services: [WorkspaceService],
|
services: [WorkspaceService],
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||||
|
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||||
@ -19,7 +19,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
|
|||||||
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
TypeOrmModule.forFeature([FeatureFlag], 'core'),
|
||||||
FileModule,
|
FileModule,
|
||||||
ThrottlerModule,
|
ThrottlerModule,
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
],
|
],
|
||||||
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
providers: [ServerlessFunctionService, ServerlessFunctionResolver],
|
||||||
exports: [ServerlessFunctionService],
|
exports: [ServerlessFunctionService],
|
||||||
|
|||||||
@ -10,7 +10,8 @@ import { IsNull, Not, Repository } from 'typeorm';
|
|||||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||||
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/serverless-function/serverless-function-executed';
|
||||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||||
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
|
||||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||||
@ -35,7 +36,6 @@ import {
|
|||||||
ServerlessFunctionException,
|
ServerlessFunctionException,
|
||||||
ServerlessFunctionExceptionCode,
|
ServerlessFunctionExceptionCode,
|
||||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/serverless-function/serverless-function-executed';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerlessFunctionService {
|
export class ServerlessFunctionService {
|
||||||
@ -46,7 +46,7 @@ export class ServerlessFunctionService {
|
|||||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||||
private readonly throttlerService: ThrottlerService,
|
private readonly throttlerService: ThrottlerService,
|
||||||
private readonly twentyConfigService: TwentyConfigService,
|
private readonly twentyConfigService: TwentyConfigService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly auditService: AuditService,
|
||||||
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
|
@InjectMessageQueue(MessageQueue.serverlessFunctionQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
) {}
|
) {}
|
||||||
@ -144,8 +144,8 @@ export class ServerlessFunctionService {
|
|||||||
version,
|
version,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.analyticsService
|
this.auditService
|
||||||
.createAnalyticsContext({
|
.createContext({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
})
|
})
|
||||||
.track(SERVERLESS_FUNCTION_EXECUTED_EVENT, {
|
.track(SERVERLESS_FUNCTION_EXECUTED_EVENT, {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export const EXCLUDED_MIDDLEWARE_OPERATIONS = [
|
|||||||
'GetWorkspaceFromInviteHash',
|
'GetWorkspaceFromInviteHash',
|
||||||
'Track',
|
'Track',
|
||||||
'TrackAnalytics',
|
'TrackAnalytics',
|
||||||
|
'AuditTrack',
|
||||||
'CheckUserExists',
|
'CheckUserExists',
|
||||||
'GetLoginTokenFromCredentials',
|
'GetLoginTokenFromCredentials',
|
||||||
'GetAuthTokensFromLoginToken',
|
'GetAuthTokensFromLoginToken',
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository';
|
import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository';
|
||||||
import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository';
|
|
||||||
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
|
import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository';
|
||||||
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository';
|
||||||
|
|
||||||
export const metadataToRepositoryMapping = {
|
export const metadataToRepositoryMapping = {
|
||||||
AuditLogWorkspaceEntity: AuditLogRepository,
|
|
||||||
BlocklistWorkspaceEntity: BlocklistRepository,
|
BlocklistWorkspaceEntity: BlocklistRepository,
|
||||||
TimelineActivityWorkspaceEntity: TimelineActivityRepository,
|
TimelineActivityWorkspaceEntity: TimelineActivityRepository,
|
||||||
WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository,
|
WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository,
|
||||||
|
|||||||
@ -21,8 +21,6 @@ import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-obj
|
|||||||
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
|
||||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity';
|
||||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||||
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
|
|
||||||
import { BehavioralEventWorkspaceEntity } from 'src/modules/timeline/standard-objects/behavioral-event.workspace-entity';
|
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||||
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||||
@ -31,19 +29,17 @@ import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view
|
|||||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
import { WebhookWorkspaceEntity } from 'src/modules/webhook/standard-objects/webhook.workspace-entity';
|
||||||
|
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
|
||||||
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
|
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
|
||||||
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { WorkflowAutomatedTriggerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
|
|
||||||
|
|
||||||
// TODO: Maybe we should automate this with the DiscoverService of Nest.JS
|
// TODO: Maybe we should automate this with the DiscoverService of Nest.JS
|
||||||
export const standardObjectMetadataDefinitions = [
|
export const standardObjectMetadataDefinitions = [
|
||||||
ApiKeyWorkspaceEntity,
|
ApiKeyWorkspaceEntity,
|
||||||
AuditLogWorkspaceEntity,
|
|
||||||
AttachmentWorkspaceEntity,
|
AttachmentWorkspaceEntity,
|
||||||
BehavioralEventWorkspaceEntity,
|
|
||||||
BlocklistWorkspaceEntity,
|
BlocklistWorkspaceEntity,
|
||||||
CalendarEventWorkspaceEntity,
|
CalendarEventWorkspaceEntity,
|
||||||
CalendarChannelWorkspaceEntity,
|
CalendarChannelWorkspaceEntity,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { API_KEY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { API_KEY_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
@ -21,7 +20,6 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync
|
|||||||
labelIdentifierStandardId: API_KEY_STANDARD_FIELD_IDS.name,
|
labelIdentifierStandardId: API_KEY_STANDARD_FIELD_IDS.name,
|
||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
export class ApiKeyWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ApiKeyWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: API_KEY_STANDARD_FIELD_IDS.name,
|
standardId: API_KEY_STANDARD_FIELD_IDS.name,
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
|
||||||
|
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
@ -27,7 +26,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
labelIdentifierStandardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
|
labelIdentifierStandardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
|
||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity {
|
export class BlocklistWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
|
standardId: BLOCKLIST_STANDARD_FIELD_IDS.handle,
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/i
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -33,7 +32,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
labelIdentifierStandardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.handle,
|
labelIdentifierStandardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.handle,
|
||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.handle,
|
standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.handle,
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity';
|
||||||
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
|
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -28,7 +28,6 @@ import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard
|
|||||||
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.favorite,
|
standardId: STANDARD_OBJECT_IDS.favorite,
|
||||||
@ -38,7 +37,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
|
|||||||
description: msg`A favorite that can be accessed from the left menu`,
|
description: msg`A favorite that can be accessed from the left menu`,
|
||||||
icon: STANDARD_OBJECT_ICONS.favorite,
|
icon: STANDARD_OBJECT_ICONS.favorite,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
|
export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import {
|
|||||||
MessageImportSyncStep,
|
MessageImportSyncStep,
|
||||||
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
||||||
import { MessagingPartialMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service';
|
import { MessagingPartialMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service';
|
||||||
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
|
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||||
|
|
||||||
export type MessagingMessageListFetchJobData = {
|
export type MessagingMessageListFetchJobData = {
|
||||||
messageChannelId: string;
|
messageChannelId: string;
|
||||||
@ -36,7 +36,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly messagingFullMessageListFetchService: MessagingFullMessageListFetchService,
|
private readonly messagingFullMessageListFetchService: MessagingFullMessageListFetchService,
|
||||||
private readonly messagingPartialMessageListFetchService: MessagingPartialMessageListFetchService,
|
private readonly messagingPartialMessageListFetchService: MessagingPartialMessageListFetchService,
|
||||||
private readonly messagingTelemetryService: MessagingTelemetryService,
|
private readonly messagingMonitoringService: MessagingMonitoringService,
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService,
|
private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService,
|
||||||
private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService,
|
private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService,
|
||||||
@ -46,7 +46,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
async handle(data: MessagingMessageListFetchJobData): Promise<void> {
|
async handle(data: MessagingMessageListFetchJobData): Promise<void> {
|
||||||
const { messageChannelId, workspaceId } = data;
|
const { messageChannelId, workspaceId } = data;
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'message_list_fetch_job.triggered',
|
eventName: 'message_list_fetch_job.triggered',
|
||||||
messageChannelId,
|
messageChannelId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -65,7 +65,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!messageChannel) {
|
if (!messageChannel) {
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'message_list_fetch_job.error.message_channel_not_found',
|
eventName: 'message_list_fetch_job.error.message_channel_not_found',
|
||||||
messageChannelId,
|
messageChannelId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@ -94,7 +94,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
|
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
|
||||||
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
|
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: `refresh_token.error.insufficient_permissions`,
|
eventName: `refresh_token.error.insufficient_permissions`,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccountId,
|
connectedAccountId: messageChannel.connectedAccountId,
|
||||||
@ -121,7 +121,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
`Fetching partial message list for workspace ${workspaceId} and messageChannelId ${messageChannel.id}`,
|
`Fetching partial message list for workspace ${workspaceId} and messageChannelId ${messageChannel.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'partial_message_list_fetch.started',
|
eventName: 'partial_message_list_fetch.started',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccount.id,
|
connectedAccountId: messageChannel.connectedAccount.id,
|
||||||
@ -134,7 +134,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'partial_message_list_fetch.completed',
|
eventName: 'partial_message_list_fetch.completed',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccount.id,
|
connectedAccountId: messageChannel.connectedAccount.id,
|
||||||
@ -148,7 +148,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
`Fetching full message list for workspace ${workspaceId} and account ${messageChannel.connectedAccount.id}`,
|
`Fetching full message list for workspace ${workspaceId} and account ${messageChannel.connectedAccount.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'full_message_list_fetch.started',
|
eventName: 'full_message_list_fetch.started',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccount.id,
|
connectedAccountId: messageChannel.connectedAccount.id,
|
||||||
@ -160,7 +160,7 @@ export class MessagingMessageListFetchJob {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'full_message_list_fetch.completed',
|
eventName: 'full_message_list_fetch.completed',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccount.id,
|
connectedAccountId: messageChannel.connectedAccount.id,
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import {
|
|||||||
MessageChannelWorkspaceEntity,
|
MessageChannelWorkspaceEntity,
|
||||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
|
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
|
||||||
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
|
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||||
|
|
||||||
export type MessagingMessagesImportJobData = {
|
export type MessagingMessagesImportJobData = {
|
||||||
messageChannelId: string;
|
messageChannelId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -24,7 +23,7 @@ export type MessagingMessagesImportJobData = {
|
|||||||
export class MessagingMessagesImportJob {
|
export class MessagingMessagesImportJob {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly messagingMessagesImportService: MessagingMessagesImportService,
|
private readonly messagingMessagesImportService: MessagingMessagesImportService,
|
||||||
private readonly messagingTelemetryService: MessagingTelemetryService,
|
private readonly messagingMonitoringService: MessagingMonitoringService,
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly twentyORMManager: TwentyORMManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -32,7 +31,7 @@ export class MessagingMessagesImportJob {
|
|||||||
async handle(data: MessagingMessagesImportJobData): Promise<void> {
|
async handle(data: MessagingMessagesImportJobData): Promise<void> {
|
||||||
const { messageChannelId, workspaceId } = data;
|
const { messageChannelId, workspaceId } = data;
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'messages_import.triggered',
|
eventName: 'messages_import.triggered',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
messageChannelId,
|
messageChannelId,
|
||||||
@ -51,7 +50,7 @@ export class MessagingMessagesImportJob {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!messageChannel) {
|
if (!messageChannel) {
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'messages_import.error.message_channel_not_found',
|
eventName: 'messages_import.error.message_channel_not_found',
|
||||||
messageChannelId,
|
messageChannelId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@ -20,8 +20,7 @@ import { MessagingGetMessagesService } from 'src/modules/messaging/message-impor
|
|||||||
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
||||||
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
|
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
|
||||||
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
||||||
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
|
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||||
|
|
||||||
describe('MessagingMessagesImportService', () => {
|
describe('MessagingMessagesImportService', () => {
|
||||||
let service: MessagingMessagesImportService;
|
let service: MessagingMessagesImportService;
|
||||||
let messageChannelSyncStatusService: MessageChannelSyncStatusService;
|
let messageChannelSyncStatusService: MessageChannelSyncStatusService;
|
||||||
@ -78,7 +77,7 @@ describe('MessagingMessagesImportService', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MessagingTelemetryService,
|
provide: MessagingMonitoringService,
|
||||||
useValue: {
|
useValue: {
|
||||||
track: jest.fn().mockResolvedValue(undefined),
|
track: jest.fn().mockResolvedValue(undefined),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -26,8 +26,7 @@ import {
|
|||||||
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
|
||||||
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
import { MessagingSaveMessagesAndEnqueueContactCreationService } from 'src/modules/messaging/message-import-manager/services/messaging-save-messages-and-enqueue-contact-creation.service';
|
||||||
import { filterEmails } from 'src/modules/messaging/message-import-manager/utils/filter-emails.util';
|
import { filterEmails } from 'src/modules/messaging/message-import-manager/utils/filter-emails.util';
|
||||||
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
|
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessagingMessagesImportService {
|
export class MessagingMessagesImportService {
|
||||||
private readonly logger = new Logger(MessagingMessagesImportService.name);
|
private readonly logger = new Logger(MessagingMessagesImportService.name);
|
||||||
@ -38,7 +37,7 @@ export class MessagingMessagesImportService {
|
|||||||
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
|
private readonly messageChannelSyncStatusService: MessageChannelSyncStatusService,
|
||||||
private readonly saveMessagesAndEnqueueContactCreationService: MessagingSaveMessagesAndEnqueueContactCreationService,
|
private readonly saveMessagesAndEnqueueContactCreationService: MessagingSaveMessagesAndEnqueueContactCreationService,
|
||||||
private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService,
|
private readonly connectedAccountRefreshTokensService: ConnectedAccountRefreshTokensService,
|
||||||
private readonly messagingTelemetryService: MessagingTelemetryService,
|
private readonly messagingMonitoringService: MessagingMonitoringService,
|
||||||
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
|
@InjectObjectMetadataRepository(BlocklistWorkspaceEntity)
|
||||||
private readonly blocklistRepository: BlocklistRepository,
|
private readonly blocklistRepository: BlocklistRepository,
|
||||||
private readonly emailAliasManagerService: EmailAliasManagerService,
|
private readonly emailAliasManagerService: EmailAliasManagerService,
|
||||||
@ -62,7 +61,7 @@ export class MessagingMessagesImportService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'messages_import.started',
|
eventName: 'messages_import.started',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccountId,
|
connectedAccountId: messageChannel.connectedAccountId,
|
||||||
@ -87,7 +86,7 @@ export class MessagingMessagesImportService {
|
|||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
|
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_ACCESS_TOKEN_FAILED:
|
||||||
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
|
case ConnectedAccountRefreshAccessTokenExceptionCode.REFRESH_TOKEN_NOT_FOUND:
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: `refresh_token.error.insufficient_permissions`,
|
eventName: `refresh_token.error.insufficient_permissions`,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccountId,
|
connectedAccountId: messageChannel.connectedAccountId,
|
||||||
@ -208,7 +207,7 @@ export class MessagingMessagesImportService {
|
|||||||
messageChannel: MessageChannelWorkspaceEntity,
|
messageChannel: MessageChannelWorkspaceEntity,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
) {
|
) {
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'messages_import.completed',
|
eventName: 'messages_import.completed',
|
||||||
workspaceId,
|
workspaceId,
|
||||||
connectedAccountId: messageChannel.connectedAccountId,
|
connectedAccountId: messageChannel.connectedAccountId,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
@ -22,7 +22,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([FeatureFlag, Workspace], 'core'),
|
TypeOrmModule.forFeature([FeatureFlag, Workspace], 'core'),
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
ContactCreationManagerModule,
|
ContactCreationManagerModule,
|
||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
ObjectMetadataRepositoryModule.forFeature([
|
ObjectMetadataRepositoryModule.forFeature([
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
|
|||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
|
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||||
|
|
||||||
export const MESSAGING_MESSAGE_CHANNEL_SYNC_STATUS_MONITORING_CRON_PATTERN =
|
export const MESSAGING_MESSAGE_CHANNEL_SYNC_STATUS_MONITORING_CRON_PATTERN =
|
||||||
'2/10 * * * *'; //Every 10 minutes, starting at 2 minutes past the hour
|
'2/10 * * * *'; //Every 10 minutes, starting at 2 minutes past the hour
|
||||||
@ -27,7 +27,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly messagingTelemetryService: MessagingTelemetryService,
|
private readonly messagingMonitoringService: MessagingMonitoringService,
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||||
) {}
|
) {}
|
||||||
@ -40,7 +40,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
|
|||||||
async handle(): Promise<void> {
|
async handle(): Promise<void> {
|
||||||
this.logger.log('Starting message channel sync status monitoring...');
|
this.logger.log('Starting message channel sync status monitoring...');
|
||||||
|
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: 'message_channel.monitoring.sync_status.start',
|
eventName: 'message_channel.monitoring.sync_status.start',
|
||||||
message: 'Starting message channel sync status monitoring',
|
message: 'Starting message channel sync status monitoring',
|
||||||
});
|
});
|
||||||
@ -66,7 +66,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
|
|||||||
if (!messageChannel.syncStatus) {
|
if (!messageChannel.syncStatus) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await this.messagingTelemetryService.track({
|
await this.messagingMonitoringService.track({
|
||||||
eventName: `message_channel.monitoring.sync_status.${snakeCase(
|
eventName: `message_channel.monitoring.sync_status.${snakeCase(
|
||||||
messageChannel.syncStatus,
|
messageChannel.syncStatus,
|
||||||
)}`,
|
)}`,
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
|
||||||
import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command';
|
import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command';
|
||||||
import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job';
|
import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.job';
|
||||||
import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service';
|
import { MessagingMonitoringService } from 'src/modules/messaging/monitoring/services/messaging-monitoring.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
MessagingCommonModule,
|
MessagingCommonModule,
|
||||||
BillingModule,
|
BillingModule,
|
||||||
TypeOrmModule.forFeature([Workspace], 'core'),
|
TypeOrmModule.forFeature([Workspace], 'core'),
|
||||||
@ -21,8 +21,8 @@ import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/serv
|
|||||||
providers: [
|
providers: [
|
||||||
MessagingMessageChannelSyncStatusMonitoringCronCommand,
|
MessagingMessageChannelSyncStatusMonitoringCronCommand,
|
||||||
MessagingMessageChannelSyncStatusMonitoringCronJob,
|
MessagingMessageChannelSyncStatusMonitoringCronJob,
|
||||||
MessagingTelemetryService,
|
MessagingMonitoringService,
|
||||||
],
|
],
|
||||||
exports: [MessagingTelemetryService],
|
exports: [MessagingMonitoringService],
|
||||||
})
|
})
|
||||||
export class MessagingMonitoringModule {}
|
export class MessagingMonitoringModule {}
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
|
||||||
|
type MessagingMonitoringTrackInput = {
|
||||||
|
eventName: string;
|
||||||
|
workspaceId?: string;
|
||||||
|
userId?: string;
|
||||||
|
connectedAccountId?: string;
|
||||||
|
messageChannelId?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MessagingMonitoringService {
|
||||||
|
constructor(private readonly auditService: AuditService) {}
|
||||||
|
|
||||||
|
public async track({
|
||||||
|
eventName,
|
||||||
|
workspaceId,
|
||||||
|
userId,
|
||||||
|
connectedAccountId,
|
||||||
|
messageChannelId,
|
||||||
|
message,
|
||||||
|
}: MessagingMonitoringTrackInput): Promise<void> {
|
||||||
|
const _eventName = eventName;
|
||||||
|
const _workspaceId = workspaceId;
|
||||||
|
const _userId = userId;
|
||||||
|
const _connectedAccountId = connectedAccountId;
|
||||||
|
const _messageChannelId = messageChannelId;
|
||||||
|
const _message = message;
|
||||||
|
|
||||||
|
// TODO: replace once we have Prometheus
|
||||||
|
/*
|
||||||
|
await this.auditService
|
||||||
|
.createContext({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
})
|
||||||
|
.track(MONITORING_EVENT, {
|
||||||
|
eventName: `messaging.${eventName}`,
|
||||||
|
connectedAccountId,
|
||||||
|
messageChannelId,
|
||||||
|
message,
|
||||||
|
}); */
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
|
||||||
import { MONITORING_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/monitoring/monitoring';
|
|
||||||
|
|
||||||
type MessagingTelemetryTrackInput = {
|
|
||||||
eventName: string;
|
|
||||||
workspaceId?: string;
|
|
||||||
userId?: string;
|
|
||||||
connectedAccountId?: string;
|
|
||||||
messageChannelId?: string;
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class MessagingTelemetryService {
|
|
||||||
constructor(private readonly analyticsService: AnalyticsService) {}
|
|
||||||
|
|
||||||
public async track({
|
|
||||||
eventName,
|
|
||||||
workspaceId,
|
|
||||||
userId,
|
|
||||||
connectedAccountId,
|
|
||||||
messageChannelId,
|
|
||||||
message,
|
|
||||||
}: MessagingTelemetryTrackInput): Promise<void> {
|
|
||||||
await this.analyticsService
|
|
||||||
.createAnalyticsContext({
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
})
|
|
||||||
.track(MONITORING_EVENT, {
|
|
||||||
eventName: `messaging.${eventName}`,
|
|
||||||
connectedAccountId,
|
|
||||||
messageChannelId,
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +1,17 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
import { CreateAuditLogFromInternalEvent } from 'src/modules/timeline/jobs/create-audit-log-from-internal-event';
|
|
||||||
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
|
import { UpsertTimelineActivityFromInternalEvent } from 'src/modules/timeline/jobs/upsert-timeline-activity-from-internal-event.job';
|
||||||
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
|
|
||||||
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ObjectMetadataRepositoryModule.forFeature([
|
ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]),
|
||||||
WorkspaceMemberWorkspaceEntity,
|
|
||||||
AuditLogWorkspaceEntity,
|
|
||||||
]),
|
|
||||||
TimelineActivityModule,
|
TimelineActivityModule,
|
||||||
AnalyticsModule,
|
AuditModule,
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
CreateAuditLogFromInternalEvent,
|
|
||||||
UpsertTimelineActivityFromInternalEvent,
|
|
||||||
],
|
],
|
||||||
|
providers: [UpsertTimelineActivityFromInternalEvent],
|
||||||
})
|
})
|
||||||
export class TimelineJobModule {}
|
export class TimelineJobModule {}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
// TODO
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuditLogRepository {
|
|
||||||
constructor(
|
|
||||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async insert(
|
|
||||||
name: string,
|
|
||||||
properties: object | null,
|
|
||||||
workspaceMemberId: string | null,
|
|
||||||
objectName: string,
|
|
||||||
objectMetadataId: string,
|
|
||||||
recordId: string,
|
|
||||||
workspaceId: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const dataSourceSchema =
|
|
||||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
|
||||||
|
|
||||||
await this.workspaceDataSourceService.executeRawQuery(
|
|
||||||
`INSERT INTO ${dataSourceSchema}."auditLog"
|
|
||||||
("name", "properties", "workspaceMemberId", "objectName", "objectMetadataId", "recordId")
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
||||||
[
|
|
||||||
name,
|
|
||||||
properties,
|
|
||||||
workspaceMemberId,
|
|
||||||
objectName,
|
|
||||||
objectMetadataId,
|
|
||||||
recordId,
|
|
||||||
],
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
|
||||||
|
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
|
||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
|
|
||||||
|
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
|
||||||
import { AUDIT_LOGS_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
|
||||||
standardId: STANDARD_OBJECT_IDS.auditLog,
|
|
||||||
namePlural: 'auditLogs',
|
|
||||||
labelSingular: msg`Audit Log`,
|
|
||||||
labelPlural: msg`Audit Logs`,
|
|
||||||
description: msg`An audit log of actions performed in the system`,
|
|
||||||
icon: STANDARD_OBJECT_ICONS.auditLog,
|
|
||||||
labelIdentifierStandardId: AUDIT_LOGS_STANDARD_FIELD_IDS.name,
|
|
||||||
})
|
|
||||||
@WorkspaceIsSystem()
|
|
||||||
export class AuditLogWorkspaceEntity extends BaseWorkspaceEntity {
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.name,
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: msg`Event name`,
|
|
||||||
description: msg`Event name/type`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.properties,
|
|
||||||
type: FieldMetadataType.RAW_JSON,
|
|
||||||
label: msg`Event details`,
|
|
||||||
description: msg`Json value for event details`,
|
|
||||||
icon: 'IconListDetails',
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
properties: JSON | null;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.context,
|
|
||||||
type: FieldMetadataType.RAW_JSON,
|
|
||||||
label: msg`Event context`,
|
|
||||||
description: msg`Json object to provide context (user, device, workspace, etc.)`,
|
|
||||||
icon: 'IconListDetails',
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
context: JSON | null;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.objectName,
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: msg`Object name`,
|
|
||||||
description: msg`Object name`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
objectName: string;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.objectMetadataId,
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: msg`Object metadata id`,
|
|
||||||
description: msg`Object metadata id`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
objectMetadataId: string;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.recordId,
|
|
||||||
type: FieldMetadataType.UUID,
|
|
||||||
label: msg`Record id`,
|
|
||||||
description: msg`Record id`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
recordId: string | null;
|
|
||||||
|
|
||||||
@WorkspaceRelation({
|
|
||||||
standardId: AUDIT_LOGS_STANDARD_FIELD_IDS.workspaceMember,
|
|
||||||
type: RelationType.MANY_TO_ONE,
|
|
||||||
label: msg`Workspace Member`,
|
|
||||||
description: msg`Event workspace member`,
|
|
||||||
icon: 'IconCircleUser',
|
|
||||||
inverseSideTarget: () => WorkspaceMemberWorkspaceEntity,
|
|
||||||
inverseSideFieldKey: 'auditLogs',
|
|
||||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
workspaceMember: Relation<WorkspaceMemberWorkspaceEntity> | null;
|
|
||||||
|
|
||||||
@WorkspaceJoinColumn('workspaceMember')
|
|
||||||
workspaceMemberId: string | null;
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
|
||||||
|
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
|
||||||
import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
|
||||||
import { BEHAVIORAL_EVENT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
|
||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
|
||||||
standardId: STANDARD_OBJECT_IDS.behavioralEvent,
|
|
||||||
namePlural: 'behavioralEvents',
|
|
||||||
labelSingular: msg`Behavioral Event`,
|
|
||||||
labelPlural: msg`Behavioral Events`,
|
|
||||||
description: msg`An event related to user behavior`,
|
|
||||||
icon: STANDARD_OBJECT_ICONS.behavioralEvent,
|
|
||||||
})
|
|
||||||
@WorkspaceIsSystem()
|
|
||||||
@WorkspaceGate({
|
|
||||||
featureFlag: FeatureFlagKey.IsEventObjectEnabled,
|
|
||||||
})
|
|
||||||
export class BehavioralEventWorkspaceEntity extends BaseWorkspaceEntity {
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Common in Segment, Rudderstack, etc.
|
|
||||||
* = Track, Screen, Page...
|
|
||||||
* But doesn't feel that useful.
|
|
||||||
* Let's try living without it.
|
|
||||||
*
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: behavioralEventStandardFieldIds.type,
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: msg`Event type`,
|
|
||||||
description: msg`Event type`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
type: string;
|
|
||||||
*/
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.name,
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: msg`Event name`,
|
|
||||||
description: msg`Event name`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.properties,
|
|
||||||
type: FieldMetadataType.RAW_JSON,
|
|
||||||
label: msg`Event details`,
|
|
||||||
description: msg`Json value for event details`,
|
|
||||||
icon: 'IconListDetails',
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
properties: JSON | null;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.context,
|
|
||||||
type: FieldMetadataType.RAW_JSON,
|
|
||||||
label: msg`Event context`,
|
|
||||||
description: msg`Json object to provide context (user, device, workspace, etc.)`,
|
|
||||||
icon: 'IconListDetails',
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
context: JSON | null;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.objectName,
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: msg`Object name`,
|
|
||||||
description: msg`If the event is related to a particular object`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
objectName: string;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
|
||||||
standardId: BEHAVIORAL_EVENT_STANDARD_FIELD_IDS.recordId,
|
|
||||||
type: FieldMetadataType.UUID,
|
|
||||||
label: msg`Object id`,
|
|
||||||
description: msg`Event name/type`,
|
|
||||||
icon: 'IconAbc',
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
recordId: string | null;
|
|
||||||
}
|
|
||||||
@ -7,11 +7,11 @@ import { Relation } from 'typeorm';
|
|||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -20,7 +20,6 @@ import { VIEW_FIELD_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/work
|
|||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
|
|
||||||
registerEnumType(AGGREGATE_OPERATIONS, {
|
registerEnumType(AGGREGATE_OPERATIONS, {
|
||||||
name: 'AggregateOperations',
|
name: 'AggregateOperations',
|
||||||
@ -34,7 +33,6 @@ registerEnumType(AGGREGATE_OPERATIONS, {
|
|||||||
description: msg`(System) View Fields`,
|
description: msg`(System) View Fields`,
|
||||||
icon: STANDARD_OBJECT_ICONS.viewField,
|
icon: STANDARD_OBJECT_ICONS.viewField,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
||||||
isUnique: true,
|
isUnique: true,
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { Relation } from 'typeorm';
|
|||||||
|
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -15,7 +15,6 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re
|
|||||||
import { VIEW_FILTER_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { VIEW_FILTER_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
|
|
||||||
export enum ViewFilterGroupLogicalOperator {
|
export enum ViewFilterGroupLogicalOperator {
|
||||||
AND = 'AND',
|
AND = 'AND',
|
||||||
@ -31,7 +30,6 @@ export enum ViewFilterGroupLogicalOperator {
|
|||||||
description: msg`(System) View Filter Groups`,
|
description: msg`(System) View Filter Groups`,
|
||||||
icon: 'IconFilterBolt',
|
icon: 'IconFilterBolt',
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class ViewFilterGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ViewFilterGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceRelation({
|
@WorkspaceRelation({
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -16,7 +16,6 @@ import { VIEW_FILTER_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/wor
|
|||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.viewFilter,
|
standardId: STANDARD_OBJECT_IDS.viewFilter,
|
||||||
@ -26,7 +25,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
|
|||||||
description: msg`(System) View Filters`,
|
description: msg`(System) View Filters`,
|
||||||
icon: STANDARD_OBJECT_ICONS.viewFilter,
|
icon: STANDARD_OBJECT_ICONS.viewFilter,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ViewFilterWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||||||
|
|
||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -15,7 +15,6 @@ import { VIEW_GROUP_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/work
|
|||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.viewGroup,
|
standardId: STANDARD_OBJECT_IDS.viewGroup,
|
||||||
@ -25,7 +24,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
|
|||||||
description: msg`(System) View Groups`,
|
description: msg`(System) View Groups`,
|
||||||
icon: STANDARD_OBJECT_ICONS.viewGroup,
|
icon: STANDARD_OBJECT_ICONS.viewGroup,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ViewGroupWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import { FieldMetadataType } from 'twenty-shared/types';
|
|||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||||
|
|
||||||
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -17,7 +17,6 @@ import { VIEW_SORT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/works
|
|||||||
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
|
||||||
|
|
||||||
@WorkspaceEntity({
|
@WorkspaceEntity({
|
||||||
standardId: STANDARD_OBJECT_IDS.viewSort,
|
standardId: STANDARD_OBJECT_IDS.viewSort,
|
||||||
@ -27,7 +26,6 @@ import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-met
|
|||||||
description: msg`(System) View Sorts`,
|
description: msg`(System) View Sorts`,
|
||||||
icon: STANDARD_OBJECT_ICONS.viewSort,
|
icon: STANDARD_OBJECT_ICONS.viewSort,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
@WorkspaceIndex(['fieldMetadataId', 'viewId'], {
|
||||||
isUnique: true,
|
isUnique: true,
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import { AGGREGATE_OPERATIONS } from 'src/engine/api/graphql/graphql-query-runne
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
|
||||||
@ -43,7 +42,6 @@ registerEnumType(ViewOpenRecordInType, {
|
|||||||
icon: STANDARD_OBJECT_ICONS.view,
|
icon: STANDARD_OBJECT_ICONS.view,
|
||||||
labelIdentifierStandardId: VIEW_STANDARD_FIELD_IDS.name,
|
labelIdentifierStandardId: VIEW_STANDARD_FIELD_IDS.name,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
export class ViewWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import { Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { AnalyticsService } from 'src/engine/core-modules/analytics/services/analytics.service';
|
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||||
|
import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/audit/utils/events/track/webhook/webhook-response';
|
||||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||||
import { WEBHOOK_RESPONSE_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/webhook/webhook-response';
|
|
||||||
|
|
||||||
export type CallWebhookJobData = {
|
export type CallWebhookJobData = {
|
||||||
targetUrl: string;
|
targetUrl: string;
|
||||||
@ -26,7 +26,7 @@ export class CallWebhookJob {
|
|||||||
private readonly logger = new Logger(CallWebhookJob.name);
|
private readonly logger = new Logger(CallWebhookJob.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly analyticsService: AnalyticsService,
|
private readonly auditService: AuditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private generateSignature(
|
private generateSignature(
|
||||||
@ -47,7 +47,7 @@ export class CallWebhookJob {
|
|||||||
webhookId: data.webhookId,
|
webhookId: data.webhookId,
|
||||||
eventName: data.eventName,
|
eventName: data.eventName,
|
||||||
};
|
};
|
||||||
const analytics = this.analyticsService.createAnalyticsContext({
|
const analytics = this.auditService.createContext({
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||||
import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job';
|
import { CallWebhookJobsJob } from 'src/modules/webhook/jobs/call-webhook-jobs.job';
|
||||||
import { CallWebhookJob } from 'src/modules/webhook/jobs/call-webhook.job';
|
import { CallWebhookJob } from 'src/modules/webhook/jobs/call-webhook.job';
|
||||||
import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [HttpModule, AnalyticsModule],
|
imports: [HttpModule, AuditModule],
|
||||||
providers: [CallWebhookJobsJob, CallWebhookJob],
|
providers: [CallWebhookJobsJob, CallWebhookJob],
|
||||||
})
|
})
|
||||||
export class WebhookJobModule {}
|
export class WebhookJobModule {}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
|
|||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WEBHOOK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { WEBHOOK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
@ -21,7 +20,6 @@ import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync
|
|||||||
icon: STANDARD_OBJECT_ICONS.webhook,
|
icon: STANDARD_OBJECT_ICONS.webhook,
|
||||||
labelIdentifierStandardId: WEBHOOK_STANDARD_FIELD_IDS.targetUrl,
|
labelIdentifierStandardId: WEBHOOK_STANDARD_FIELD_IDS.targetUrl,
|
||||||
})
|
})
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
export class WebhookWorkspaceEntity extends BaseWorkspaceEntity {
|
export class WebhookWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/compos
|
|||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
|
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
|
||||||
@ -54,6 +55,7 @@ export type WorkflowRunOutput = {
|
|||||||
labelIdentifierStandardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,
|
labelIdentifierStandardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,
|
||||||
icon: STANDARD_OBJECT_ICONS.workflowRun,
|
icon: STANDARD_OBJECT_ICONS.workflowRun,
|
||||||
})
|
})
|
||||||
|
@WorkspaceIsNotAuditLogged()
|
||||||
export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
|
export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,
|
standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity
|
|||||||
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
|
||||||
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
|
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
|
import { WorkspaceIsSearchable } from 'src/engine/twenty-orm/decorators/workspace-is-searchable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
@ -35,7 +34,6 @@ import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/s
|
|||||||
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||||
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
|
||||||
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
|
||||||
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
|
|
||||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
|
||||||
export enum WorkspaceMemberDateFormatEnum {
|
export enum WorkspaceMemberDateFormatEnum {
|
||||||
@ -81,7 +79,6 @@ export const SEARCH_FIELDS_FOR_WORKSPACE_MEMBER: FieldTypeAndNameMetadata[] = [
|
|||||||
imageIdentifierStandardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.avatarUrl,
|
imageIdentifierStandardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.avatarUrl,
|
||||||
})
|
})
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
@WorkspaceIsNotAuditLogged()
|
|
||||||
@WorkspaceIsSearchable()
|
@WorkspaceIsSearchable()
|
||||||
export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
|
export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
@ -347,19 +344,6 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
|
|||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
|
timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>;
|
||||||
|
|
||||||
@WorkspaceRelation({
|
|
||||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.auditLogs,
|
|
||||||
type: RelationType.ONE_TO_MANY,
|
|
||||||
label: msg`Audit Logs`,
|
|
||||||
description: msg`Audit Logs linked to the workspace member`,
|
|
||||||
icon: 'IconTimelineEvent',
|
|
||||||
inverseSideTarget: () => AuditLogWorkspaceEntity,
|
|
||||||
onDelete: RelationOnDeleteAction.SET_NULL,
|
|
||||||
})
|
|
||||||
@WorkspaceIsNullable()
|
|
||||||
@WorkspaceIsSystem()
|
|
||||||
auditLogs: Relation<AuditLogWorkspaceEntity[]>;
|
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.searchVector,
|
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.searchVector,
|
||||||
type: FieldMetadataType.TS_VECTOR,
|
type: FieldMetadataType.TS_VECTOR,
|
||||||
|
|||||||
@ -1,30 +1,30 @@
|
|||||||
import process from 'process';
|
import process from 'process';
|
||||||
|
|
||||||
|
import { ClickHouseClient, createClient } from '@clickhouse/client';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { createClient, ClickHouseClient } from '@clickhouse/client';
|
|
||||||
|
|
||||||
import { GenericTrackEvent } from 'src/engine/core-modules/analytics/utils/events/track/track';
|
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/track/object-record/object-record-created';
|
||||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/analytics/utils/events/track/object-record/object-record-created';
|
import { GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/track/track';
|
||||||
|
|
||||||
describe('ClickHouse Event Registration (integration)', () => {
|
describe('ClickHouse Event Registration (integration)', () => {
|
||||||
let clickhouseClient: ClickHouseClient;
|
let clickHouseClient: ClickHouseClient;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
|
|
||||||
clickhouseClient = createClient({
|
clickHouseClient = createClient({
|
||||||
url: process.env.CLICKHOUSE_URL,
|
url: process.env.CLICKHOUSE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
await clickhouseClient.query({
|
await clickHouseClient.query({
|
||||||
query: 'TRUNCATE TABLE events',
|
query: 'TRUNCATE TABLE auditEvent',
|
||||||
format: 'JSONEachRow',
|
format: 'JSONEachRow',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
if (clickhouseClient) {
|
if (clickHouseClient) {
|
||||||
await clickhouseClient.close();
|
await clickHouseClient.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,10 +53,10 @@ describe('ClickHouse Event Registration (integration)', () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.data.trackAnalytics.success).toBe(true);
|
expect(response.body.data.trackAnalytics.success).toBe(true);
|
||||||
|
|
||||||
const queryResult = await clickhouseClient.query({
|
const queryResult = await clickHouseClient.query({
|
||||||
query: `
|
query: `
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM events
|
FROM auditEvent
|
||||||
WHERE event = '${OBJECT_RECORD_CREATED_EVENT}' AND timestamp >= now() - INTERVAL 1 SECOND
|
WHERE event = '${OBJECT_RECORD_CREATED_EVENT}' AND timestamp >= now() - INTERVAL 1 SECOND
|
||||||
|
|
||||||
`,
|
`,
|
||||||
@ -1,65 +0,0 @@
|
|||||||
import request from 'supertest';
|
|
||||||
|
|
||||||
const client = request(`http://localhost:${APP_PORT}`);
|
|
||||||
|
|
||||||
describe('auditLogsResolver (e2e)', () => {
|
|
||||||
it('should find many auditLogs', () => {
|
|
||||||
const queryData = {
|
|
||||||
query: `
|
|
||||||
query auditLogs {
|
|
||||||
auditLogs {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
name
|
|
||||||
properties
|
|
||||||
context
|
|
||||||
objectName
|
|
||||||
objectMetadataId
|
|
||||||
recordId
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
deletedAt
|
|
||||||
workspaceMemberId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
return client
|
|
||||||
.post('/graphql')
|
|
||||||
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
|
|
||||||
.send(queryData)
|
|
||||||
.expect(200)
|
|
||||||
.expect((res) => {
|
|
||||||
expect(res.body.data).toBeDefined();
|
|
||||||
expect(res.body.errors).toBeUndefined();
|
|
||||||
})
|
|
||||||
.expect((res) => {
|
|
||||||
const data = res.body.data.auditLogs;
|
|
||||||
|
|
||||||
expect(data).toBeDefined();
|
|
||||||
expect(Array.isArray(data.edges)).toBe(true);
|
|
||||||
|
|
||||||
const edges = data.edges;
|
|
||||||
|
|
||||||
if (edges.length > 0) {
|
|
||||||
const auditLogs = edges[0].node;
|
|
||||||
|
|
||||||
expect(auditLogs).toHaveProperty('name');
|
|
||||||
expect(auditLogs).toHaveProperty('properties');
|
|
||||||
expect(auditLogs).toHaveProperty('context');
|
|
||||||
expect(auditLogs).toHaveProperty('objectName');
|
|
||||||
expect(auditLogs).toHaveProperty('objectMetadataId');
|
|
||||||
expect(auditLogs).toHaveProperty('recordId');
|
|
||||||
expect(auditLogs).toHaveProperty('id');
|
|
||||||
expect(auditLogs).toHaveProperty('createdAt');
|
|
||||||
expect(auditLogs).toHaveProperty('updatedAt');
|
|
||||||
expect(auditLogs).toHaveProperty('deletedAt');
|
|
||||||
expect(auditLogs).toHaveProperty('workspaceMemberId');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user