Upgrade to Node22 (#12488)

BlocknoteJS requires an ESM module where our server is CJS, this forced
us to pin the server-util version, which led us to force the resolution
of several packages, leading to bugs downstream.

From Node 22.12 Node supports requiring ESM modules (available from Node
22.0 with a flag). So I upgrade the module.
I picked Node 22 and not Node 23 or Node 24 because 22 is the LTS and we
don't plan to change node versions frequently.

If you remain on Node 18, things should still mostly work, except if you
edit a Rich Text field.

I also starting changing the default runtime for Serverless Functions
which isn't directly related. This means new serverless functions will
be created on Node 22, but we will still need another PR to migrate
existing serverless functions before September (end of support by AWS).

(In this PR I also remove the upgrade commands from 0.43 since they rely
on Blocknote and I didn't want to have to deal with this)

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Félix Malfait
2025-06-06 18:35:30 +02:00
committed by GitHub
parent 0188b66280
commit 322c8a1852
39 changed files with 1882 additions and 3146 deletions

View File

@ -2,7 +2,7 @@ name: Yarn Install
inputs: inputs:
node-version: node-version:
required: false required: false
default: '18' default: '22'
runs: runs:
using: "composite" using: "composite"

2
.nvmrc
View File

@ -1 +1 @@
18.17.1 22.12.0

2
.vscode/launch.json vendored
View File

@ -37,7 +37,7 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"runtimeExecutable": "npx", "runtimeExecutable": "npx",
"runtimeVersion": "18", "runtimeVersion": "^22.12.0",
"runtimeArgs": [ "runtimeArgs": [
"nx", "nx",
"run", "run",

View File

@ -43,12 +43,11 @@
], ],
"typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifier": "non-relative",
"search.exclude": { "search.exclude": {
"**/.yarn": true, "**/.yarn": true
}, },
"eslint.debug": true, "eslint.debug": true,
"files.associations": { "files.associations": {
".cursorrules": "markdown" ".cursorrules": "markdown"
}, },
"jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}" "jestrunner.codeLensSelector": "**/*.{test,spec,integration-spec}.{js,jsx,ts,tsx}"
}
} }

View File

@ -3,13 +3,10 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.7.17", "@apollo/client": "^3.7.17",
"@apollo/server": "^4.7.3", "@apollo/server": "^4.7.3",
"@aws-sdk/client-lambda": "^3.614.0", "@aws-sdk/client-lambda": "^3.700.0",
"@aws-sdk/client-s3": "^3.363.0", "@aws-sdk/client-s3": "^3.700.0",
"@aws-sdk/client-sts": "^3.744.0", "@aws-sdk/client-sts": "^3.700.0",
"@aws-sdk/credential-providers": "^3.363.0", "@aws-sdk/credential-providers": "^3.700.0",
"@blocknote/mantine": "^0.31.1",
"@blocknote/react": "^0.31.1",
"@blocknote/server-util": "^0.17.1",
"@codesandbox/sandpack-react": "^2.13.5", "@codesandbox/sandpack-react": "^2.13.5",
"@dagrejs/dagre": "^1.1.2", "@dagrejs/dagre": "^1.1.2",
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
@ -70,7 +67,6 @@
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.6.2", "axios": "^1.6.2",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"better-sqlite3": "^9.2.2",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"bullmq": "^5.40.0", "bullmq": "^5.40.0",
"bytes": "^3.1.2", "bytes": "^3.1.2",
@ -242,12 +238,11 @@
"@swc/cli": "^0.3.12", "@swc/cli": "^0.3.12",
"@swc/core": "1.7.42", "@swc/core": "1.7.42",
"@swc/helpers": "~0.5.2", "@swc/helpers": "~0.5.2",
"@testing-library/jest-dom": "^6.1.5", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "14.0.0", "@testing-library/react": "^16.3.0",
"@types/addressparser": "^1.0.3", "@types/addressparser": "^1.0.3",
"@types/apollo-upload-client": "^17.0.2", "@types/apollo-upload-client": "^17.0.2",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/better-sqlite3": "^7.6.8",
"@types/bytes": "^3.1.1", "@types/bytes": "^3.1.1",
"@types/chrome": "^0.0.267", "@types/chrome": "^0.0.267",
"@types/deep-equal": "^1.0.1", "@types/deep-equal": "^1.0.1",
@ -274,7 +269,7 @@
"@types/lodash.upperfirst": "^4.3.7", "@types/lodash.upperfirst": "^4.3.7",
"@types/luxon": "^3.3.0", "@types/luxon": "^3.3.0",
"@types/ms": "^0.7.31", "@types/ms": "^0.7.31",
"@types/node": "18.19.26", "@types/node": "^22.0.0",
"@types/passport-google-oauth20": "^2.0.11", "@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8", "@types/passport-jwt": "^3.0.8",
"@types/pluralize": "^0.0.33", "@types/pluralize": "^0.0.33",
@ -316,7 +311,7 @@
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.0.0",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"jest": "29.7.0", "jest": "29.7.0",
"jest-environment-jsdom": "29.7.0", "jest-environment-jsdom": "30.0.0-beta.3",
"jest-environment-node": "^29.4.1", "jest-environment-node": "^29.4.1",
"jest-fetch-mock": "^3.0.3", "jest-fetch-mock": "^3.0.3",
"jsdom": "~22.1.0", "jsdom": "~22.1.0",
@ -340,12 +335,13 @@
"tsx": "^4.17.0", "tsx": "^4.17.0",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-checker": "^0.6.2", "vite-plugin-checker": "^0.6.2",
"vite-plugin-cjs-interop": "^2.2.0",
"vite-plugin-dts": "3.8.1", "vite-plugin-dts": "3.8.1",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vitest": "1.4.0" "vitest": "1.4.0"
}, },
"engines": { "engines": {
"node": "^18.17.1", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
"yarn": ">=4.0.2" "yarn": ">=4.0.2"
}, },
@ -356,9 +352,8 @@
"graphql": "16.8.0", "graphql": "16.8.0",
"type-fest": "4.10.1", "type-fest": "4.10.1",
"typescript": "5.3.3", "typescript": "5.3.3",
"prosemirror-model": "1.23.0", "graphql-redis-subscriptions/ioredis": "^5.6.0",
"yjs": "13.6.18", "prosemirror-view": "1.40.0"
"graphql-redis-subscriptions/ioredis": "^5.6.0"
}, },
"version": "0.2.1", "version": "0.2.1",
"nx": {}, "nx": {},

