Skip to content

Twenty config core implementation #11595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 99 commits into from
Apr 26, 2025
Merged
Show file tree
Hide file tree
Changes from 72 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
e556261
change system_var to config_variable and user_var to user_variable an…
ehconitin Apr 9, 2025
3ad7cb2
add IS_CONFIG_VAR_IN_DB_ENABLED
ehconitin Apr 9, 2025
1f0a9a6
Merge remote-tracking branch 'upstream/main' into config-prep-2
ehconitin Apr 9, 2025
f61b830
lint
ehconitin Apr 9, 2025
b671d96
Merge remote-tracking branch 'upstream/main' into config-prep-2
ehconitin Apr 10, 2025
e58ef31
rename service spec
ehconitin Apr 10, 2025
31ed7e7
Copy over from POC
ehconitin Apr 10, 2025
46ba298
continue
ehconitin Apr 10, 2025
d5c6038
twenty config module is global?
ehconitin Apr 10, 2025
eeb7448
wip: const renaming
ehconitin Apr 10, 2025
07744af
hasty commit, needs rework :)
ehconitin Apr 10, 2025
448cf9a
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 10, 2025
2c9748f
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 11, 2025
7c072cb
refactor
ehconitin Apr 12, 2025
8a1bee7
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 12, 2025
3a4c317
grep review
ehconitin Apr 15, 2025
1ed5e8f
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 15, 2025
1eece63
dont need this overenginnered code, max entries would always be same …
ehconitin Apr 15, 2025
be2bf64
WIP: tests - add cache test
ehconitin Apr 15, 2025
071b147
WIP: tests -- add tests for drivers
ehconitin Apr 15, 2025
f0fe2d9
WIP: tests -- add test for storage service
ehconitin Apr 15, 2025
5b917f5
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 15, 2025
c52cecf
fix test
ehconitin Apr 15, 2025
3f9f84d
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 15, 2025
da53140
Merge remote-tracking branch 'upstream/main' into config-prep-2
ehconitin Apr 16, 2025
5f1f1aa
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 16, 2025
6646b76
Merge remote-tracking branch 'upstream/config-prep-2' into twenty-con…
ehconitin Apr 16, 2025
0f8012b
WIP: tests -- fix config cache test
ehconitin Apr 16, 2025
337ff08
WIP: tests -- add more test cases in db config driver
ehconitin Apr 16, 2025
b0c8021
WIP: tests -- fix incorrect env driver test
ehconitin Apr 16, 2025
0c79d81
WIP: tests -- fix test
ehconitin Apr 16, 2025
4f96a72
change IS_CONFIG_VAR_IN_DB_ENABLED to IS_CONFIG_VARIABLES_IN_DB_ENABLED
ehconitin Apr 16, 2025
df938fa
lint
ehconitin Apr 16, 2025
af7fe90
Merge remote-tracking branch 'upstream/config-prep-2' into twenty-con…
ehconitin Apr 16, 2025
e0bd022
lint
ehconitin Apr 16, 2025
2462f0e
WIP: tests -- scavenging test cases
ehconitin Apr 16, 2025
b259e41
fix
ehconitin Apr 16, 2025
1b9d524
WIP: continue
ehconitin Apr 16, 2025
62c93a6
fix
ehconitin Apr 16, 2025
9b58af4
materialize config source into a enum
ehconitin Apr 16, 2025
44b1a00
small improvements
ehconitin Apr 16, 2025
fac8f30
rename
ehconitin Apr 16, 2025
c3b61e9
fix
ehconitin Apr 16, 2025
2182804
cleaning
ehconitin Apr 16, 2025
cd5fc92
Merge branch 'main' into config-prep-2
ehconitin Apr 16, 2025
2a11b8d
Merge remote-tracking branch 'upstream/config-prep-2' into twenty-con…
ehconitin Apr 16, 2025
ee2131b
fix initialization bug
ehconitin Apr 16, 2025
a409d21
add service test
ehconitin Apr 16, 2025
51fdd5c
grep review
ehconitin Apr 16, 2025
0ca0bf1
rename
ehconitin Apr 16, 2025
51811b6
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 16, 2025
b29a0f8
Merge branch 'main' into twenty-config-core-implementation
charlesBochet Apr 16, 2025
427fbcb
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 17, 2025
49b532c
rethink conversion
ehconitin Apr 17, 2025
3aadb2e
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 18, 2025
ebc03b1
improvements
ehconitin Apr 18, 2025
9650281
automate simple validation and transformers
ehconitin Apr 21, 2025
02e5ca1
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 21, 2025
afa4f73
WIP: use p retry instead of custom retry mechanism
ehconitin Apr 21, 2025
1504a20
fix tests
ehconitin Apr 21, 2025
8ac0b15
hasty commit
ehconitin Apr 22, 2025
35535a8
remove p retry
ehconitin Apr 22, 2025
c1cf699
remove lib
ehconitin Apr 22, 2025
84776c0
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 22, 2025
5286dee
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 23, 2025
5ce55be
add @nestjs/schedule package
ehconitin Apr 23, 2025
edbe5e2
continue
ehconitin Apr 23, 2025
970fbbf
fix
ehconitin Apr 23, 2025
42d7061
imrove
ehconitin Apr 23, 2025
44373a9
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 23, 2025
2ffce8e
rename timestamp to registered at
ehconitin Apr 23, 2025
f0a06ba
lint
ehconitin Apr 23, 2025
ba2a645
add debounce mechanism to prevent duplicate config refreshes
ehconitin Apr 23, 2025
2d2de86
lint
ehconitin Apr 23, 2025
f58a3ae
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 23, 2025
c62f677
simplify
ehconitin Apr 24, 2025
61aae91
lint
ehconitin Apr 24, 2025
50c5565
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 24, 2025
0393cfc
refactor
ehconitin Apr 24, 2025
c8fdaca
converters improvements
ehconitin Apr 24, 2025
2af3020
add safe guards on conversion
ehconitin Apr 24, 2025
9f267e7
add tests and more improvements
ehconitin Apr 24, 2025
883f50a
lint
ehconitin Apr 24, 2025
8fef8d1
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 24, 2025
04d8b68
Merge branch 'main' into twenty-config-core-implementation
FelixMalfait Apr 25, 2025
275918f
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 25, 2025
a273216
Refactor config system: Eliminate custom state management for NestJS …
ehconitin Apr 25, 2025
df9a755
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 25, 2025
496b4f3
rename lingo
ehconitin Apr 25, 2025
79c8e3a
continue
ehconitin Apr 25, 2025
44660cb
grep
ehconitin Apr 25, 2025
a887bf8
lint
ehconitin Apr 25, 2025
7ff696a
last one I promise
ehconitin Apr 25, 2025
af26e74
Fix one tesdt
FelixMalfait Apr 26, 2025
f3c1690
fix
ehconitin Apr 26, 2025
f1c112b
Merge remote-tracking branch 'upstream/main' into twenty-config-core-…
ehconitin Apr 26, 2025
e2344f0
fix test
ehconitin Apr 26, 2025
649a68e
lint
ehconitin Apr 26, 2025
b37d278
fix test
ehconitin Apr 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/twenty-server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ FRONTEND_URL=http://localhost:3001
# CLOUDFLARE_WEBHOOK_SECRET=
# IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
# ANALYTICS_ENABLED=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: ANALYTICS_ENABLED is empty but should have a default boolean value (true/false)

# CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
# CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
1 change: 1 addition & 0 deletions packages/twenty-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"@nestjs/cache-manager": "^2.2.1",
"@nestjs/devtools-integration": "^0.1.6",
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
"@nestjs/schedule": "^3.0.0",
"@node-saml/passport-saml": "^5.0.0",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.200.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import { Module } from '@nestjs/common';
import { FileUploadResolver } from 'src/engine/core-modules/file/file-upload/resolvers/file-upload.resolver';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileModule } from 'src/engine/core-modules/file/file.module';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';

@Module({
imports: [FileModule],
providers: [FileUploadService, FileUploadResolver, TwentyConfigService],
providers: [FileUploadService, FileUploadResolver],
exports: [FileUploadService, FileUploadResolver],
})
export class FileUploadModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { FileWorkspaceFolderDeletionJob } from 'src/engine/core-modules/file/job
import { FileAttachmentListener } from 'src/engine/core-modules/file/listeners/file-attachment.listener';
import { FileWorkspaceMemberListener } from 'src/engine/core-modules/file/listeners/file-workspace-member.listener';
import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';

import { FileController } from './controllers/file.controller';
import { FileService } from './services/file.service';
Expand All @@ -15,7 +14,6 @@ import { FileService } from './services/file.service';
imports: [JwtModule],
providers: [
FileService,
TwentyConfigService,
FilePathGuard,
FileAttachmentListener,
FileWorkspaceMemberListener,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import { ScheduleModule } from '@nestjs/schedule';
import { Test, TestingModule } from '@nestjs/testing';

import { ConfigCacheService } from 'src/engine/core-modules/twenty-config/cache/config-cache.service';
import { ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
import { CONFIG_VARIABLES_CACHE_TTL } from 'src/engine/core-modules/twenty-config/constants/config-variables-cache-ttl';

describe('ConfigCacheService', () => {
let service: ConfigCacheService;

const withMockedDate = (timeOffset: number, callback: () => void) => {
const originalNow = Date.now;

try {
Date.now = jest.fn(() => originalNow() + timeOffset);
callback();
} finally {
Date.now = originalNow;
}
};

beforeEach(async () => {
jest.useFakeTimers();

const module: TestingModule = await Test.createTestingModule({
imports: [ScheduleModule.forRoot()],
providers: [ConfigCacheService],
}).compile();

service = module.get<ConfigCacheService>(ConfigCacheService);
});

afterEach(() => {
service.onModuleDestroy();
jest.useRealTimers();
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('get and set', () => {
it('should set and get a value from cache', () => {
const key = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const value = true;

service.set(key, value);
const result = service.get(key);

expect(result.value).toBe(value);
expect(result.isStale).toBe(false);
});

it('should return undefined for non-existent key', () => {
const result = service.get(
'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables,
);

expect(result.value).toBeUndefined();
expect(result.isStale).toBe(false);
});

it('should handle different value types', () => {
const booleanKey = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const stringKey = 'EMAIL_FROM_ADDRESS' as keyof ConfigVariables;
const numberKey = 'NODE_PORT' as keyof ConfigVariables;

service.set(booleanKey, true);
service.set(stringKey, 'test@example.com');
service.set(numberKey, 3000);

expect(service.get(booleanKey).value).toBe(true);
expect(service.get(stringKey).value).toBe('test@example.com');
expect(service.get(numberKey).value).toBe(3000);
});
});

describe('negative lookup cache', () => {
it('should check if a negative cache entry exists', () => {
const key = 'TEST_KEY' as keyof ConfigVariables;

service.markKeyAsMissing(key);
const result = service.isKeyKnownMissing(key);

expect(result).toBe(true);
});

it('should return false for negative cache entry check when not in cache', () => {
const key = 'NON_EXISTENT_KEY' as keyof ConfigVariables;

const result = service.isKeyKnownMissing(key);

expect(result).toBe(false);
});

it('should return false for negative cache entry check when expired', () => {
const key = 'TEST_KEY' as keyof ConfigVariables;

service.markKeyAsMissing(key);

// Mock a date beyond the TTL
jest.spyOn(Date, 'now').mockReturnValueOnce(Date.now() + 1000000);

expect(service.isKeyKnownMissing(key)).toBe(false);
});
});

describe('clear operations', () => {
it('should clear specific key', () => {
const key1 = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const key2 = 'EMAIL_FROM_ADDRESS' as keyof ConfigVariables;

service.set(key1, true);
service.set(key2, 'test@example.com');
service.clear(key1);

expect(service.get(key1).value).toBeUndefined();
expect(service.get(key2).value).toBe('test@example.com');
});

it('should clear all entries', () => {
const key1 = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const key2 = 'EMAIL_FROM_ADDRESS' as keyof ConfigVariables;

service.set(key1, true);
service.set(key2, 'test@example.com');
service.clearAll();

expect(service.get(key1).value).toBeUndefined();
expect(service.get(key2).value).toBeUndefined();
});
});

describe('cache expiration', () => {
it('should mark entries as stale after TTL', () => {
const key = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const value = true;

service.set(key, value);

withMockedDate(CONFIG_VARIABLES_CACHE_TTL + 1, () => {
const result = service.get(key);

expect(result.value).toBe(value);
expect(result.isStale).toBe(true);
});
});

it('should not mark entries as stale before TTL', () => {
const key = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const value = true;

service.set(key, value);

withMockedDate(CONFIG_VARIABLES_CACHE_TTL - 1, () => {
const result = service.get(key);

expect(result.value).toBe(value);
expect(result.isStale).toBe(false);
});
});
});

describe('getCacheInfo', () => {
it('should return correct cache information', () => {
const key1 = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;
const key2 = 'EMAIL_FROM_ADDRESS' as keyof ConfigVariables;
const key3 = 'NODE_PORT' as keyof ConfigVariables;

service.set(key1, true);
service.set(key2, 'test@example.com');
service.markKeyAsMissing(key3);

const info = service.getCacheInfo();

expect(info.positiveEntries).toBe(2);
expect(info.negativeEntries).toBe(1);
expect(info.cacheKeys).toContain(key1);
expect(info.cacheKeys).toContain(key2);
expect(info.cacheKeys).not.toContain(key3);
expect(service.isKeyKnownMissing(key3)).toBe(true);
});

it('should not include expired entries in cache info', () => {
const key = 'AUTH_PASSWORD_ENABLED' as keyof ConfigVariables;

service.set(key, true);

withMockedDate(CONFIG_VARIABLES_CACHE_TTL + 1, () => {
const info = service.getCacheInfo();

expect(info.positiveEntries).toBe(0);
expect(info.negativeEntries).toBe(0);
expect(info.cacheKeys).toHaveLength(0);
});
});

it('should properly count cache entries', () => {
const key1 = 'KEY1' as keyof ConfigVariables;
const key2 = 'KEY2' as keyof ConfigVariables;
const key3 = 'KEY3' as keyof ConfigVariables;

// Add some values to the cache
service.set(key1, 'value1');
service.set(key2, 'value2');
service.markKeyAsMissing(key3);

const cacheInfo = service.getCacheInfo();

expect(cacheInfo.positiveEntries).toBe(2);
expect(cacheInfo.negativeEntries).toBe(1);
expect(cacheInfo.cacheKeys).toContain(key1);
expect(cacheInfo.cacheKeys).toContain(key2);
expect(service.isKeyKnownMissing(key3)).toBe(true);
});
});

describe('module lifecycle', () => {
it('should clear cache on module destroy', () => {
const key = 'TEST_KEY' as keyof ConfigVariables;

service.set(key, 'test');

service.onModuleDestroy();

expect(service.get(key).value).toBeUndefined();
});
});

describe('getExpiredKeys', () => {
it('should return expired keys from both positive and negative caches', () => {
const expiredKey1 = 'EXPIRED_KEY1' as keyof ConfigVariables;
const expiredKey2 = 'EXPIRED_KEY2' as keyof ConfigVariables;
const expiredNegativeKey =
'EXPIRED_NEGATIVE_KEY' as keyof ConfigVariables;

// Set up keys that will expire
service.set(expiredKey1, 'value1');
service.set(expiredKey2, 'value2');
service.markKeyAsMissing(expiredNegativeKey);

// Make the above keys expire
withMockedDate(CONFIG_VARIABLES_CACHE_TTL + 1, () => {
// Add a fresh key after the time change
const freshKey = 'FRESH_KEY' as keyof ConfigVariables;

service.set(freshKey, 'value3');

const expiredKeys = service.getExpiredKeys();

expect(expiredKeys).toContain(expiredKey1);
expect(expiredKeys).toContain(expiredKey2);
expect(expiredKeys).toContain(expiredNegativeKey);
expect(expiredKeys).not.toContain(freshKey);
});
});

it('should return empty array when no keys are expired', () => {
const key1 = 'KEY1' as keyof ConfigVariables;
const key2 = 'KEY2' as keyof ConfigVariables;
const negativeKey = 'NEGATIVE_KEY' as keyof ConfigVariables;

service.set(key1, 'value1');
service.set(key2, 'value2');
service.markKeyAsMissing(negativeKey);

const expiredKeys = service.getExpiredKeys();

expect(expiredKeys).toHaveLength(0);
});

it('should not have duplicates if a key is in both caches', () => {
const key = 'DUPLICATE_KEY' as keyof ConfigVariables;

// Manually manipulate the caches to simulate a key in both caches
service.set(key, 'value');
service.markKeyAsMissing(key);

withMockedDate(CONFIG_VARIABLES_CACHE_TTL + 1, () => {
const expiredKeys = service.getExpiredKeys();

// Should only appear once in the result
expect(expiredKeys.filter((k) => k === key)).toHaveLength(1);
});
});
});
});
Loading
Loading