6658 workflows add a first twenty piece email sender (#6965)
This commit is contained in:
@ -7,9 +7,9 @@ import { AISQLQueryResolver } from 'src/engine/core-modules/ai-sql-query/ai-sql-
|
||||
import { AISQLQueryService } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.service';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module';
|
||||
import { LLMChatModelModule } from 'src/engine/integrations/llm-chat-model/llm-chat-model.module';
|
||||
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
|
||||
import { LLMTracingModule } from 'src/engine/integrations/llm-tracing/llm-tracing.module';
|
||||
import { LLMChatModelModule } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module';
|
||||
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
||||
import { LLMTracingModule } from 'src/engine/core-modules/llm-tracing/llm-tracing.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module';
|
||||
@Module({
|
||||
|
||||
@ -11,8 +11,8 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||
import { sqlGenerationPromptTemplate } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates';
|
||||
import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto';
|
||||
import { LLMChatModelService } from 'src/engine/integrations/llm-chat-model/llm-chat-model.service';
|
||||
import { LLMTracingService } from 'src/engine/integrations/llm-tracing/llm-tracing.service';
|
||||
import { LLMChatModelService } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.service';
|
||||
import { LLMTracingService } from 'src/engine/core-modules/llm-tracing/llm-tracing.service';
|
||||
import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
import { AnalyticsResolver } from './analytics.resolver';
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
|
||||
@ -8,7 +8,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
import { Analytics } from './analytics.entity';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
import { AnalyticsService } from './analytics.service';
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
type CreateEventInput = {
|
||||
type: string;
|
||||
|
||||
@ -6,7 +6,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { CaptchaGuard } from 'src/engine/integrations/captcha/captcha.guard';
|
||||
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { CaptchaGuard } from 'src/engine/integrations/captcha/captcha.guard';
|
||||
import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard';
|
||||
|
||||
import { ChallengeInput } from './dto/challenge.input';
|
||||
import { ImpersonateInput } from './dto/impersonate.input';
|
||||
|
||||
@ -20,7 +20,7 @@ import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-
|
||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Controller('auth/google-apis')
|
||||
@UseFilters(AuthRestApiExceptionFilter)
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy';
|
||||
import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleProviderEnabledGuard implements CanActivate {
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftProviderEnabledGuard implements CanActivate {
|
||||
|
||||
@ -5,8 +5,8 @@ import { UserService } from 'src/engine/core-modules/user/services/user.service'
|
||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import ms from 'ms';
|
||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { NodeEnvironment } from 'src/engine/integrations/environment/interfaces/node-environment.interface';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
|
||||
|
||||
import {
|
||||
AppToken,
|
||||
@ -36,8 +36,8 @@ import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-mem
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
|
||||
@ -3,10 +3,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import {
|
||||
|
||||
@ -8,7 +8,7 @@ import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
describe('SignInUpService', () => {
|
||||
let service: SignInUpService;
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||
|
||||
export type SignInUpServiceInput = {
|
||||
|
||||
@ -14,8 +14,8 @@ import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.aut
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
@ -42,8 +42,8 @@ import {
|
||||
Workspace,
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { PassportStrategy } from '@nestjs/passport';
|
||||
|
||||
import { Strategy } from 'passport-google-oauth20';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export type GoogleAPIScopeConfig = {
|
||||
isCalendarEnabled?: boolean;
|
||||
|
||||
@ -4,7 +4,7 @@ import { VerifyCallback } from 'passport-google-oauth20';
|
||||
|
||||
import { GoogleAPIsOauthCommonStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy';
|
||||
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export type GoogleAPIScopeConfig = {
|
||||
isCalendarEnabled?: boolean;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GoogleAPIsOauthCommonStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export type GoogleAPIScopeConfig = {
|
||||
isCalendarEnabled?: boolean;
|
||||
|
||||
@ -4,7 +4,7 @@ import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Request } from 'express';
|
||||
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export type GoogleRequest = Omit<
|
||||
Request,
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export type MicrosoftRequest = Omit<
|
||||
Request,
|
||||
|
||||
@ -3,9 +3,9 @@ import { Logger, Scope } from '@nestjs/common';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
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 { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
export type UpdateSubscriptionJobData = { workspaceId: string };
|
||||
|
||||
@Processor({
|
||||
|
||||
@ -5,11 +5,11 @@ import {
|
||||
UpdateSubscriptionJob,
|
||||
UpdateSubscriptionJobData,
|
||||
} from 'src/engine/core-modules/billing/jobs/update-subscription.job';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event';
|
||||
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/workspace-event.type';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.ser
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
export enum WebhookEvent {
|
||||
|
||||
@ -18,7 +18,7 @@ import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.ser
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingSubscriptionService {
|
||||
|
||||
@ -6,7 +6,7 @@ import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/bil
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
|
||||
@ -7,7 +7,7 @@ import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/ava
|
||||
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
import { CacheModuleOptions } from '@nestjs/common';
|
||||
|
||||
import { redisStore } from 'cache-manager-redis-yet';
|
||||
|
||||
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export const cacheStorageModuleFactory = (
|
||||
environmentService: EnvironmentService,
|
||||
): CacheModuleOptions => {
|
||||
const cacheStorageType = environmentService.get('CACHE_STORAGE_TYPE');
|
||||
const cacheStorageTtl = environmentService.get('CACHE_STORAGE_TTL');
|
||||
const cacheModuleOptions: CacheModuleOptions = {
|
||||
isGlobal: true,
|
||||
ttl: cacheStorageTtl * 1000,
|
||||
};
|
||||
|
||||
switch (cacheStorageType) {
|
||||
case CacheStorageType.Memory: {
|
||||
return cacheModuleOptions;
|
||||
}
|
||||
case CacheStorageType.Redis: {
|
||||
const host = environmentService.get('REDIS_HOST');
|
||||
const port = environmentService.get('REDIS_PORT');
|
||||
|
||||
if (!(host && port)) {
|
||||
throw new Error(
|
||||
`${cacheStorageType} cache storage requires host: ${host} and port: ${port} to be defined, check your .env file`,
|
||||
);
|
||||
}
|
||||
|
||||
const username = environmentService.get('REDIS_USERNAME');
|
||||
const password = environmentService.get('REDIS_PASSWORD');
|
||||
|
||||
return {
|
||||
...cacheModuleOptions,
|
||||
store: redisStore,
|
||||
socket: {
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid cache-storage (${cacheStorageType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { Module, Global, Inject, OnModuleDestroy } from '@nestjs/common';
|
||||
import { CacheModule, CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/cache-storage.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { cacheStorageModuleFactory } from 'src/engine/core-modules/cache-storage/cache-storage.module-factory';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule.registerAsync({
|
||||
isGlobal: true,
|
||||
imports: [ConfigModule],
|
||||
useFactory: cacheStorageModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
...Object.values(CacheStorageNamespace).map((cacheStorageNamespace) => ({
|
||||
provide: cacheStorageNamespace,
|
||||
useFactory: (cacheManager: Cache) => {
|
||||
return new CacheStorageService(cacheManager, cacheStorageNamespace);
|
||||
},
|
||||
inject: [CACHE_MANAGER],
|
||||
})),
|
||||
],
|
||||
exports: [...Object.values(CacheStorageNamespace)],
|
||||
})
|
||||
export class CacheStorageModule implements OnModuleDestroy {
|
||||
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if ((this.cacheManager.store as any)?.name === 'redis') {
|
||||
await (this.cacheManager.store as any).client.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||
|
||||
import { RedisCache } from 'cache-manager-redis-yet';
|
||||
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
|
||||
@Injectable()
|
||||
export class CacheStorageService {
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER)
|
||||
private readonly cache: Cache,
|
||||
private readonly namespace: CacheStorageNamespace,
|
||||
) {}
|
||||
|
||||
async get<T>(key: string): Promise<T | undefined> {
|
||||
return this.cache.get(`${this.namespace}:${key}`);
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T, ttl?: number) {
|
||||
return this.cache.set(`${this.namespace}:${key}`, value, ttl);
|
||||
}
|
||||
|
||||
async del(key: string) {
|
||||
return this.cache.del(`${this.namespace}:${key}`);
|
||||
}
|
||||
|
||||
async setAdd(key: string, value: string[]) {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.isRedisCache()) {
|
||||
return (this.cache as RedisCache).store.client.sAdd(
|
||||
`${this.namespace}:${key}`,
|
||||
value,
|
||||
);
|
||||
}
|
||||
this.get(key).then((res: string[]) => {
|
||||
if (res) {
|
||||
this.set(key, [...res, ...value]);
|
||||
} else {
|
||||
this.set(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setPop(key: string, size = 1) {
|
||||
if (this.isRedisCache()) {
|
||||
return (this.cache as RedisCache).store.client.sPop(
|
||||
`${this.namespace}:${key}`,
|
||||
size,
|
||||
);
|
||||
}
|
||||
|
||||
return this.get(key).then((res: string[]) => {
|
||||
if (res) {
|
||||
this.set(key, res.slice(0, -size));
|
||||
|
||||
return res.slice(-size);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
async flush() {
|
||||
return this.cache.reset();
|
||||
}
|
||||
|
||||
private isRedisCache() {
|
||||
return (this.cache.store as any)?.name === 'redis';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
|
||||
export const InjectCacheStorage = (
|
||||
cacheStorageNamespace: CacheStorageNamespace,
|
||||
) => {
|
||||
return Inject(cacheStorageNamespace);
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
export enum CacheStorageNamespace {
|
||||
ModuleMessaging = 'module:messaging',
|
||||
ModuleCalendar = 'module:calendar',
|
||||
EngineWorkspace = 'engine:workspace',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum CacheStorageType {
|
||||
Memory = 'memory',
|
||||
Redis = 'redis',
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const CAPTCHA_DRIVER = Symbol('CAPTCHA_DRIVER');
|
||||
@ -0,0 +1,28 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
|
||||
import { CaptchaService } from 'src/engine/core-modules/captcha/captcha.service';
|
||||
|
||||
@Injectable()
|
||||
export class CaptchaGuard implements CanActivate {
|
||||
constructor(private captchaService: CaptchaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const ctx = GqlExecutionContext.create(context);
|
||||
|
||||
const { captchaToken: token } = ctx.getArgs();
|
||||
|
||||
const result = await this.captchaService.validate(token || '');
|
||||
|
||||
if (result.success) return true;
|
||||
else
|
||||
throw new BadRequestException(
|
||||
'Invalid Captcha, please try another device',
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import {
|
||||
CaptchaDriverOptions,
|
||||
CaptchaModuleOptions,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export const captchaModuleFactory = (
|
||||
environmentService: EnvironmentService,
|
||||
): CaptchaModuleOptions | undefined => {
|
||||
const driver = environmentService.get('CAPTCHA_DRIVER');
|
||||
const siteKey = environmentService.get('CAPTCHA_SITE_KEY');
|
||||
const secretKey = environmentService.get('CAPTCHA_SECRET_KEY');
|
||||
|
||||
if (!driver) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!siteKey || !secretKey) {
|
||||
throw new Error('Captcha driver requires site key and secret key');
|
||||
}
|
||||
|
||||
const captchaOptions: CaptchaDriverOptions = {
|
||||
siteKey,
|
||||
secretKey,
|
||||
};
|
||||
|
||||
return {
|
||||
type: driver,
|
||||
options: captchaOptions,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,42 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/captcha.constants';
|
||||
import { CaptchaService } from 'src/engine/core-modules/captcha/captcha.service';
|
||||
import { GoogleRecaptchaDriver } from 'src/engine/core-modules/captcha/drivers/google-recaptcha.driver';
|
||||
import { TurnstileDriver } from 'src/engine/core-modules/captcha/drivers/turnstile.driver';
|
||||
import {
|
||||
CaptchaDriverType,
|
||||
CaptchaModuleAsyncOptions,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
@Global()
|
||||
export class CaptchaModule {
|
||||
static forRoot(options: CaptchaModuleAsyncOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: CAPTCHA_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (config.type) {
|
||||
case CaptchaDriverType.GoogleRecaptcha:
|
||||
return new GoogleRecaptchaDriver(config.options);
|
||||
case CaptchaDriverType.Turnstile:
|
||||
return new TurnstileDriver(config.options);
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
return {
|
||||
module: CaptchaModule,
|
||||
providers: [CaptchaService, provider],
|
||||
exports: [CaptchaService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
|
||||
import { CAPTCHA_DRIVER } from 'src/engine/core-modules/captcha/captcha.constants';
|
||||
import { CaptchaValidateResult } from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
@Injectable()
|
||||
export class CaptchaService implements CaptchaDriver {
|
||||
constructor(@Inject(CAPTCHA_DRIVER) private driver: CaptchaDriver) {}
|
||||
|
||||
async validate(token: string): Promise<CaptchaValidateResult> {
|
||||
if (this.driver) {
|
||||
return await this.driver.validate(token);
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import { CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
import { CaptchaServerResponse } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-server-response';
|
||||
|
||||
import {
|
||||
CaptchaDriverOptions,
|
||||
CaptchaValidateResult,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
export class GoogleRecaptchaDriver implements CaptchaDriver {
|
||||
private readonly siteKey: string;
|
||||
private readonly secretKey: string;
|
||||
private readonly httpService: AxiosInstance;
|
||||
constructor(private options: CaptchaDriverOptions) {
|
||||
this.siteKey = options.siteKey;
|
||||
this.secretKey = options.secretKey;
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(token: string): Promise<CaptchaValidateResult> {
|
||||
const formData = new URLSearchParams({
|
||||
secret: this.secretKey,
|
||||
response: token,
|
||||
});
|
||||
|
||||
const response = await this.httpService.post('', formData);
|
||||
const responseData = response.data as CaptchaServerResponse;
|
||||
|
||||
return {
|
||||
success: responseData.success,
|
||||
...(!responseData.success && {
|
||||
error: responseData['error-codes']?.[0] ?? 'Captcha Error',
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
import { CaptchaValidateResult } from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
export interface CaptchaDriver {
|
||||
validate(token: string): Promise<CaptchaValidateResult>;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
export type CaptchaServerResponse = {
|
||||
success: boolean;
|
||||
challenge_ts: string;
|
||||
hostname: string;
|
||||
'error-codes': string[];
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
import { CaptchaDriver } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-driver.interface';
|
||||
import { CaptchaServerResponse } from 'src/engine/core-modules/captcha/drivers/interfaces/captcha-server-response';
|
||||
|
||||
import {
|
||||
CaptchaDriverOptions,
|
||||
CaptchaValidateResult,
|
||||
} from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
export class TurnstileDriver implements CaptchaDriver {
|
||||
private readonly siteKey: string;
|
||||
private readonly secretKey: string;
|
||||
private readonly httpService: AxiosInstance;
|
||||
constructor(private options: CaptchaDriverOptions) {
|
||||
this.siteKey = options.siteKey;
|
||||
this.secretKey = options.secretKey;
|
||||
this.httpService = axios.create({
|
||||
baseURL: 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(token: string): Promise<CaptchaValidateResult> {
|
||||
const formData = new URLSearchParams({
|
||||
secret: this.secretKey,
|
||||
response: token,
|
||||
});
|
||||
const response = await this.httpService.post('', formData);
|
||||
|
||||
const responseData = response.data as CaptchaServerResponse;
|
||||
|
||||
return {
|
||||
success: responseData.success,
|
||||
...(!responseData.success && {
|
||||
error: responseData['error-codes']?.[0] ?? 'Captcha Error',
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum CaptchaDriverType {
|
||||
GoogleRecaptcha = 'google-recaptcha',
|
||||
Turnstile = 'turnstile',
|
||||
}
|
||||
|
||||
registerEnumType(CaptchaDriverType, {
|
||||
name: 'CaptchaDriverType',
|
||||
});
|
||||
|
||||
export type CaptchaDriverOptions = {
|
||||
siteKey: string;
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
export interface GoogleRecaptchaDriverFactoryOptions {
|
||||
type: CaptchaDriverType.GoogleRecaptcha;
|
||||
options: CaptchaDriverOptions;
|
||||
}
|
||||
|
||||
export interface TurnstileDriverFactoryOptions {
|
||||
type: CaptchaDriverType.Turnstile;
|
||||
options: CaptchaDriverOptions;
|
||||
}
|
||||
|
||||
export type CaptchaModuleOptions =
|
||||
| GoogleRecaptchaDriverFactoryOptions
|
||||
| TurnstileDriverFactoryOptions;
|
||||
|
||||
export type CaptchaModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
...args: any[]
|
||||
) => CaptchaModuleOptions | Promise<CaptchaModuleOptions> | undefined;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
|
||||
export type CaptchaValidateResult = { success: boolean; error?: string };
|
||||
@ -0,0 +1 @@
|
||||
export * from 'src/engine/core-modules/captcha/interfaces/captcha.interface';
|
||||
@ -1,6 +1,6 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { CaptchaDriverType } from 'src/engine/integrations/captcha/interfaces';
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
|
||||
@ObjectType()
|
||||
class AuthProviders {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
import { ClientConfigResolver } from './client-config.resolver';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Resolver, Query } from '@nestjs/graphql';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
import { ClientConfig } from './client-config.entity';
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { AISQLQueryModule } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.module';
|
||||
@ -15,10 +17,32 @@ import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
|
||||
import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { fileStorageModuleFactory } from 'src/engine/core-modules/file-storage/file-storage.module-factory';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { LoggerModule } from 'src/engine/core-modules/logger/logger.module';
|
||||
import { loggerModuleFactory } from 'src/engine/core-modules/logger/logger.module-factory';
|
||||
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
|
||||
import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue/message-queue.module-factory';
|
||||
import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module';
|
||||
import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory';
|
||||
import { EmailModule } from 'src/engine/core-modules/email/email.module';
|
||||
import { emailModuleFactory } from 'src/engine/core-modules/email/email.module-factory';
|
||||
import { CaptchaModule } from 'src/engine/core-modules/captcha/captcha.module';
|
||||
import { captchaModuleFactory } from 'src/engine/core-modules/captcha/captcha.module-factory';
|
||||
import { CacheStorageModule } from 'src/engine/core-modules/cache-storage/cache-storage.module';
|
||||
import { LLMChatModelModule } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module';
|
||||
import { llmChatModelModuleFactory } from 'src/engine/core-modules/llm-chat-model/llm-chat-model.module-factory';
|
||||
import { LLMTracingModule } from 'src/engine/core-modules/llm-tracing/llm-tracing.module';
|
||||
import { llmTracingModuleFactory } from 'src/engine/core-modules/llm-tracing/llm-tracing.module-factory';
|
||||
import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module';
|
||||
import { serverlessModuleFactory } from 'src/engine/core-modules/serverless/serverless-module.factory';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
|
||||
import { AnalyticsModule } from './analytics/analytics.module';
|
||||
import { ClientConfigModule } from './client-config/client-config.module';
|
||||
import { FileModule } from './file/file.module';
|
||||
import { ClientConfigModule } from './client-config/client-config.module';
|
||||
import { AnalyticsModule } from './analytics/analytics.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -40,6 +64,47 @@ import { FileModule } from './file/file.module';
|
||||
WorkflowTriggerApiModule,
|
||||
WorkspaceEventEmitterModule,
|
||||
ActorModule,
|
||||
EnvironmentModule.forRoot({}),
|
||||
FileStorageModule.forRootAsync({
|
||||
useFactory: fileStorageModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
LoggerModule.forRootAsync({
|
||||
useFactory: loggerModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
MessageQueueModule.registerAsync({
|
||||
useFactory: messageQueueModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
ExceptionHandlerModule.forRootAsync({
|
||||
useFactory: exceptionHandlerModuleFactory,
|
||||
inject: [EnvironmentService, HttpAdapterHost],
|
||||
}),
|
||||
EmailModule.forRoot({
|
||||
useFactory: emailModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
CaptchaModule.forRoot({
|
||||
useFactory: captchaModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
EventEmitterModule.forRoot({
|
||||
wildcard: true,
|
||||
}),
|
||||
CacheStorageModule,
|
||||
LLMChatModelModule.forRoot({
|
||||
useFactory: llmChatModelModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
LLMTracingModule.forRoot({
|
||||
useFactory: llmTracingModuleFactory,
|
||||
inject: [EnvironmentService],
|
||||
}),
|
||||
ServerlessModule.forRootAsync({
|
||||
useFactory: serverlessModuleFactory,
|
||||
inject: [EnvironmentService, FileStorageService],
|
||||
}),
|
||||
],
|
||||
exports: [
|
||||
AnalyticsModule,
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
export interface EmailDriver {
|
||||
send(sendMailOptions: SendMailOptions): Promise<void>;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
export class LoggerDriver implements EmailDriver {
|
||||
private readonly logger = new Logger(LoggerDriver.name);
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
const info =
|
||||
`Sent email to: ${sendMailOptions.to}\n` +
|
||||
`From: ${sendMailOptions.from}\n` +
|
||||
`Subject: ${sendMailOptions.subject}\n` +
|
||||
`Content Text: ${sendMailOptions.text}\n` +
|
||||
`Content HTML: ${sendMailOptions.html}`;
|
||||
|
||||
this.logger.log(info);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { createTransport, Transporter, SendMailOptions } from 'nodemailer';
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
export class SmtpDriver implements EmailDriver {
|
||||
private readonly logger = new Logger(SmtpDriver.name);
|
||||
private transport: Transporter;
|
||||
|
||||
constructor(options: SMTPConnection.Options) {
|
||||
this.transport = createTransport(options);
|
||||
}
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
this.transport
|
||||
.sendMail(sendMailOptions)
|
||||
.then(() =>
|
||||
this.logger.log(`Email to '${sendMailOptions.to}' successfully sent`),
|
||||
)
|
||||
.catch((err) =>
|
||||
this.logger.error(`sending email to '${sendMailOptions.to}': ${err}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailSenderService } from 'src/engine/core-modules/email/email-sender.service';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
|
||||
@Processor(MessageQueue.emailQueue)
|
||||
export class EmailSenderJob {
|
||||
constructor(private readonly emailSenderService: EmailSenderService) {}
|
||||
|
||||
@Process(EmailSenderJob.name)
|
||||
async handle(data: SendMailOptions): Promise<void> {
|
||||
await this.emailSenderService.send(data);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
import { EMAIL_DRIVER } from 'src/engine/core-modules/email/email.constants';
|
||||
|
||||
@Injectable()
|
||||
export class EmailSenderService implements EmailDriver {
|
||||
constructor(@Inject(EMAIL_DRIVER) private driver: EmailDriver) {}
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
await this.driver.send(sendMailOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const EMAIL_DRIVER = Symbol('EMAIL_DRIVER');
|
||||
@ -0,0 +1,40 @@
|
||||
import {
|
||||
EmailDriver,
|
||||
EmailModuleOptions,
|
||||
} from 'src/engine/core-modules/email/interfaces/email.interface';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
export const emailModuleFactory = (
|
||||
environmentService: EnvironmentService,
|
||||
): EmailModuleOptions => {
|
||||
const driver = environmentService.get('EMAIL_DRIVER');
|
||||
|
||||
switch (driver) {
|
||||
case EmailDriver.Logger: {
|
||||
return;
|
||||
}
|
||||
case EmailDriver.Smtp: {
|
||||
const host = environmentService.get('EMAIL_SMTP_HOST');
|
||||
const port = environmentService.get('EMAIL_SMTP_PORT');
|
||||
const user = environmentService.get('EMAIL_SMTP_USER');
|
||||
const pass = environmentService.get('EMAIL_SMTP_PASSWORD');
|
||||
|
||||
if (!(host && port)) {
|
||||
throw new Error(
|
||||
`${driver} email driver requires host: ${host} and port: ${port} to be defined, check your .env file`,
|
||||
);
|
||||
}
|
||||
|
||||
const auth = user && pass ? { user, pass } : undefined;
|
||||
|
||||
if (auth) {
|
||||
return { host, port, auth };
|
||||
}
|
||||
|
||||
return { host, port };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Invalid email driver (${driver}), check your .env file`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { EmailModuleAsyncOptions } from 'src/engine/core-modules/email/interfaces/email.interface';
|
||||
|
||||
import { EMAIL_DRIVER } from 'src/engine/core-modules/email/email.constants';
|
||||
import { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver';
|
||||
import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { EmailSenderService } from 'src/engine/core-modules/email/email-sender.service';
|
||||
|
||||
@Global()
|
||||
export class EmailModule {
|
||||
static forRoot(options: EmailModuleAsyncOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: EMAIL_DRIVER,
|
||||
useFactory: (...args: any[]) => {
|
||||
const config = options.useFactory(...args);
|
||||
|
||||
return config ? new SmtpDriver(config) : new LoggerDriver();
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
return {
|
||||
module: EmailModule,
|
||||
providers: [EmailSenderService, EmailService, provider],
|
||||
exports: [EmailSenderService, EmailService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
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 { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
constructor(
|
||||
@InjectMessageQueue(MessageQueue.emailQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
) {}
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
await this.messageQueueService.add<SendMailOptions>(
|
||||
EmailSenderJob.name,
|
||||
sendMailOptions,
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
export enum EmailDriver {
|
||||
Logger = 'logger',
|
||||
Smtp = 'smtp',
|
||||
}
|
||||
|
||||
export type EmailModuleOptions = SMTPConnection.Options | undefined;
|
||||
|
||||
export type EmailModuleAsyncOptions = {
|
||||
useFactory: (...args: any[]) => EmailModuleOptions;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
@ -0,0 +1,66 @@
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
|
||||
|
||||
class TestClass {
|
||||
@CastToLogLevelArray()
|
||||
logLevels?: any;
|
||||
}
|
||||
|
||||
describe('CastToLogLevelArray Decorator', () => {
|
||||
it('should cast "log" to ["log"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'log' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['log']);
|
||||
});
|
||||
|
||||
it('should cast "error" to ["error"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'error' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['error']);
|
||||
});
|
||||
|
||||
it('should cast "warn" to ["warn"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'warn' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['warn']);
|
||||
});
|
||||
|
||||
it('should cast "debug" to ["debug"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'debug' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['debug']);
|
||||
});
|
||||
|
||||
it('should cast "verbose" to ["verbose"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'verbose' });
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual(['verbose']);
|
||||
});
|
||||
|
||||
it('should cast "verbose,error,warn" to ["verbose", "error", "warn"]', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
logLevels: 'verbose,error,warn',
|
||||
});
|
||||
|
||||
expect(transformedClass.logLevels).toStrictEqual([
|
||||
'verbose',
|
||||
'error',
|
||||
'warn',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should cast "toto" to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, { logLevels: 'toto' });
|
||||
|
||||
expect(transformedClass.logLevels).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should cast "verbose,error,toto" to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
logLevels: 'verbose,error,toto',
|
||||
});
|
||||
|
||||
expect(transformedClass.logLevels).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,58 @@
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
|
||||
|
||||
class TestClass {
|
||||
@CastToPositiveNumber()
|
||||
numberProperty?: any;
|
||||
}
|
||||
|
||||
describe('CastToPositiveNumber Decorator', () => {
|
||||
it('should cast number to number', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: 123 });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(123);
|
||||
});
|
||||
|
||||
it('should cast string to number', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: '123' });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(123);
|
||||
});
|
||||
|
||||
it('should cast null to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: null });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast negative number to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, { numberProperty: -12 });
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast undefined to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
numberProperty: undefined,
|
||||
});
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast NaN string to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
numberProperty: 'toto',
|
||||
});
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should cast a negative string to undefined', () => {
|
||||
const transformedClass = plainToClass(TestClass, {
|
||||
numberProperty: '-123',
|
||||
});
|
||||
|
||||
expect(transformedClass.numberProperty).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToBoolean = () =>
|
||||
Transform(({ value }: { value: string }) => toBoolean(value));
|
||||
|
||||
const toBoolean = (value: any) => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value;
|
||||
}
|
||||
if (['true', 'on', 'yes', '1'].includes(value.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
if (['false', 'off', 'no', '0'].includes(value.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToLogLevelArray = () =>
|
||||
Transform(({ value }: { value: string }) => toLogLevelArray(value));
|
||||
|
||||
const toLogLevelArray = (value: any) => {
|
||||
if (typeof value === 'string') {
|
||||
const rawLogLevels = value.split(',').map((level) => level.trim());
|
||||
const isInvalid = rawLogLevels.some(
|
||||
(level) => !['log', 'error', 'warn', 'debug', 'verbose'].includes(level),
|
||||
);
|
||||
|
||||
if (!isInvalid) {
|
||||
return rawLogLevels;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToPositiveNumber = () =>
|
||||
Transform(({ value }: { value: string }) => toNumber(value));
|
||||
|
||||
const toNumber = (value: any) => {
|
||||
if (typeof value === 'number') {
|
||||
return value >= 0 ? value : undefined;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return isNaN(+value) ? undefined : toNumber(+value);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,12 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export const CastToStringArray = () =>
|
||||
Transform(({ value }: { value: string }) => toStringArray(value));
|
||||
|
||||
const toStringArray = (value: any) => {
|
||||
if (typeof value === 'string') {
|
||||
return value.split(',').map((item) => item.trim());
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
export class IsAWSRegionConstraint implements ValidatorConstraintInterface {
|
||||
validate(region: string) {
|
||||
const regex = /^[a-z]{2}-[a-z]+-\d{1}$/;
|
||||
|
||||
return regex.test(region); // Returns true if region matches regex
|
||||
}
|
||||
}
|
||||
|
||||
export const IsAWSRegion =
|
||||
(validationOptions?: ValidationOptions) =>
|
||||
(object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsAWSRegionConstraint,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
export class IsDurationConstraint implements ValidatorConstraintInterface {
|
||||
validate(duration: string) {
|
||||
const regex =
|
||||
/^-?[0-9]+(.[0-9]+)?(m(illiseconds?)?|s(econds?)?|h((ou)?rs?)?|d(ays?)?|w(eeks?)?|M(onths?)?|y(ears?)?)?$/;
|
||||
|
||||
return regex.test(duration); // Returns true if duration matches regex
|
||||
}
|
||||
}
|
||||
|
||||
export const IsDuration =
|
||||
(validationOptions?: ValidationOptions) =>
|
||||
(object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [],
|
||||
validator: IsDurationConstraint,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
|
||||
export const IsStrictlyLowerThan = (
|
||||
property: string,
|
||||
validationOptions?: ValidationOptions,
|
||||
) => {
|
||||
return (object: object, propertyName: string) => {
|
||||
registerDecorator({
|
||||
name: 'isStrictlyLowerThan',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
constraints: [property],
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const [relatedPropertyName] = args.constraints;
|
||||
const relatedValue = (args.object as any)[relatedPropertyName];
|
||||
|
||||
return (
|
||||
typeof value === 'number' &&
|
||||
typeof relatedValue === 'number' &&
|
||||
value < relatedValue
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,446 @@
|
||||
import { LogLevel } from '@nestjs/common';
|
||||
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsDefined,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUrl,
|
||||
Max,
|
||||
Min,
|
||||
ValidateIf,
|
||||
validateSync,
|
||||
} from 'class-validator';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/interfaces/email.interface';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/environment/interfaces/node-environment.interface';
|
||||
import { LLMChatModelDriver } from 'src/engine/core-modules/llm-chat-model/interfaces/llm-chat-model.interface';
|
||||
import { LLMTracingDriver } from 'src/engine/core-modules/llm-tracing/interfaces/llm-tracing.interface';
|
||||
import { AwsRegion } from 'src/engine/core-modules/environment/interfaces/aws-region.interface';
|
||||
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
|
||||
|
||||
import { IsDuration } from 'src/engine/core-modules/environment/decorators/is-duration.decorator';
|
||||
import { IsAWSRegion } from 'src/engine/core-modules/environment/decorators/is-aws-region.decorator';
|
||||
import { CastToPositiveNumber } from 'src/engine/core-modules/environment/decorators/cast-to-positive-number.decorator';
|
||||
import { CastToLogLevelArray } from 'src/engine/core-modules/environment/decorators/cast-to-log-level-array.decorator';
|
||||
import { CastToBoolean } from 'src/engine/core-modules/environment/decorators/cast-to-boolean.decorator';
|
||||
import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum';
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { CastToStringArray } from 'src/engine/core-modules/environment/decorators/cast-to-string-array.decorator';
|
||||
import { IsStrictlyLowerThan } from 'src/engine/core-modules/environment/decorators/is-strictly-lower-than.decorator';
|
||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
import { StorageDriverType } from 'src/engine/core-modules/file-storage/interfaces';
|
||||
import { LoggerDriverType } from 'src/engine/core-modules/logger/interfaces';
|
||||
import { MessageQueueDriverType } from 'src/engine/core-modules/message-queue/interfaces';
|
||||
import { ServerlessDriverType } from 'src/engine/core-modules/serverless/serverless.interface';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
export class EnvironmentVariables {
|
||||
// Misc
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
DEBUG_MODE = false;
|
||||
|
||||
@IsEnum(NodeEnvironment)
|
||||
@IsString()
|
||||
NODE_ENV: NodeEnvironment = NodeEnvironment.development;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(65535)
|
||||
DEBUG_PORT = 9000;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
IS_BILLING_ENABLED = false;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_PLAN_REQUIRED_LINK: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_STRIPE_BASE_PLAN_PRODUCT_ID: string;
|
||||
|
||||
@IsNumber()
|
||||
@CastToPositiveNumber()
|
||||
@IsOptional()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_FREE_TRIAL_DURATION_IN_DAYS = 7;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_STRIPE_API_KEY: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_STRIPE_WEBHOOK_SECRET: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
TELEMETRY_ENABLED = true;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
PORT = 3000;
|
||||
|
||||
// Database
|
||||
@IsDefined()
|
||||
@IsUrl({
|
||||
protocols: ['postgres'],
|
||||
require_tld: false,
|
||||
allow_underscores: true,
|
||||
})
|
||||
PG_DATABASE_URL: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
PG_SSL_ALLOW_SELF_SIGNED = false;
|
||||
|
||||
// Frontend URL
|
||||
@IsUrl({ require_tld: false })
|
||||
FRONT_BASE_URL: string;
|
||||
|
||||
// Server URL
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsOptional()
|
||||
SERVER_URL: string;
|
||||
|
||||
// Json Web Token
|
||||
@IsString()
|
||||
ACCESS_TOKEN_SECRET: string;
|
||||
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
ACCESS_TOKEN_EXPIRES_IN = '30m';
|
||||
|
||||
@IsString()
|
||||
REFRESH_TOKEN_SECRET: string;
|
||||
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
REFRESH_TOKEN_EXPIRES_IN = '60d';
|
||||
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
REFRESH_TOKEN_COOL_DOWN = '1m';
|
||||
|
||||
@IsString()
|
||||
LOGIN_TOKEN_SECRET = '30m';
|
||||
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
LOGIN_TOKEN_EXPIRES_IN = '15m';
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
FILE_TOKEN_SECRET = 'random_string';
|
||||
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
FILE_TOKEN_EXPIRES_IN = '1d';
|
||||
|
||||
// Auth
|
||||
@IsUrl({ require_tld: false })
|
||||
@IsOptional()
|
||||
FRONT_AUTH_CALLBACK_URL: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_PASSWORD_ENABLED = true;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ValidateIf((env) => env.AUTH_PASSWORD_ENABLED)
|
||||
SIGN_IN_PREFILLED = false;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_MICROSOFT_ENABLED = false;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_CLIENT_ID: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_TENANT_ID: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_CLIENT_SECRET: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_CALLBACK_URL: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_GOOGLE_ENABLED = false;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||
AUTH_GOOGLE_CLIENT_ID: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||
AUTH_GOOGLE_CLIENT_SECRET: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||
AUTH_GOOGLE_CALLBACK_URL: string;
|
||||
|
||||
// Custom Code Engine
|
||||
@IsEnum(ServerlessDriverType)
|
||||
@IsOptional()
|
||||
SERVERLESS_TYPE: ServerlessDriverType = ServerlessDriverType.Local;
|
||||
|
||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||
@IsAWSRegion()
|
||||
SERVERLESS_LAMBDA_REGION: AwsRegion;
|
||||
|
||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
SERVERLESS_LAMBDA_ROLE: string;
|
||||
|
||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
SERVERLESS_LAMBDA_ACCESS_KEY_ID: string;
|
||||
|
||||
@ValidateIf((env) => env.SERVERLESS_TYPE === ServerlessDriverType.Lambda)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
SERVERLESS_LAMBDA_SECRET_ACCESS_KEY: string;
|
||||
|
||||
// Storage
|
||||
@IsEnum(StorageDriverType)
|
||||
@IsOptional()
|
||||
STORAGE_TYPE: StorageDriverType = StorageDriverType.Local;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsAWSRegion()
|
||||
STORAGE_S3_REGION: AwsRegion;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsString()
|
||||
STORAGE_S3_NAME: string;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
STORAGE_S3_ENDPOINT: string;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
STORAGE_S3_ACCESS_KEY_ID: string;
|
||||
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.S3)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
STORAGE_S3_SECRET_ACCESS_KEY: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.STORAGE_TYPE === StorageDriverType.Local)
|
||||
STORAGE_LOCAL_PATH = '.local-storage';
|
||||
|
||||
// Support
|
||||
@IsEnum(SupportDriver)
|
||||
@IsOptional()
|
||||
SUPPORT_DRIVER: SupportDriver = SupportDriver.None;
|
||||
|
||||
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
||||
@IsString()
|
||||
SUPPORT_FRONT_CHAT_ID: string;
|
||||
|
||||
@ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front)
|
||||
@IsString()
|
||||
SUPPORT_FRONT_HMAC_KEY: string;
|
||||
|
||||
@IsEnum(LoggerDriverType)
|
||||
@IsOptional()
|
||||
LOGGER_DRIVER: LoggerDriverType = LoggerDriverType.Console;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
LOGGER_IS_BUFFER_ENABLED = true;
|
||||
|
||||
@IsEnum(ExceptionHandlerDriver)
|
||||
@IsOptional()
|
||||
EXCEPTION_HANDLER_DRIVER: ExceptionHandlerDriver =
|
||||
ExceptionHandlerDriver.Console;
|
||||
|
||||
@CastToLogLevelArray()
|
||||
@IsOptional()
|
||||
LOG_LEVELS: LogLevel[] = ['log', 'error', 'warn'];
|
||||
|
||||
@CastToStringArray()
|
||||
@IsOptional()
|
||||
DEMO_WORKSPACE_IDS: string[] = [];
|
||||
|
||||
@ValidateIf(
|
||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||
)
|
||||
@IsString()
|
||||
SENTRY_DSN: string;
|
||||
|
||||
@ValidateIf(
|
||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||
)
|
||||
@IsString()
|
||||
SENTRY_FRONT_DSN: string;
|
||||
|
||||
@ValidateIf(
|
||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||
)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
SENTRY_RELEASE: string;
|
||||
|
||||
@ValidateIf(
|
||||
(env) => env.EXCEPTION_HANDLER_DRIVER === ExceptionHandlerDriver.Sentry,
|
||||
)
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
SENTRY_ENVIRONMENT: string;
|
||||
|
||||
@IsDuration()
|
||||
@IsOptional()
|
||||
PASSWORD_RESET_TOKEN_EXPIRES_IN = '5m';
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsNumber()
|
||||
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0)
|
||||
@IsStrictlyLowerThan('WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', {
|
||||
message:
|
||||
'"WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION" should be strictly lower that "WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION"',
|
||||
})
|
||||
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION > 0)
|
||||
WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION = 30;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsNumber()
|
||||
@ValidateIf((env) => env.WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION > 0)
|
||||
WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION = 60;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
IS_SIGN_UP_DISABLED = false;
|
||||
|
||||
@IsEnum(CaptchaDriverType)
|
||||
@IsOptional()
|
||||
CAPTCHA_DRIVER?: CaptchaDriverType;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
CAPTCHA_SITE_KEY?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
CAPTCHA_SECRET_KEY?: string;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
MUTATION_MAXIMUM_AFFECTED_RECORDS = 100;
|
||||
|
||||
REDIS_HOST = '127.0.0.1';
|
||||
|
||||
@CastToPositiveNumber()
|
||||
REDIS_PORT = 6379;
|
||||
|
||||
REDIS_USERNAME: string;
|
||||
|
||||
REDIS_PASSWORD: string;
|
||||
|
||||
API_TOKEN_EXPIRES_IN = '100y';
|
||||
|
||||
SHORT_TERM_TOKEN_EXPIRES_IN = '5m';
|
||||
|
||||
@CastToBoolean()
|
||||
MESSAGING_PROVIDER_GMAIL_ENABLED = false;
|
||||
|
||||
MESSAGE_QUEUE_TYPE: string = MessageQueueDriverType.Sync;
|
||||
|
||||
EMAIL_FROM_ADDRESS = 'noreply@yourdomain.com';
|
||||
|
||||
EMAIL_SYSTEM_ADDRESS = 'system@yourdomain.com';
|
||||
|
||||
EMAIL_FROM_NAME = 'Felix from Twenty';
|
||||
|
||||
EMAIL_DRIVER: EmailDriver = EmailDriver.Logger;
|
||||
|
||||
EMAIL_SMTP_HOST: string;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
EMAIL_SMTP_PORT = 587;
|
||||
|
||||
EMAIL_SMTP_USER: string;
|
||||
|
||||
EMAIL_SMTP_PASSWORD: string;
|
||||
|
||||
LLM_CHAT_MODEL_DRIVER: LLMChatModelDriver;
|
||||
|
||||
OPENAI_API_KEY: string;
|
||||
|
||||
LANGFUSE_SECRET_KEY: string;
|
||||
|
||||
LANGFUSE_PUBLIC_KEY: string;
|
||||
|
||||
LLM_TRACING_DRIVER: LLMTracingDriver = LLMTracingDriver.Console;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
API_RATE_LIMITING_TTL = 100;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
API_RATE_LIMITING_LIMIT = 500;
|
||||
|
||||
CACHE_STORAGE_TYPE: CacheStorageType = CacheStorageType.Memory;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
CACHE_STORAGE_TTL: number = 3600 * 24 * 7;
|
||||
|
||||
@CastToBoolean()
|
||||
CALENDAR_PROVIDER_GOOGLE_ENABLED = false;
|
||||
|
||||
AUTH_GOOGLE_APIS_CALLBACK_URL: string;
|
||||
|
||||
CHROME_EXTENSION_ID: string;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
SERVERLESS_FUNCTION_EXEC_THROTTLE_LIMIT = 10;
|
||||
|
||||
// milliseconds
|
||||
@CastToPositiveNumber()
|
||||
SERVERLESS_FUNCTION_EXEC_THROTTLE_TTL = 1000;
|
||||
}
|
||||
|
||||
export const validate = (
|
||||
config: Record<string, unknown>,
|
||||
): EnvironmentVariables => {
|
||||
const validatedConfig = plainToClass(EnvironmentVariables, config);
|
||||
|
||||
const errors = validateSync(validatedConfig);
|
||||
|
||||
assert(!errors.length, errors.toString());
|
||||
|
||||
return validatedConfig;
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } =
|
||||
new ConfigurableModuleBuilder({
|
||||
moduleName: 'Environment',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
@ -0,0 +1,20 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { ConfigurableModuleClass } from 'src/engine/core-modules/environment/environment.module-definition';
|
||||
import { validate } from 'src/engine/core-modules/environment/environment-variables';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
expandVariables: true,
|
||||
validate,
|
||||
}),
|
||||
],
|
||||
providers: [EnvironmentService],
|
||||
exports: [EnvironmentService],
|
||||
})
|
||||
export class EnvironmentModule extends ConfigurableModuleClass {}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
describe('EnvironmentService', () => {
|
||||
let service: EnvironmentService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EnvironmentService,
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EnvironmentService>(EnvironmentService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
import { EnvironmentVariables } from 'src/engine/core-modules/environment/environment-variables';
|
||||
|
||||
@Injectable()
|
||||
export class EnvironmentService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
get<T extends keyof EnvironmentVariables>(key: T): EnvironmentVariables[T] {
|
||||
return this.configService.get<EnvironmentVariables[T]>(
|
||||
key,
|
||||
new EnvironmentVariables()[key],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export type AwsRegion = `${string}-${string}-${number}`;
|
||||
@ -0,0 +1,4 @@
|
||||
export enum NodeEnvironment {
|
||||
development = 'development',
|
||||
production = 'production',
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export enum SupportDriver {
|
||||
None = 'none',
|
||||
Front = 'front',
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event';
|
||||
|
||||
export class ObjectRecordCreateEvent<T> extends ObjectRecordBaseEvent {
|
||||
properties: {
|
||||
after: T;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event';
|
||||
|
||||
export class ObjectRecordDeleteEvent<T> extends ObjectRecordBaseEvent {
|
||||
properties: {
|
||||
before: T;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event';
|
||||
|
||||
export class ObjectRecordUpdateEvent<T> extends ObjectRecordBaseEvent {
|
||||
properties: {
|
||||
updatedFields: string[];
|
||||
before: T;
|
||||
after: T;
|
||||
diff?: Partial<T>;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
export class ObjectRecordBaseEvent {
|
||||
recordId: string;
|
||||
userId?: string;
|
||||
workspaceMemberId?: string;
|
||||
objectMetadata: ObjectMetadataInterface;
|
||||
properties: any;
|
||||
}
|
||||
|
||||
export class ObjectRecordBaseEventWithNameAndWorkspaceId extends ObjectRecordBaseEvent {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { objectRecordChangedValues } from 'src/engine/core-modules/event-emitter/utils/object-record-changed-values';
|
||||
|
||||
const mockObjectMetadata: ObjectMetadataInterface = {
|
||||
id: '1',
|
||||
nameSingular: 'Object',
|
||||
namePlural: 'Objects',
|
||||
labelSingular: 'Object',
|
||||
labelPlural: 'Objects',
|
||||
description: 'Test object metadata',
|
||||
targetTableName: 'test_table',
|
||||
fromRelations: [],
|
||||
toRelations: [],
|
||||
fields: [],
|
||||
isSystem: false,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isRemote: false,
|
||||
isAuditLogged: true,
|
||||
};
|
||||
|
||||
describe('objectRecordChangedValues', () => {
|
||||
it('detects changes in scalar values correctly', () => {
|
||||
const oldRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516m',
|
||||
name: 'Original Name',
|
||||
updatedAt: new Date().toString(),
|
||||
};
|
||||
const newRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516m',
|
||||
name: 'Updated Name',
|
||||
updatedAt: new Date().toString(),
|
||||
};
|
||||
|
||||
const result = objectRecordChangedValues(
|
||||
oldRecord,
|
||||
newRecord,
|
||||
['name'],
|
||||
mockObjectMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: { before: 'Original Name', after: 'Updated Name' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores changes to the updatedAt field', () => {
|
||||
const oldRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516d',
|
||||
updatedAt: new Date('2020-01-01').toDateString(),
|
||||
};
|
||||
const newRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516d',
|
||||
updatedAt: new Date('2024-01-01').toDateString(),
|
||||
};
|
||||
|
||||
const result = objectRecordChangedValues(
|
||||
oldRecord,
|
||||
newRecord,
|
||||
[],
|
||||
mockObjectMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('returns an empty object when there are no changes', () => {
|
||||
const oldRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516k',
|
||||
name: 'Name',
|
||||
value: 100,
|
||||
};
|
||||
const newRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516k',
|
||||
name: 'Name',
|
||||
value: 100,
|
||||
};
|
||||
|
||||
const result = objectRecordChangedValues(
|
||||
oldRecord,
|
||||
newRecord,
|
||||
['name', 'value'],
|
||||
mockObjectMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('correctly handles a mix of changed, unchanged, and special case values', () => {
|
||||
const oldRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516l',
|
||||
name: 'Original',
|
||||
status: 'active',
|
||||
updatedAt: new Date(2020, 1, 1).toDateString(),
|
||||
config: { theme: 'dark' },
|
||||
};
|
||||
const newRecord = {
|
||||
id: '74316f58-29b0-4a6a-b8fa-d2b506d5516l',
|
||||
name: 'Updated',
|
||||
status: 'active',
|
||||
updatedAt: new Date(2021, 1, 1).toDateString(),
|
||||
config: { theme: 'light' },
|
||||
};
|
||||
const expectedChanges = {
|
||||
name: { before: 'Original', after: 'Updated' },
|
||||
config: { before: { theme: 'dark' }, after: { theme: 'light' } },
|
||||
};
|
||||
|
||||
const result = objectRecordChangedValues(
|
||||
oldRecord,
|
||||
newRecord,
|
||||
['name', 'config', 'status'],
|
||||
mockObjectMetadata,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expectedChanges);
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import { Record } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||
|
||||
export const objectRecordChangedProperties = <
|
||||
PRecord extends Partial<Record | BaseWorkspaceEntity> = Partial<Record>,
|
||||
>(
|
||||
oldRecord: PRecord,
|
||||
newRecord: PRecord,
|
||||
) => {
|
||||
const changedProperties = Object.keys(newRecord).filter(
|
||||
(key) => !deepEqual(oldRecord[key], newRecord[key]),
|
||||
);
|
||||
|
||||
return changedProperties;
|
||||
};
|
||||
@ -0,0 +1,41 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
export const objectRecordChangedValues = (
|
||||
oldRecord: Partial<IRecord>,
|
||||
newRecord: Partial<IRecord>,
|
||||
updatedKeys: string[],
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
) => {
|
||||
const fieldsByKey = new Map(
|
||||
objectMetadata.fields.map((field) => [field.name, field]),
|
||||
);
|
||||
|
||||
const changedValues = Object.keys(newRecord).reduce(
|
||||
(acc, key) => {
|
||||
const field = fieldsByKey.get(key);
|
||||
const oldRecordValue = oldRecord[key];
|
||||
const newRecordValue = newRecord[key];
|
||||
|
||||
if (
|
||||
key === 'updatedAt' ||
|
||||
!updatedKeys.includes(key) ||
|
||||
field?.type === FieldMetadataType.RELATION ||
|
||||
deepEqual(oldRecordValue, newRecordValue)
|
||||
) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[key] = { before: oldRecordValue, after: newRecordValue };
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { before: any; after: any }>,
|
||||
);
|
||||
|
||||
return changedValues;
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
export function objectRecordDiffMerge(
|
||||
oldRecord: Record<string, any>,
|
||||
newRecord: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = { diff: {} };
|
||||
|
||||
// Iterate over the keys in the oldRecord diff
|
||||
Object.keys(oldRecord.diff ?? {}).forEach((key) => {
|
||||
if (newRecord.diff && newRecord.diff[key]) {
|
||||
// If the key also exists in the newRecord, merge the 'before' from the oldRecord and the 'after' from the newRecord
|
||||
result.diff[key] = {
|
||||
before: oldRecord.diff[key].before,
|
||||
after: newRecord.diff[key].after,
|
||||
};
|
||||
} else {
|
||||
// If the key does not exist in the newRecord, copy it as is from the oldRecord
|
||||
result.diff[key] = oldRecord.diff[key];
|
||||
}
|
||||
});
|
||||
|
||||
// Iterate over the keys in the newRecord diff to catch any that weren't in the oldRecord
|
||||
Object.keys(newRecord.diff ?? {}).forEach((key) => {
|
||||
if (!result.diff[key]) {
|
||||
// If the key was not already added from the oldRecord, add it from the newRecord
|
||||
result.diff[key] = newRecord.diff[key];
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/engine/core-modules/exception-handler/exception-handler.constants';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
describe('ExceptionHandlerService', () => {
|
||||
let service: ExceptionHandlerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ExceptionHandlerService,
|
||||
{
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ExceptionHandlerService>(ExceptionHandlerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
/* eslint-disable no-console */
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerConsoleDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
) {
|
||||
console.group('Exception Captured');
|
||||
console.info(options);
|
||||
console.error(exceptions);
|
||||
console.groupEnd();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser): void {
|
||||
console.group('Message Captured');
|
||||
console.info(user);
|
||||
console.info(message);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { ProfilingIntegration } from '@sentry/profiling-node';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import {
|
||||
ExceptionHandlerDriverInterface,
|
||||
ExceptionHandlerSentryDriverFactoryOptions,
|
||||
} from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerSentryDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
constructor(options: ExceptionHandlerSentryDriverFactoryOptions['options']) {
|
||||
Sentry.init({
|
||||
environment: options.environment,
|
||||
release: options.release,
|
||||
dsn: options.dsn,
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Sentry.Integrations.Express({ app: options.serverInstance }),
|
||||
new Sentry.Integrations.GraphQL(),
|
||||
new Sentry.Integrations.Postgres(),
|
||||
new ProfilingIntegration(),
|
||||
],
|
||||
tracesSampleRate: 0.1,
|
||||
profilesSampleRate: 0.3,
|
||||
debug: options.debug,
|
||||
});
|
||||
}
|
||||
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
) {
|
||||
const eventIds: string[] = [];
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
if (options?.operation) {
|
||||
scope.setTag('operation', options.operation.name);
|
||||
scope.setTag('operationName', options.operation.name);
|
||||
}
|
||||
|
||||
if (options?.document) {
|
||||
scope.setExtra('document', options.document);
|
||||
}
|
||||
|
||||
if (options?.user) {
|
||||
scope.setUser({
|
||||
id: options.user.id,
|
||||
email: options.user.email,
|
||||
firstName: options.user.firstName,
|
||||
lastName: options.user.lastName,
|
||||
workspaceId: options.user.workspaceId,
|
||||
workspaceDisplayName: options.user.workspaceDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
for (const exception of exceptions) {
|
||||
const errorPath = (exception.path ?? [])
|
||||
.map((v: string | number) => (typeof v === 'number' ? '$index' : v))
|
||||
.join(' > ');
|
||||
|
||||
if (errorPath) {
|
||||
scope.addBreadcrumb({
|
||||
category: 'execution-path',
|
||||
message: errorPath,
|
||||
level: 'debug',
|
||||
});
|
||||
}
|
||||
|
||||
const eventId = Sentry.captureException(exception, {
|
||||
fingerprint: [
|
||||
'graphql',
|
||||
errorPath,
|
||||
options?.operation?.name,
|
||||
options?.operation?.type,
|
||||
],
|
||||
contexts: {
|
||||
GraphQL: {
|
||||
operationName: options?.operation?.name,
|
||||
operationType: options?.operation?.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
eventIds.push(eventId);
|
||||
}
|
||||
});
|
||||
|
||||
return eventIds;
|
||||
}
|
||||
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser) {
|
||||
Sentry.captureMessage(message, (scope) => {
|
||||
if (user) {
|
||||
scope.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
workspaceId: user.workspaceId,
|
||||
workspaceDisplayName: user.workspaceDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
return scope;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const EXCEPTION_HANDLER_DRIVER = Symbol('EXCEPTION_HANDLER_DRIVER');
|
||||
@ -0,0 +1,14 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerModuleOptions } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
export const {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} = new ConfigurableModuleBuilder<ExceptionHandlerModuleOptions>({
|
||||
moduleName: 'ExceptionHandlerModule',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
@ -0,0 +1,42 @@
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OPTIONS_TYPE } from 'src/engine/core-modules/exception-handler/exception-handler.module-definition';
|
||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
/**
|
||||
* ExceptionHandler Module factory
|
||||
* @returns ExceptionHandlerModuleOptions
|
||||
* @param environmentService
|
||||
* @param adapterHost
|
||||
*/
|
||||
export const exceptionHandlerModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
adapterHost: HttpAdapterHost,
|
||||
): Promise<typeof OPTIONS_TYPE> => {
|
||||
const driverType = environmentService.get('EXCEPTION_HANDLER_DRIVER');
|
||||
|
||||
switch (driverType) {
|
||||
case ExceptionHandlerDriver.Console: {
|
||||
return {
|
||||
type: ExceptionHandlerDriver.Console,
|
||||
};
|
||||
}
|
||||
case ExceptionHandlerDriver.Sentry: {
|
||||
return {
|
||||
type: ExceptionHandlerDriver.Sentry,
|
||||
options: {
|
||||
environment: environmentService.get('SENTRY_ENVIRONMENT'),
|
||||
release: environmentService.get('SENTRY_RELEASE'),
|
||||
dsn: environmentService.get('SENTRY_DSN') ?? '',
|
||||
serverInstance: adapterHost.httpAdapter?.getInstance(),
|
||||
debug: environmentService.get('DEBUG_MODE'),
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid exception capturer driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/engine/core-modules/exception-handler/exception-handler.constants';
|
||||
import {
|
||||
ConfigurableModuleClass,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} from 'src/engine/core-modules/exception-handler/exception-handler.module-definition';
|
||||
import { ExceptionHandlerConsoleDriver } from 'src/engine/core-modules/exception-handler/drivers/console.driver';
|
||||
import { ExceptionHandlerSentryDriver } from 'src/engine/core-modules/exception-handler/drivers/sentry.driver';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ExceptionHandlerService],
|
||||
exports: [ExceptionHandlerService],
|
||||
})
|
||||
export class ExceptionHandlerModule extends ConfigurableModuleClass {
|
||||
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useValue:
|
||||
options.type === ExceptionHandlerDriver.Console
|
||||
? new ExceptionHandlerConsoleDriver()
|
||||
: new ExceptionHandlerSentryDriver(options.options),
|
||||
};
|
||||
const dynamicModule = super.forRoot(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options?.useFactory?.(...args);
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config.type === ExceptionHandlerDriver.Console
|
||||
? new ExceptionHandlerConsoleDriver()
|
||||
: new ExceptionHandlerSentryDriver(config.options);
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
const dynamicModule = super.forRootAsync(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/engine/core-modules/exception-handler/exception-handler.constants';
|
||||
|
||||
@Injectable()
|
||||
export class ExceptionHandlerService {
|
||||
constructor(
|
||||
@Inject(EXCEPTION_HANDLER_DRIVER)
|
||||
private driver: ExceptionHandlerDriverInterface,
|
||||
) {}
|
||||
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
): string[] {
|
||||
return this.driver.captureExceptions(exceptions, options);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
handleStreamOrSingleExecutionResult,
|
||||
Plugin,
|
||||
getDocumentString,
|
||||
} from '@envelop/core';
|
||||
import { OperationDefinitionNode, Kind, print } from 'graphql';
|
||||
|
||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
|
||||
|
||||
export const useSentryTracing = <
|
||||
PluginContext extends GraphQLContext,
|
||||
>(): Plugin<PluginContext> => {
|
||||
return {
|
||||
onExecute({ args }) {
|
||||
const transactionName = args.operationName || 'Anonymous Operation';
|
||||
const rootOperation = args.document.definitions.find(
|
||||
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
||||
) as OperationDefinitionNode;
|
||||
const operationType = rootOperation.operation;
|
||||
|
||||
const user = args.contextValue.user;
|
||||
const workspace = args.contextValue.workspace;
|
||||
const document = getDocumentString(args.document, print);
|
||||
|
||||
Sentry.setTags({
|
||||
operationName: transactionName,
|
||||
operation: operationType,
|
||||
});
|
||||
|
||||
const scope = Sentry.getCurrentScope();
|
||||
|
||||
scope.setTransactionName(transactionName);
|
||||
|
||||
if (user) {
|
||||
scope.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
workspaceId: workspace?.id,
|
||||
workspaceDisplayName: workspace?.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
if (document) {
|
||||
scope.setExtra('document', document);
|
||||
}
|
||||
|
||||
return {
|
||||
onExecuteDone(payload) {
|
||||
return handleStreamOrSingleExecutionResult(payload, () => {});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
|
||||
export interface ExceptionHandlerDriverInterface {
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
): string[];
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser): void;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { OperationTypeNode } from 'graphql';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
|
||||
export interface ExceptionHandlerOptions {
|
||||
operation?: {
|
||||
type: OperationTypeNode;
|
||||
name: string;
|
||||
};
|
||||
document?: string;
|
||||
user?: ExceptionHandlerUser | null;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
export interface ExceptionHandlerUser {
|
||||
id?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
workspaceId?: string;
|
||||
workspaceDisplayName?: string;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user