View File

@ -7,7 +7,6 @@ TAG=latest
#REDIS_URL=redis://redis:6379 #REDIS_URL=redis://redis:6379
SERVER_URL=http://localhost:3000 SERVER_URL=http://localhost:3000
SIGN_IN_PREFILLED=false
# Use openssl rand -base64 32 for each secret # Use openssl rand -base64 32 for each secret
# APP_SECRET=replace_me_with_a_random_string # APP_SECRET=replace_me_with_a_random_string

View File

@ -1,4 +1,4 @@
FROM node:18.17.1-alpine as twenty-website-build FROM node:22-alpine as twenty-website-build
WORKDIR /app WORKDIR /app
@ -23,7 +23,7 @@ COPY ./packages/twenty-ui /app/packages/twenty-ui
COPY ./packages/twenty-website /app/packages/twenty-website COPY ./packages/twenty-website /app/packages/twenty-website
RUN npx nx build twenty-website RUN npx nx build twenty-website
FROM node:18.17.1-alpine as twenty-website FROM node:22-alpine as twenty-website
WORKDIR /app/packages/twenty-website WORKDIR /app/packages/twenty-website

View File

@ -1,5 +1,5 @@
# Base image for common dependencies # Base image for common dependencies
FROM node:18.17.1-alpine as common-deps FROM node:22-alpine as common-deps
WORKDIR /app WORKDIR /app
@ -49,7 +49,7 @@ RUN npx nx build twenty-front
# Final stage: Run the application # Final stage: Run the application
FROM node:18.17.1-alpine as twenty FROM node:22-alpine as twenty
# Used to run healthcheck in docker # Used to run healthcheck in docker
RUN apk add --no-cache curl jq RUN apk add --no-cache curl jq

View File

@ -27,13 +27,13 @@
}, },
"exports": { "exports": {
".": { ".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs", "import": "./dist/index.mjs",
"require": "./dist/index.js", "require": "./dist/index.js"
"types": "./dist/index.d.ts"
} }
}, },
"engines": { "engines": {
"node": "^18.17.1", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
"yarn": "^4.0.2" "yarn": "^4.0.2"
} }

View File

