Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36952cf81c | ||
|
|
4f439bbe43 | ||
|
|
4fbd8b207d | ||
|
|
f7d812fca6 | ||
|
|
c3a320c27b |
+1
-1
@@ -4,6 +4,6 @@ set -e
|
||||
echo "==> START Registering cron jobs"
|
||||
|
||||
cd /app/packages/twenty-server
|
||||
yarn command:prod cron:register:all --dev-mode
|
||||
yarn command:prod cron:register:all
|
||||
|
||||
echo "==> DONE"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
|
||||
import { MarketplaceCatalogSyncCronCommand } from 'src/engine/core-modules/application/application-marketplace/crons/commands/marketplace-catalog-sync.cron.command';
|
||||
import { StaleRegistrationCleanupCronCommand } from 'src/engine/core-modules/application/application-oauth/stale-registration-cleanup/commands/stale-registration-cleanup.cron.command';
|
||||
@@ -68,33 +68,8 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
super();
|
||||
}
|
||||
|
||||
private devMode = false;
|
||||
|
||||
@Option({
|
||||
flags: '--dev-mode',
|
||||
description:
|
||||
'Only register cron jobs relevant to app development (cron triggers, marketplace sync, version check, stale cleanup)',
|
||||
required: false,
|
||||
})
|
||||
parseDevMode(): boolean {
|
||||
this.devMode = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly DEV_MODE_COMMANDS = new Set([
|
||||
'CronTrigger',
|
||||
'MarketplaceCatalogSync',
|
||||
'ApplicationVersionCheck',
|
||||
'StaleRegistrationCleanup',
|
||||
]);
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.logger.log(
|
||||
this.devMode
|
||||
? 'Registering app-dev cron jobs...'
|
||||
: 'Registering all background sync cron jobs...',
|
||||
);
|
||||
this.logger.log('Registering all background sync cron jobs...');
|
||||
|
||||
const allCommands = [
|
||||
{
|
||||
@@ -195,18 +170,12 @@ export class CronRegisterAllCommand extends CommandRunner {
|
||||
},
|
||||
];
|
||||
|
||||
const commands = this.devMode
|
||||
? allCommands.filter(({ name }) =>
|
||||
CronRegisterAllCommand.DEV_MODE_COMMANDS.has(name),
|
||||
)
|
||||
: allCommands;
|
||||
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
const failures: string[] = [];
|
||||
const successes: string[] = [];
|
||||
|
||||
for (const { name, command } of commands) {
|
||||
for (const { name, command } of allCommands) {
|
||||
try {
|
||||
this.logger.log(`Registering ${name} cron job...`);
|
||||
await command.run();
|
||||
|
||||
+11
-4
@@ -6,19 +6,26 @@ import {
|
||||
DnsManagerException,
|
||||
DnsManagerExceptionCode,
|
||||
} from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception';
|
||||
import {
|
||||
BaseGraphQLError,
|
||||
ConflictError,
|
||||
NotFoundError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
|
||||
@Catch(DnsManagerException)
|
||||
export class DnsManagerExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: DnsManagerException) {
|
||||
switch (exception.code) {
|
||||
case DnsManagerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
case DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED:
|
||||
case DnsManagerExceptionCode.HOSTNAME_NOT_REGISTERED:
|
||||
case DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL:
|
||||
throw new NotFoundError(exception);
|
||||
case DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED:
|
||||
throw new ConflictError(exception);
|
||||
case DnsManagerExceptionCode.INVALID_INPUT_DATA:
|
||||
case DnsManagerExceptionCode.CLOUDFLARE_CLIENT_NOT_INITIALIZED:
|
||||
case DnsManagerExceptionCode.MULTIPLE_HOSTNAMES_FOUND:
|
||||
case DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL:
|
||||
throw exception;
|
||||
case DnsManagerExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
throw new BaseGraphQLError(exception);
|
||||
default: {
|
||||
assertUnreachable(exception.code);
|
||||
}
|
||||
|
||||
+57
@@ -288,6 +288,63 @@ describe('DnsManagerService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshHostname', () => {
|
||||
it('should throw DnsManagerException when hostname is not found in Cloudflare', async () => {
|
||||
const hostname = 'example.com';
|
||||
const cloudflareMock = {
|
||||
customHostnames: {
|
||||
list: jest.fn().mockResolvedValueOnce({ result: [] }),
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
|
||||
(dnsManagerService as any).cloudflareClient = cloudflareMock;
|
||||
|
||||
await expect(
|
||||
dnsManagerService.refreshHostname(hostname),
|
||||
).rejects.toThrow(DnsManagerException);
|
||||
});
|
||||
|
||||
it('should refresh and return hostname records when hostname exists', async () => {
|
||||
const hostname = 'example.com';
|
||||
const mockResult = {
|
||||
id: 'custom-id',
|
||||
hostname,
|
||||
verification_errors: [],
|
||||
ssl: {
|
||||
dcv_delegation_records: [],
|
||||
},
|
||||
};
|
||||
const cloudflareMock = {
|
||||
customHostnames: {
|
||||
list: jest.fn().mockResolvedValueOnce({ result: [mockResult] }),
|
||||
edit: jest.fn().mockResolvedValueOnce({}),
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id');
|
||||
jest
|
||||
.spyOn(domainServerConfigService, 'getBaseUrl')
|
||||
.mockReturnValue(new URL('https://front.domain'));
|
||||
(dnsManagerService as any).cloudflareClient = cloudflareMock;
|
||||
|
||||
const result = await dnsManagerService.refreshHostname(hostname);
|
||||
|
||||
expect(result).toEqual({
|
||||
id: 'custom-id',
|
||||
domain: hostname,
|
||||
records: expect.any(Array),
|
||||
});
|
||||
expect(cloudflareMock.customHostnames.edit).toHaveBeenCalledWith(
|
||||
'custom-id',
|
||||
{
|
||||
zone_id: 'test-zone-id',
|
||||
ssl: expect.any(Object),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateHostname', () => {
|
||||
it('should update a custom domain and register a new one', async () => {
|
||||
const fromHostname = 'old.com';
|
||||
|
||||
+10
-1
@@ -138,7 +138,16 @@ export class DnsManagerService {
|
||||
options,
|
||||
);
|
||||
|
||||
assertIsDefinedOrThrow(publicDomainWithRecords);
|
||||
assertIsDefinedOrThrow(
|
||||
publicDomainWithRecords,
|
||||
new DnsManagerException(
|
||||
'Hostname not found in Cloudflare',
|
||||
DnsManagerExceptionCode.HOSTNAME_NOT_REGISTERED,
|
||||
{
|
||||
userFriendlyMessage: msg`Domain is not registered in Cloudflare`,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await this.cloudflareClient.customHostnames.edit(
|
||||
publicDomainWithRecords.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
|
||||
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
@@ -31,6 +32,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@UseFilters(
|
||||
DnsManagerExceptionFilter,
|
||||
PublicDomainExceptionFilter,
|
||||
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/bill
|
||||
import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records';
|
||||
import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter';
|
||||
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
|
||||
import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
@@ -82,6 +83,7 @@ const OriginHeader = createParamDecorator(
|
||||
@MetadataResolver(() => WorkspaceEntity)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@UseFilters(
|
||||
DnsManagerExceptionFilter,
|
||||
PreventNestToAutoLogGraphqlErrorsFilter,
|
||||
PermissionsGraphqlApiExceptionFilter,
|
||||
)
|
||||
|
||||
+1
@@ -7,6 +7,7 @@ export const WORKFLOW_SYSTEM_PROMPTS = {
|
||||
|
||||
Tool usage strategy:
|
||||
- Chain multiple tools to solve complex tasks
|
||||
- Prefer batch tools (\`create_many_*\`, \`update_many_*\`, etc.) over looping single-item calls
|
||||
- If a tool fails, try alternative approaches
|
||||
- Use results from one tool to inform the next
|
||||
- Don't give up after first failure - be persistent
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ For simple CRUD operations (find/create/update/delete a record), you do NOT need
|
||||
- Always apply filters to narrow results — don't fetch all records of a type.
|
||||
- Fetch one type of data at a time and check if you have what you need before fetching more.
|
||||
- Every record returned consumes context. Fetching too many records at once will cause failures.
|
||||
- For multiple items of the same type, use batch tools (\`create_many_*\`, \`update_many_*\`, etc.) instead of looping single-item calls.
|
||||
|
||||
## Tool Strategy
|
||||
|
||||
|
||||
@@ -1304,6 +1304,17 @@
|
||||
"contextWindowTokens": 64000,
|
||||
"maxOutputTokens": 64000
|
||||
},
|
||||
{
|
||||
"name": "mistral-medium-2604",
|
||||
"label": "Mistral Medium 3.5",
|
||||
"modelFamily": "MISTRAL",
|
||||
"inputCostPerMillionTokens": 1.5,
|
||||
"outputCostPerMillionTokens": 7.5,
|
||||
"contextWindowTokens": 262144,
|
||||
"maxOutputTokens": 262144,
|
||||
"modalities": ["image"],
|
||||
"supportsReasoning": true
|
||||
},
|
||||
{
|
||||
"name": "devstral-small-2505",
|
||||
"label": "Devstral Small 2505",
|
||||
@@ -1322,16 +1333,6 @@
|
||||
"contextWindowTokens": 128000,
|
||||
"maxOutputTokens": 128000
|
||||
},
|
||||
{
|
||||
"name": "mistral-medium-latest",
|
||||
"label": "Mistral Medium (latest)",
|
||||
"modelFamily": "MISTRAL",
|
||||
"inputCostPerMillionTokens": 0.4,
|
||||
"outputCostPerMillionTokens": 2,
|
||||
"contextWindowTokens": 128000,
|
||||
"maxOutputTokens": 16384,
|
||||
"modalities": ["image"]
|
||||
},
|
||||
{
|
||||
"name": "open-mistral-7b",
|
||||
"label": "Mistral 7B",
|
||||
@@ -1378,6 +1379,17 @@
|
||||
"contextWindowTokens": 262144,
|
||||
"maxOutputTokens": 262144,
|
||||
"modalities": ["image"]
|
||||
},
|
||||
{
|
||||
"name": "mistral-medium-latest",
|
||||
"label": "Mistral Medium (latest)",
|
||||
"modelFamily": "MISTRAL",
|
||||
"inputCostPerMillionTokens": 1.5,
|
||||
"outputCostPerMillionTokens": 7.5,
|
||||
"contextWindowTokens": 262144,
|
||||
"maxOutputTokens": 262144,
|
||||
"modalities": ["image"],
|
||||
"supportsReasoning": true
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1407,6 +1419,24 @@
|
||||
"maxOutputTokens": 4096,
|
||||
"modalities": ["image"]
|
||||
},
|
||||
{
|
||||
"name": "grok-4.3",
|
||||
"label": "Grok 4.3",
|
||||
"modelFamily": "GROK",
|
||||
"inputCostPerMillionTokens": 1.25,
|
||||
"outputCostPerMillionTokens": 2.5,
|
||||
"cachedInputCostPerMillionTokens": 0.2,
|
||||
"longContextCost": {
|
||||
"inputCostPerMillionTokens": 2.5,
|
||||
"outputCostPerMillionTokens": 5,
|
||||
"thresholdTokens": 200000,
|
||||
"cachedInputCostPerMillionTokens": 0.4
|
||||
},
|
||||
"contextWindowTokens": 1000000,
|
||||
"maxOutputTokens": 30000,
|
||||
"modalities": ["image"],
|
||||
"supportsReasoning": true
|
||||
},
|
||||
{
|
||||
"name": "grok-3-mini-fast",
|
||||
"label": "Grok 3 Mini Fast",
|
||||
|
||||
+6
-6
@@ -42,7 +42,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
handle: 'tim@apple.dev',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
@@ -50,7 +50,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
handle: 'jony@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.PHIL,
|
||||
@@ -58,7 +58,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
handle: 'phil@apple.dev',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
@@ -66,7 +66,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
handle: 'jane.austen@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.COMPANY_MAIN,
|
||||
@@ -74,7 +74,7 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
handle: 'company-main@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.TEAM_CALENDAR,
|
||||
@@ -82,6 +82,6 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
handle: 'team-calendar@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
+6
-6
@@ -59,7 +59,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
@@ -72,7 +72,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
handle: 'jony.ive@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
@@ -85,7 +85,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.PHIL,
|
||||
handle: 'phil.schiler@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
@@ -98,7 +98,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
@@ -111,7 +111,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, // Use TIM's connected account for shared inbox
|
||||
handle: 'support@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
@@ -124,7 +124,7 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM, // Use TIM's connected account for shared inbox
|
||||
handle: 'sales@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
isSyncEnabled: false,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user