## Summary Replace the single `metadataVersion` integer with per-entity-type **collection hashes** for granular metadata staleness detection. The backend already generates a UUID per flat entity map on each cache recompute (`crypto.randomUUID()` in `WorkspaceCacheService`); we now expose these via the minimal metadata endpoint and SSE events so the frontend can compare and know exactly which entity types are stale. ### Key changes **Backend:** - `WorkspaceCacheService.getCacheHashes()` — new public method that reads only `:hash` keys from Redis without fetching full data - `MinimalMetadataDTO` — added `collectionHashes: Record<string, string>` (JSON scalar mapping `AllMetadataName` → collection hash), removed `metadataVersion` - `MetadataEventDTO` — added optional `updatedCollectionHash` field to SSE events - `MetadataEventsToDbListener` — reads the collection hash for the affected entity type after cache invalidation and attaches it to the SSE event before publishing - `MinimalMetadataService` — no longer queries the workspace table; uses `getCacheHashes()` for all flat entity maps and maps cache keys to `AllMetadataName` locally **Frontend:** - `metadataCollectionHashesState` — new Jotai atom with `atomWithStorage` + `getOnInit: true` storing `Partial<Record<MetadataEntityKey, string>>` - `mapAllMetadataNameToEntityKey()` — explicit mapping from backend `AllMetadataName` to frontend `MetadataEntityKey` (23 entries) - `useLoadMinimalMetadata` — stores `collectionHashes` from server, computes `staleEntityKeys` by comparing local vs server hashes - `patchMetadataStoreFromSSEEvent()` — accepts optional `updatedCollectionHash` and updates `metadataCollectionHashesState` - All 11 SSE effect components — pass `eventDetail.updatedCollectionHash` through to the patch function - `useStaleMetadataEntities` — new hook returning entity keys missing from collection hashes (not yet loaded/synced) - `resetMetadataStore()` — also clears collection hashes - Deleted `metadataVersionState` (superseded by collection hashes) ### Design decisions - **No change to hash generation** — existing `crypto.randomUUID()` is sufficient. Hashes are persisted in Redis, survive server restarts, and change only on `invalidateAndRecompute`. - **"Collection hash" naming** — used consistently to clarify the hash represents an entire entity collection (e.g., all views), not a single record. - **Mapping localized** — backend `WorkspaceCacheKeyName` → `AllMetadataName` mapping lives in the minimal metadata service. Frontend `AllMetadataName` → `MetadataEntityKey` mapping lives in a local utility. Nothing in `twenty-shared`. - **Backward compatible** — `collectionHashes` is additive; `updatedCollectionHash` is nullable.
95 lines
2.6 KiB
JavaScript
95 lines
2.6 KiB
JavaScript
import { readFileSync } from 'fs';
|
|
import { dirname, resolve } from 'path';
|
|
import { pathsToModuleNameMapper } from 'ts-jest';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
const tsConfigPath = resolve(__dirname, './tsconfig.json');
|
|
const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf8'));
|
|
|
|
// oxlint-disable-next-line no-undef
|
|
process.env.TZ = 'GMT';
|
|
// oxlint-disable-next-line no-undef
|
|
process.env.LC_ALL = 'en_US.UTF-8';
|
|
const jestConfig = {
|
|
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
|
|
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
|
|
prettierPath: null,
|
|
displayName: 'twenty-front',
|
|
preset: '../../jest.preset.js',
|
|
setupFilesAfterEnv: ['./setupTests.ts'],
|
|
testEnvironment: 'jsdom',
|
|
testEnvironmentOptions: {},
|
|
|
|
transformIgnorePatterns: [
|
|
'/node_modules/(?!(twenty-ui|apollo-upload-client|extract-files|is-plain-obj)/.*)',
|
|
'../../node_modules/(?!(twenty-ui|apollo-upload-client|extract-files|is-plain-obj)/.*)',
|
|
'../../twenty-ui/',
|
|
],
|
|
transform: {
|
|
'^.+\\.(ts|js|tsx|jsx|mjs)$': [
|
|
'@swc/jest',
|
|
{
|
|
jsc: {
|
|
parser: {
|
|
syntax: 'typescript',
|
|
tsx: true,
|
|
},
|
|
transform: {
|
|
react: {
|
|
runtime: 'automatic',
|
|
},
|
|
},
|
|
experimental: {
|
|
plugins: [['@lingui/swc-plugin', {}]],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
moduleNameMapper: {
|
|
'\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$':
|
|
'<rootDir>/__mocks__/imageMockFront.js',
|
|
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
|
|
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
|
|
prefix: '<rootDir>/',
|
|
}),
|
|
},
|
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
|
coverageThreshold: {
|
|
global: {
|
|
statements: 48.5,
|
|
lines: 47.0,
|
|
functions: 39.5,
|
|
},
|
|
},
|
|
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
|
coveragePathIgnorePatterns: [
|
|
'states/.+State.ts$',
|
|
'states/selectors/*',
|
|
'contexts/.+Context.ts',
|
|
'testing/*',
|
|
'tests/*',
|
|
'config/*',
|
|
'graphql/queries/*',
|
|
'graphql/mutations/*',
|
|
'graphql/subscriptions/*',
|
|
'graphql/fragments/*',
|
|
'types/*',
|
|
'constants/*',
|
|
'generated-metadata/*',
|
|
'generated/*',
|
|
'__stories__/*',
|
|
'display/icon/index.ts',
|
|
],
|
|
coverageDirectory: './coverage',
|
|
maxWorkers: 3,
|
|
workerIdleMemoryLimit: '512MB',
|
|
errorOnDeprecated: true,
|
|
};
|
|
|
|
export default jestConfig;
|