@ -3,13 +3,13 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh", "build": "NODE_ENV=production VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
"build:sourcemaps": "VITE_BUILD_SOURCEMAP=true VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh", "build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build && sh ./scripts/inject-runtime-env.sh",
"start:prod": "NODE_ENV=production npx serve -s build", "start:prod": "NODE_ENV=production npx serve -s build",
"tsup": "npx tsup" "tsup": "npx tsup"
}, },
"engines": { "engines": {
"node": "^18.17.1", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
"yarn": "^4.0.2" "yarn": "^4.0.2"
}, },
@ -29,6 +29,8 @@
"workerDirectory": "public" "workerDirectory": "public"
}, },
"dependencies": { "dependencies": {
"@blocknote/mantine": "^0.31.1",
"@blocknote/react": "^0.31.1",
"@blocknote/xl-ai": "^0.31.1", "@blocknote/xl-ai": "^0.31.1",
"@blocknote/xl-docx-exporter": "^0.31.1", "@blocknote/xl-docx-exporter": "^0.31.1",
"@blocknote/xl-pdf-exporter": "^0.31.1", "@blocknote/xl-pdf-exporter": "^0.31.1",

View File

@ -1,4 +1,4 @@
import { FlagComponent } from 'country-flag-icons/react/3x2'; import type { FlagComponent } from 'country-flag-icons/react/3x2';
import type { CountryCallingCode, CountryCode } from 'libphonenumber-js'; import type { CountryCallingCode, CountryCode } from 'libphonenumber-js';
export type Country = { export type Country = {

View File

@ -1,6 +1,7 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/test'; import { within } from '@storybook/test';
import { HttpResponse, graphql, http } from 'msw'; import { HttpResponse, graphql, http } from 'msw';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail'; import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail';
import { import {
@ -9,7 +10,6 @@ import {
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { sleep } from '~/utils/sleep'; import { sleep } from '~/utils/sleep';
import { getImageAbsoluteURI } from 'twenty-shared/utils';
const SOURCE_CODE_FULL_PATH = const SOURCE_CODE_FULL_PATH =
'serverless-function/20202020-1c25-4d02-bf25-6aeccf7ea419/adb4bd21-7670-4c81-9f74-1fc196fe87ea/source.ts'; 'serverless-function/20202020-1c25-4d02-bf25-6aeccf7ea419/adb4bd21-7670-4c81-9f74-1fc196fe87ea/source.ts';
@ -36,7 +36,7 @@ const meta: Meta<PageDecoratorArgs> = {
id: 'adb4bd21-7670-4c81-9f74-1fc196fe87ea', id: 'adb4bd21-7670-4c81-9f74-1fc196fe87ea',
name: 'Serverless Function Name', name: 'Serverless Function Name',
description: '', description: '',
runtime: 'nodejs18.x', runtime: 'nodejs22.x',
updatedAt: '2024-02-24T10:23:10.673Z', updatedAt: '2024-02-24T10:23:10.673Z',
createdAt: '2024-02-24T10:23:10.673Z', createdAt: '2024-02-24T10:23:10.673Z',
}, },

View File

@ -0,0 +1,76 @@
import { MutableRefObject } from 'react';
import { combineRefs } from '../combineRefs';
describe('combineRefs', () => {
it('should handle function refs', () => {
const ref1 = jest.fn();
const ref2 = jest.fn();
const node = document.createElement('div');
const combinedRef = combineRefs(ref1, ref2);
combinedRef(node);
expect(ref1).toHaveBeenCalledWith(node);
expect(ref2).toHaveBeenCalledWith(node);
});
it('should handle object refs', () => {
const ref1: MutableRefObject<HTMLDivElement | null> = { current: null };
const ref2: MutableRefObject<HTMLDivElement | null> = { current: null };
const node = document.createElement('div');
const combinedRef = combineRefs(ref1, ref2);
combinedRef(node);
expect(ref1.current).toBe(node);
expect(ref2.current).toBe(node);
});
it('should handle mixed function and object refs', () => {
const funcRef = jest.fn();
const objRef: MutableRefObject<HTMLDivElement | null> = { current: null };
const node = document.createElement('div');
const combinedRef = combineRefs(funcRef, objRef);
combinedRef(node);
expect(funcRef).toHaveBeenCalledWith(node);
expect(objRef.current).toBe(node);
});
it('should handle undefined refs', () => {
const ref1 = jest.fn();
const node = document.createElement('div');
const combinedRef = combineRefs(ref1, undefined);
combinedRef(node);
expect(ref1).toHaveBeenCalledWith(node);
});
it('should handle all undefined refs', () => {
const node = document.createElement('div');
const combinedRef = combineRefs(undefined, undefined);
expect(() => combinedRef(node)).not.toThrow();
});
it('should handle empty refs array', () => {
const node = document.createElement('div');
const combinedRef = combineRefs();
expect(() => combinedRef(node)).not.toThrow();
});
it('should handle null refs', () => {
const ref1 = jest.fn();
const node = document.createElement('div');
const combinedRef = combineRefs(ref1, null);
combinedRef(node);
expect(ref1).toHaveBeenCalledWith(node);
});
});

View File

@ -1,4 +1,4 @@
import { getDirtyFields } from './getDirtyFields'; import { getDirtyFields } from '~/utils/getDirtyFields';
describe('getDirtyFields', () => { describe('getDirtyFields', () => {
it('should return all defined fields from draft when persisted is null', () => { it('should return all defined fields from draft when persisted is null', () => {

View File

@ -0,0 +1,84 @@
import { isEmptyObject } from '../isEmptyObject';
describe('isEmptyObject', () => {
it('should return true for empty object', () => {
expect(isEmptyObject({})).toBe(true);
});
it('should return false for object with properties', () => {
expect(isEmptyObject({ key: 'value' })).toBe(false);
});
it('should return false for object with multiple properties', () => {
expect(isEmptyObject({ key1: 'value1', key2: 'value2' })).toBe(false);
});
it('should return false for null', () => {
expect(isEmptyObject(null)).toBe(false);
});
it('should return false for undefined', () => {
expect(isEmptyObject(undefined)).toBe(false);
});
it('should return false for string', () => {
expect(isEmptyObject('test')).toBe(false);
});
it('should return false for number', () => {
expect(isEmptyObject(42)).toBe(false);
});
it('should return false for boolean', () => {
expect(isEmptyObject(true)).toBe(false);
expect(isEmptyObject(false)).toBe(false);
});
it('should return true for empty array (as it has no enumerable keys)', () => {
expect(isEmptyObject([])).toBe(true);
});
it('should return false for non-empty array with enumerable properties', () => {
const arr = [1, 2, 3];
expect(isEmptyObject(arr)).toBe(false);
});
it('should return false for function', () => {
expect(isEmptyObject(() => {})).toBe(false);
});
it('should return true for Date object (as it has no enumerable keys)', () => {
expect(isEmptyObject(new Date())).toBe(true);
});
it('should return true for object created with Object.create(null)', () => {
expect(isEmptyObject(Object.create(null))).toBe(true);
});
it('should return true for object with inherited properties only', () => {
const parent = { parentProp: 'value' };
const child = Object.create(parent);
expect(isEmptyObject(child)).toBe(true); // Only checks own enumerable properties
});
it('should return true for object with non-enumerable properties only', () => {
const obj = {};
Object.defineProperty(obj, 'nonEnumerableProp', {
value: 'test',
enumerable: false,
});
expect(isEmptyObject(obj)).toBe(true); // Object.keys only returns enumerable properties
});
it('should return false for array with custom properties', () => {
const arr: any = [];
arr.customProp = 'value';
expect(isEmptyObject(arr)).toBe(false);
});
it('should return false for Date object with custom properties', () => {
const date: any = new Date();
date.customProp = 'value';
expect(isEmptyObject(date)).toBe(false);
});
});

View File

@ -0,0 +1,12 @@
import { isInFrame } from '../isInIframe';
describe('isInFrame', () => {
it('should return a boolean value', () => {
const result = isInFrame();
expect(typeof result).toBe('boolean');
});
it('should not throw an error when called', () => {
expect(() => isInFrame()).not.toThrow();
});
});

View File

@ -0,0 +1,51 @@
import { logDebug } from '../logDebug';
describe('logDebug', () => {
let consoleDebugSpy: jest.SpyInstance;
beforeEach(() => {
consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation(() => {});
});
afterEach(() => {
consoleDebugSpy.mockRestore();
});
it('should call console.debug with the provided message', () => {
const debugMessage = 'Test debug message';
logDebug(debugMessage);
expect(consoleDebugSpy).toHaveBeenCalledWith(debugMessage, []);
expect(consoleDebugSpy).toHaveBeenCalledTimes(1);
});
it('should handle optional parameters', () => {
const debugMessage = 'Test debug message';
const param1 = 'param1';
const param2 = { key: 'value' };
logDebug(debugMessage, param1, param2);
expect(consoleDebugSpy).toHaveBeenCalledWith(debugMessage, [
param1,
param2,
]);
});
it('should handle no optional parameters', () => {
const debugMessage = 'Test debug message';
logDebug(debugMessage);
expect(consoleDebugSpy).toHaveBeenCalledWith(debugMessage, []);
});
it('should handle null and undefined messages', () => {
logDebug(null);
expect(consoleDebugSpy).toHaveBeenCalledWith(null, []);
logDebug(undefined);
expect(consoleDebugSpy).toHaveBeenCalledWith(undefined, []);
});
});

View File

@ -0,0 +1,46 @@
import { logError } from '../logError';
describe('logError', () => {
let consoleErrorSpy: jest.SpyInstance;
beforeEach(() => {
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
it('should call console.error with the provided message', () => {
const errorMessage = 'Test error message';
logError(errorMessage);
expect(consoleErrorSpy).toHaveBeenCalledWith(errorMessage);
expect(consoleErrorSpy).toHaveBeenCalledTimes(1);
});
it('should handle object messages', () => {
const errorObject = { error: 'Test error', code: 500 };
logError(errorObject);
expect(consoleErrorSpy).toHaveBeenCalledWith(errorObject);
});
it('should handle null and undefined messages', () => {
logError(null);
expect(consoleErrorSpy).toHaveBeenCalledWith(null);
logError(undefined);
expect(consoleErrorSpy).toHaveBeenCalledWith(undefined);
});
it('should handle Error objects', () => {
const error = new Error('Test error');
logError(error);
expect(consoleErrorSpy).toHaveBeenCalledWith(error);
});
});

View File

@ -42,8 +42,8 @@ export default defineConfig(({ command, mode }) => {
// Please don't increase this limit for main index chunk // Please don't increase this limit for main index chunk
// If it gets too big then find modules in the code base // If it gets too big then find modules in the code base
// that can be loaded lazily, there are more! // that can be loaded lazily, there are more!
const MAIN_CHUNK_SIZE_LIMIT = 4.1 * 1024 * 1024; // 4.1MB for main index chunk const MAIN_CHUNK_SIZE_LIMIT = 4.7 * 1024 * 1024; // 4.7MB for main index chunk
const OTHER_CHUNK_SIZE_LIMIT = 15 * 1024 * 1024; // 5MB for other chunks const OTHER_CHUNK_SIZE_LIMIT = 5 * 1024 * 1024; // 5MB for other chunks
const checkers: Checkers = { const checkers: Checkers = {
overlay: false, overlay: false,
@ -196,7 +196,7 @@ export default defineConfig(({ command, mode }) => {
const limitType = isMainChunk ? 'main' : 'other'; const limitType = isMainChunk ? 'main' : 'other';
if (size > sizeLimit) { if (size > sizeLimit) {
oversizedChunks.push(`${fileName} (${limitType}): ${(size / 1024 / 1024).toFixed(2)}MB (limit: ${(sizeLimit / 1024 / 1024).toFixed(0)}MB)`); oversizedChunks.push(`${fileName} (${limitType}): ${(size / 1024 / 1024).toFixed(2)}MB (limit: ${(sizeLimit / 1024 / 1024).toFixed(2)}MB)`);
} }
} }
}); });

View File

@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@ai-sdk/openai": "^1.3.22", "@ai-sdk/openai": "^1.3.22",
"@blocknote/server-util": "^0.31.1",
"@clickhouse/client": "^1.11.0", "@clickhouse/client": "^1.11.0",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2", "@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch", "@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga+nestjs+2.1.0.patch",
@ -89,7 +90,7 @@
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"engines": { "engines": {
"node": "^18.17.1", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
"yarn": "^4.0.2" "yarn": "^4.0.2"
} }

View File

@ -18,7 +18,7 @@
"options": { "options": {
"cwd": "packages/twenty-server", "cwd": "packages/twenty-server",
"commands": [ "commands": [
"NODE_ENV=test nx jest --config ./jest-integration.config.ts" "NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" nx jest --config ./jest-integration.config.ts"
] ]
}, },
"parallel": false, "parallel": false,
@ -26,7 +26,7 @@
"with-db-reset": { "with-db-reset": {
"cwd": "packages/twenty-server", "cwd": "packages/twenty-server",
"commands": [ "commands": [
"NODE_ENV=test nx database:reset > reset-logs.log && NODE_ENV=test nx jest --config ./jest-integration.config.ts" "NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" nx database:reset > reset-logs.log && NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" nx jest --config ./jest-integration.config.ts"
] ]
} }
} }

View File

@ -1,208 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import { v4 } from 'uuid';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me';
import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
@Command({
name: 'upgrade:0-43:add-tasks-assigned-to-me-view',
description: 'Add tasks assigned to me view',
})
export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
await this.createTasksAssignedToMeView(workspaceId);
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
}
private async createTasksAssignedToMeView(
workspaceId: string,
): Promise<void> {
const objectMetadata = await this.objectMetadataRepository.find({
where: { workspaceId },
relations: ['fields'],
});
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
// @ts-expect-error legacy noImplicitAny
acc[object.standardId ?? ''] = {
id: object.id,
fields: object.fields.reduce((acc, field) => {
// @ts-expect-error legacy noImplicitAny
acc[field.standardId ?? ''] = field.id;
return acc;
}, {}),
};
return acc;
}, {});
const taskObjectMetadata = objectMetadata.find(
(object) => object.standardId === STANDARD_OBJECT_IDS.task,
);
if (!taskObjectMetadata) {
throw new Error(`Task object not found for workspace ${workspaceId}`);
}
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
workspaceId,
'view',
);
const existingView = await viewRepository.findOne({
where: {
name: 'Assigned to Me',
objectMetadataId: taskObjectMetadata.id,
},
});
if (existingView) {
this.logger.log(
chalk.yellow(
`"Assigned to Me" view already exists for workspace ${workspaceId}`,
),
);
return;
}
const viewDefinition = tasksAssignedToMeView(objectMetadataMap);
const viewId = v4();
const insertedView = await viewRepository.save({
id: viewId,
name: viewDefinition.name,
objectMetadataId: viewDefinition.objectMetadataId,
type: viewDefinition.type,
position: viewDefinition.position,
icon: viewDefinition.icon,
kanbanFieldMetadataId: viewDefinition.kanbanFieldMetadataId,
});
if (viewDefinition.fields && viewDefinition.fields.length > 0) {
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
workspaceId,
'viewField',
);
const viewFields = viewDefinition.fields.map((field) => ({
fieldMetadataId: field.fieldMetadataId,
position: field.position,
isVisible: field.isVisible,
size: field.size,
viewId: insertedView.id,
}));
await viewFieldRepository.save(viewFields);
}
if (viewDefinition.filters && viewDefinition.filters.length > 0) {
const viewFilterRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
workspaceId,
'viewFilter',
);
const viewFilters = viewDefinition.filters.map((filter) => ({
fieldMetadataId: filter.fieldMetadataId,
displayValue: filter.displayValue,
operand: filter.operand,
value: filter.value,
viewId: insertedView.id,
}));
await viewFilterRepository.save(viewFilters);
}
await this.createTasksAssignedToMeViewGroups(workspaceId, insertedView.id);
}
private async createTasksAssignedToMeViewGroups(
workspaceId: string,
viewId: string,
) {
const taskStatusFieldMetadata = await this.fieldMetadataRepository.findOne({
where: {
workspaceId,
standardId: TASK_STANDARD_FIELD_IDS.status,
},
});
if (!taskStatusFieldMetadata) {
throw new Error(
`Task status field metadata not found for workspace ${workspaceId}`,
);
}
const optionValueViewGroups = taskStatusFieldMetadata.options.map(
(taskStatusOption: FieldMetadataDefaultOption, index) =>
({
fieldMetadataId: taskStatusFieldMetadata.id,
viewId,
fieldValue: taskStatusOption.value,
position: index,
}) satisfies Partial<ViewGroupWorkspaceEntity>,
);
const noValueViewGroup: Partial<ViewGroupWorkspaceEntity> = {
fieldMetadataId: taskStatusFieldMetadata.id,
viewId,
fieldValue: '',
position: optionValueViewGroups.length,
};
const viewGroups = [...optionValueViewGroups, noValueViewGroup];
const viewGroupRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
workspaceId,
'viewGroup',
);
await viewGroupRepository.insert(viewGroups);
}
}

View File

@ -1,51 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:0-43:migrate-is-searchable-for-custom-object-metadata',
description: 'Set isSearchable true for custom object metadata',
})
export class MigrateIsSearchableForCustomObjectMetadataCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
if (!options.dryRun) {
await this.objectMetadataRepository.update(
{
workspaceId,
isCustom: true,
},
{
isSearchable: true,
},
);
}
}
}

View File

@ -1,275 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { FieldMetadataType } from 'twenty-shared/types';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandOptions,
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { computeTableName } from 'src/engine/utils/compute-table-name.util';
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
type MigrateRichTextContentArgs = {
richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[];
workspaceId: string;
options: ActiveOrSuspendedWorkspacesMigrationCommandOptions;
};
type RichTextFieldsWithObjectMetadata = {
richTextField: FieldMetadataEntity;
objectMetadata: ObjectMetadataEntity | null;
};
type ProcessRichTextFieldsArgs = {
richTextFields: FieldMetadataEntity[];
workspaceId: string;
};
@Command({
name: 'upgrade:0-43:migrate-rich-text-content-patch',
description: 'Migrate RICH_TEXT content from v1 to v2',
})
export class MigrateRichTextContentPatchCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
options,
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running MigrateRichTextContentPatchCommand for workspace ${workspaceId} ${index + 1}/${total}`,
);
if (await this.hasRichTextV2FeatureFlag(workspaceId)) {
this.logger.log(
chalk.yellow(
'Rich text v2 feature flag is enabled, skipping migration',
),
);
return;
}
const richTextFields = await this.fieldMetadataRepository.find({
where: {
workspaceId,
type: FieldMetadataType.RICH_TEXT,
},
});
if (!richTextFields.length) {
this.logger.log(
chalk.yellow('No RICH_TEXT fields found in this workspace'),
);
return;
}
this.logger.log(`Found ${richTextFields.length} RICH_TEXT fields`);
const richTextFieldsWithObjectMetadata =
await this.getRichTextFieldsWithObjectMetadata({
richTextFields,
workspaceId,
});
await this.migrateToNewRichTextFieldsColumn({
richTextFieldsWithObjectMetadata,
workspaceId,
options,
});
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}`),
);
}
private async hasRichTextV2FeatureFlag(
workspaceId: string,
): Promise<boolean> {
return await this.featureFlagRepository.exists({
where: {
workspaceId,
key: 'IS_RICH_TEXT_V2_ENABLED' as FeatureFlagKey,
value: true,
},
});
}
private async getRichTextFieldsWithObjectMetadata({
richTextFields,
workspaceId,
}: ProcessRichTextFieldsArgs): Promise<RichTextFieldsWithObjectMetadata[]> {
const richTextFieldsWithObjectMetadata: RichTextFieldsWithObjectMetadata[] =
[];
for (const richTextField of richTextFields) {
const objectMetadata = await this.objectMetadataRepository.findOne({
where: { id: richTextField.objectMetadataId },
relations: {
fields: true,
},
});
if (objectMetadata === null) {
this.logger.log(
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
);
}
richTextFieldsWithObjectMetadata.push({
richTextField,
objectMetadata,
});
}
return richTextFieldsWithObjectMetadata;
}
private jsonParseOrSilentlyFail(input: string): null | unknown {
try {
return JSON.parse(input);
} catch (e) {
return null;
}
}
private async getMarkdownFieldValue({
blocknoteFieldValue,
serverBlockNoteEditor,
}: {
blocknoteFieldValue: string | null;
serverBlockNoteEditor: ServerBlockNoteEditor;
}): Promise<string | null> {
const blocknoteFieldValueIsDefined =
blocknoteFieldValue !== null &&
blocknoteFieldValue !== undefined &&
blocknoteFieldValue !== '{}';
if (!blocknoteFieldValueIsDefined) {
return null;
}
const jsonParsedblocknoteFieldValue =
this.jsonParseOrSilentlyFail(blocknoteFieldValue);
if (jsonParsedblocknoteFieldValue === null) {
return null;
}
if (!Array.isArray(jsonParsedblocknoteFieldValue)) {
this.logger.log(
`blocknoteFieldValue is defined and is not an array got ${blocknoteFieldValue}`,
);
return null;
}
let markdown: string | null = null;
try {
markdown = await serverBlockNoteEditor.blocksToMarkdownLossy(
jsonParsedblocknoteFieldValue,
);
} catch (error) {
this.logger.log(
`Error converting blocknote to markdown for ${blocknoteFieldValue}`,
);
}
return markdown;
}
private async migrateToNewRichTextFieldsColumn({
richTextFieldsWithObjectMetadata,
workspaceId,
options,
}: MigrateRichTextContentArgs) {
const serverBlockNoteEditor = ServerBlockNoteEditor.create();
for (const {
richTextField,
objectMetadata,
} of richTextFieldsWithObjectMetadata) {
if (objectMetadata === null) {
this.logger.log(
`Object metadata not found for rich text field ${richTextField.name} in workspace ${workspaceId}`,
);
continue;
}
const schemaName =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
shouldFailIfMetadataNotFound: false,
});
const rows = await workspaceDataSource.query(
`SELECT id, "${richTextField.name}" FROM "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" WHERE "${richTextField.name}" IS NOT NULL`,
undefined, // parameters
undefined, // queryRunner
{
shouldBypassPermissionChecks: true,
},
);
this.logger.log(`Generating markdown for ${rows.length} records`);
for (const row of rows) {
const blocknoteFieldValue = row[richTextField.name];
const markdownFieldValue = await this.getMarkdownFieldValue({
blocknoteFieldValue,
serverBlockNoteEditor,
});
if (!options.dryRun) {
try {
await workspaceDataSource.query(
`UPDATE "${schemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" SET "${richTextField.name}V2Blocknote" = $1, "${richTextField.name}V2Markdown" = $2 WHERE id = $3`,
[blocknoteFieldValue, markdownFieldValue, row.id],
undefined, // queryRunner
{
shouldBypassPermissionChecks: true,
},
);
} catch (error) {
this.logger.log(
chalk.red(
`Error updating rich text field ${richTextField.name} for record ${row.id} in workspace ${workspaceId}`,
),
);
}
}
}
}
}
}

View File

@ -1,96 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchVectorService } from 'src/engine/metadata-modules/search-vector/search-vector.service';
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import { SEARCH_FIELDS_FOR_NOTES } from 'src/modules/note/standard-objects/note.workspace-entity';
import { SEARCH_FIELDS_FOR_TASKS } from 'src/modules/task/standard-objects/task.workspace-entity';
@Command({
name: 'upgrade:0-43:migrate-search-vector-on-note-and-task-entities',
description: 'Migrate search vector on note and task entities',
})
export class MigrateSearchVectorOnNoteAndTaskEntitiesCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(FeatureFlag, 'core')
protected readonly featureFlagRepository: Repository<FeatureFlag>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly searchVectorService: SearchVectorService,
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const noteObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'note',
},
});
if (!options.dryRun) {
await this.searchVectorService.updateSearchVector(
noteObjectMetadata.id,
SEARCH_FIELDS_FOR_NOTES,
workspaceId,
);
}
const taskObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
select: ['id'],
where: {
workspaceId,
nameSingular: 'task',
},
});
if (!options.dryRun) {
await this.searchVectorService.updateSearchVector(
taskObjectMetadata.id,
SEARCH_FIELDS_FOR_TASKS,
workspaceId,
);
}
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
workspaceId,
);
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
this.logger.log(
`Migrated search vector on note and task entities for workspace ${workspaceId}`,
);
}
}

View File

@ -1,99 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import chalk from 'chalk';
import { Command } from 'nest-commander';
import { In, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ViewOpenRecordInType } from 'src/modules/view/standard-objects/view.workspace-entity';
@Command({
name: 'upgrade:0-43:update-default-view-record-opening-on-workflow-objects',
description:
'Update default view record opening on workflow objects to record page',
})
export class UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace, 'core')
protected readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
protected readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
index,
total,
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`Running command for workspace ${workspaceId} ${index + 1}/${total}`,
);
const workflowObjectsMetadata = await this.objectMetadataRepository.find({
select: ['id'],
where: {
workspaceId,
standardId: In([
STANDARD_OBJECT_IDS.workflow,
STANDARD_OBJECT_IDS.workflowVersion,
STANDARD_OBJECT_IDS.workflowRun,
]),
},
});
if (workflowObjectsMetadata.length === 0) {
this.logger.log(
chalk.yellow(`No workflow objects found for workspace ${workspaceId}`),
);
return;
}
if (!options.dryRun) {
await this.updateDefaultViewsRecordOpening(
workflowObjectsMetadata.map((metadata) => metadata.id),
workspaceId,
);
}
this.logger.log(
chalk.green(`Command completed for workspace ${workspaceId}.`),
);
}
private async updateDefaultViewsRecordOpening(
workflowObjectMetadataIds: string[],
workspaceId: string,
): Promise<void> {
const failOnMetadataCacheMiss = false;
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
'view',
{
shouldFailIfMetadataNotFound: failOnMetadataCacheMiss,
},
);
await viewRepository.update(
{
objectMetadataId: In(workflowObjectMetadataIds),
key: 'INDEX',
},
{
openRecordIn: ViewOpenRecordInType.RECORD_PAGE,
},
);
}
}

View File

@ -1,45 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SearchVectorModule } from 'src/engine/metadata-modules/search-vector/search-vector.module';
import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, FeatureFlag], 'core'),
TypeOrmModule.forFeature(
[FieldMetadataEntity, ObjectMetadataEntity],
'metadata',
),
WorkspaceDataSourceModule,
SearchVectorModule,
WorkspaceMigrationRunnerModule,
WorkspaceMetadataVersionModule,
],
providers: [
MigrateRichTextContentPatchCommand,
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
MigrateIsSearchableForCustomObjectMetadataCommand,
AddTasksAssignedToMeViewCommand,
],
exports: [
MigrateRichTextContentPatchCommand,
MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
MigrateIsSearchableForCustomObjectMetadataCommand,
AddTasksAssignedToMeViewCommand,
],
})
export class V0_43_UpgradeVersionCommandModule {}

View File

@ -1,7 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { V0_43_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-43/0-43-upgrade-version-command.module';
import { V0_44_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-44/0-44-upgrade-version-command.module'; import { V0_44_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-44/0-44-upgrade-version-command.module';
import { V0_50_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module'; import { V0_50_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-50/0-50-upgrade-version-command.module';
import { V0_51_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-51/0-51-upgrade-version-command.module'; import { V0_51_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-51/0-51-upgrade-version-command.module';
@ -19,7 +18,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace], 'core'),
V0_43_UpgradeVersionCommandModule,
V0_44_UpgradeVersionCommandModule, V0_44_UpgradeVersionCommandModule,
V0_50_UpgradeVersionCommandModule, V0_50_UpgradeVersionCommandModule,
V0_51_UpgradeVersionCommandModule, V0_51_UpgradeVersionCommandModule,

View File

@ -15,11 +15,6 @@ import {
UpgradeCommandRunner, UpgradeCommandRunner,
VersionCommands, VersionCommands,
} from 'src/database/commands/command-runners/upgrade.command-runner'; } from 'src/database/commands/command-runners/upgrade.command-runner';
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command'; import { InitializePermissionsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-initialize-permissions.command';
import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-update-view-aggregate-operations.command'; import { UpdateViewAggregateOperationsCommand } from 'src/database/commands/upgrade-version-command/0-44/0-44-update-view-aggregate-operations.command';
import { UpgradeCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command'; import { UpgradeCreatedByEnumCommand } from 'src/database/commands/upgrade-version-command/0-51/0-51-update-workflow-trigger-type-enum.command';
@ -152,13 +147,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
private readonly databaseMigrationService: DatabaseMigrationService, private readonly databaseMigrationService: DatabaseMigrationService,
// 0.43 Commands
protected readonly migrateRichTextContentPatchCommand: MigrateRichTextContentPatchCommand,
protected readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
protected readonly migrateIsSearchableForCustomObjectMetadataCommand: MigrateIsSearchableForCustomObjectMetadataCommand,
protected readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
protected readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
// 0.44 Commands // 0.44 Commands
protected readonly initializePermissionsCommand: InitializePermissionsCommand, protected readonly initializePermissionsCommand: InitializePermissionsCommand,
protected readonly updateViewAggregateOperationsCommand: UpdateViewAggregateOperationsCommand, protected readonly updateViewAggregateOperationsCommand: UpdateViewAggregateOperationsCommand,
@ -191,18 +179,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
syncWorkspaceMetadataCommand, syncWorkspaceMetadataCommand,
); );
const commands_043: VersionCommands = {
beforeSyncMetadata: [
this.migrateRichTextContentPatchCommand,
this.migrateIsSearchableForCustomObjectMetadataCommand,
this.migrateSearchVectorOnNoteAndTaskEntitiesCommand,
this.migrateIsSearchableForCustomObjectMetadataCommand,
],
afterSyncMetadata: [
this.updateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
this.addTasksAssignedToMeViewCommand,
],
};
const commands_044: VersionCommands = { const commands_044: VersionCommands = {
beforeSyncMetadata: [ beforeSyncMetadata: [
this.initializePermissionsCommand, this.initializePermissionsCommand,
@ -251,7 +227,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
}; };
this.allCommands = { this.allCommands = {
'0.43.0': commands_043,
'0.44.0': commands_044, '0.44.0': commands_044,
'0.50.0': commands_050, '0.50.0': commands_050,
'0.51.0': commands_051, '0.51.0': commands_051,

View File

@ -40,6 +40,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId, workspaceId: workspaceId,
value: false, value: false,
}, },
{
key: FeatureFlagKey.IS_AI_ENABLED,
workspaceId: workspaceId,
value: true,
},
]) ])
.execute(); .execute();
}; };

View File

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateServerlessFunctionDefaultRuntimeToNode221749205425841
implements MigrationInterface
{
name = 'UpdateServerlessFunctionDefaultRuntimeToNode221749205425841';
public async up(queryRunner: QueryRunner): Promise<void> {
// Update the default value for the runtime column to nodejs22.x
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" ALTER COLUMN "runtime" SET DEFAULT 'nodejs22.x'`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Revert the default value back to nodejs18.x
await queryRunner.query(
`ALTER TABLE "core"."serverlessFunction" ALTER COLUMN "runtime" SET DEFAULT 'nodejs18.x'`,
);
}
}

View File

@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ServerBlockNoteEditor } from '@blocknote/server-util';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -101,6 +100,8 @@ export class RecordInputTransformerService {
): Promise<RichTextV2Metadata> { ): Promise<RichTextV2Metadata> {
const parsedValue = richTextV2ValueSchema.parse(richTextValue); const parsedValue = richTextV2ValueSchema.parse(richTextValue);
const { ServerBlockNoteEditor } = await import('@blocknote/server-util');
const serverBlockNoteEditor = ServerBlockNoteEditor.create(); const serverBlockNoteEditor = ServerBlockNoteEditor.create();
// Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks) // Patch: Handle cases where blocknote to markdown conversion fails for certain block types (custom/code blocks)

View File

@ -1,10 +1,9 @@
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import ts, { transpileModule } from 'typescript';
import { import {
CreateFunctionCommandInput,
CreateFunctionCommand, CreateFunctionCommand,
CreateFunctionCommandInput,
DeleteFunctionCommand, DeleteFunctionCommand,
GetFunctionCommand, GetFunctionCommand,
InvokeCommand, InvokeCommand,
@ -13,14 +12,15 @@ import {
LambdaClientConfig, LambdaClientConfig,
ListLayerVersionsCommand, ListLayerVersionsCommand,
ListLayerVersionsCommandInput, ListLayerVersionsCommandInput,
LogType,
PublishLayerVersionCommand, PublishLayerVersionCommand,
PublishLayerVersionCommandInput, PublishLayerVersionCommandInput,
ResourceNotFoundException, ResourceNotFoundException,
waitUntilFunctionUpdatedV2, waitUntilFunctionUpdatedV2,
LogType,
} from '@aws-sdk/client-lambda'; } from '@aws-sdk/client-lambda';
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts'; import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import ts, { transpileModule } from 'typescript';
import { import {
ServerlessDriver, ServerlessDriver,
@ -28,8 +28,11 @@ import {
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name'; import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies'; import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
import { copyExecutor } from 'src/engine/core-modules/serverless/drivers/utils/copy-executor';
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file'; import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
import { import {
LambdaBuildDirectoryManager, LambdaBuildDirectoryManager,
@ -45,9 +48,6 @@ import {
ServerlessFunctionException, ServerlessFunctionException,
ServerlessFunctionExceptionCode, ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { copyExecutor } from 'src/engine/core-modules/serverless/drivers/utils/copy-executor';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60; const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60;
const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h
@ -172,7 +172,10 @@ export class LambdaDriver implements ServerlessDriver {
Content: { Content: {
ZipFile: await fs.readFile(lambdaZipPath), ZipFile: await fs.readFile(lambdaZipPath),
}, },
CompatibleRuntimes: [ServerlessFunctionRuntime.NODE18], CompatibleRuntimes: [
ServerlessFunctionRuntime.NODE18,
ServerlessFunctionRuntime.NODE22,
],
Description: `${version}`, Description: `${version}`,
}; };

View File

@ -15,6 +15,7 @@ const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
export enum ServerlessFunctionRuntime { export enum ServerlessFunctionRuntime {
NODE18 = 'nodejs18.x', NODE18 = 'nodejs18.x',
NODE22 = 'nodejs22.x',
} }
@Entity('serverlessFunction') @Entity('serverlessFunction')
@ -38,7 +39,7 @@ export class ServerlessFunctionEntity {
@Column({ nullable: true, type: 'jsonb' }) @Column({ nullable: true, type: 'jsonb' })
latestVersionInputSchema: InputSchema; latestVersionInputSchema: InputSchema;
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 }) @Column({ nullable: false, default: ServerlessFunctionRuntime.NODE22 })
runtime: ServerlessFunctionRuntime; runtime: ServerlessFunctionRuntime;
@Column({ nullable: false, default: DEFAULT_SERVERLESS_TIMEOUT_SECONDS }) @Column({ nullable: false, default: DEFAULT_SERVERLESS_TIMEOUT_SECONDS })

View File

@ -7,7 +7,7 @@
"build": "preconstruct build" "build": "preconstruct build"
}, },
"engines": { "engines": {
"node": "^18.17.1", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
"yarn": "^4.0.2" "yarn": "^4.0.2"
}, },

View File

@ -21,8 +21,7 @@ const jestConfig: JestConfigWithTsJest = {
], ],
}, },
moduleNameMapper: { moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|webp|svg|svg)$': '\\.(jpg|jpeg|png|gif|webp|svg|svg)$': '<rootDir>/__mocks__/imageMock.js',
'<rootDir>/__mocks__/imageMock.js',
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
prefix: '<rootDir>/', prefix: '<rootDir>/',
}), }),

View File

@ -12,7 +12,7 @@ info: The guide for contributors (or curious developers) who want to run Twenty
Before you can install and use Twenty, make sure you install the following on your computer: Before you can install and use Twenty, make sure you install the following on your computer:
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [Node v18](https://nodejs.org/en/download) - [Node v22](https://nodejs.org/en/download)
- [yarn v4](https://yarnpkg.com/getting-started/install) - [yarn v4](https://yarnpkg.com/getting-started/install)
- [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) - [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
@ -265,4 +265,4 @@ You can log in using the default demo account: `tim@apple.dev` (password: `tim@a
If you encounter any problem, check [Troubleshooting](https://twenty.com/developers/section/self-hosting/troubleshooting) for solutions. If you encounter any problem, check [Troubleshooting](https://twenty.com/developers/section/self-hosting/troubleshooting) for solutions.
<ArticleEditContent></ArticleEditContent> <ArticleEditContent />

View File

@ -12,7 +12,7 @@
"watch": "yarn clean && npx tsc --watch" "watch": "yarn clean && npx tsc --watch"
}, },
"engines": { "engines": {
"node": "^18.17.1", "node": "^22.12.0",
"npm": "please-use-yarn", "npm": "please-use-yarn",
"yarn": "^4.0.2" "yarn": "^4.0.2"
}, },

3816
yarn.lock

File diff suppressed because it is too large Load Diff