snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/babelrc.json",
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": [
[
"module-resolver",
{
"root": ["./"],
"alias": {
"@": "./src"
}
}
]
]
}

1
packages/twenty-shared/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,70 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"jsPlugins": ["../twenty-oxlint-rules/dist/oxlint-plugin.mjs"],
"plugins": ["typescript", "import", "unicorn"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "dist"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"twenty/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
},
"overrides": [
{
"files": ["**/constants/*.ts", "**/*.constants.ts"],
"rules": {
"twenty/max-consts-per-file": ["error", { "max": 1 }]
}
}
]
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-present Twenty.com, PBC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1 @@
export default 'test-file-stub';

View File

@@ -0,0 +1,52 @@
const jestConfig = {
silent: true,
displayName: 'twenty-shared',
preset: '../../jest.preset.js',
testEnvironment: 'jsdom',
setupFiles: ['<rootDir>/jest.setup.ts'],
transformIgnorePatterns: ['../../node_modules/'],
transform: {
'^.+\\.[tj]sx?$': [
'@swc/jest',
{
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: true,
},
transform: {
react: { runtime: 'automatic' },
decoratorMetadata: true,
},
},
},
],
},
moduleNameMapper: {
// TODO prastoin investigate not working with pathsToModuleNameMapper
/*
{
'^@/(.*)\\.js$': './src/$1',
'^@/(.*)$': './src/$1',
'^(\\.{1,2}/.*)\\.js$': '$1'
} // use esm true
{ '^@/(.*)$': './src/$1' } // useEsm false
*/
'/^@/(.*)$/': './src/$1',
'\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$':
'<rootDir>/__mocks__/imageMockShared.js',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageDirectory: './coverage',
coverageThreshold: {
global: {
statements: 80,
lines: 90,
functions: 75,
},
},
};
export default jestConfig;

View File

@@ -0,0 +1,4 @@
// jsdom lacks TextEncoder/TextDecoder, which @noble/hashes needs.
import { TextDecoder, TextEncoder } from 'node:util';
Object.assign(globalThis, { TextDecoder, TextEncoder });

View File

@@ -0,0 +1,212 @@
{
"name": "twenty-shared",
"private": true,
"sideEffects": false,
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"license": "MIT",
"scripts": {
"build": "npx vite build"
},
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
},
"devDependencies": {
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.24.6",
"@lingui/core": "^5.9.5",
"@prettier/sync": "^0.5.2",
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@types/addressparser": "^1.0.3",
"@types/babel__preset-env": "^7",
"@types/jest": "^30.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.escaperegexp": "^4.1.9",
"@types/qs": "6.9.16",
"@types/uuid": "^9.0.2",
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
"babel-plugin-module-resolver": "^5.0.2",
"glob": "^11.1.0",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"prettier": "^3.1.1",
"slash": "^5.1.0",
"tsc-alias": "^1.8.16",
"tsx": "^4.19.3",
"typescript": "^5.9.3",
"vite": "^8.0.0",
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.8",
"@noble/hashes": "^1.8.0",
"@sniptt/guards": "^0.2.0",
"addressparser": "1.0.1",
"ai": "7.0.93",
"class-validator": "^0.14.0",
"expr-eval-fork": "3.0.3",
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.escaperegexp": "^4.1.2",
"microdiff": "^1.3.2",
"qs": "^6.15.2",
"react-router-dom": "^6.30.6",
"temporal-polyfill": "^0.3.0",
"transliteration": "^2.3.5",
"uuid": "^11.1.1",
"zod": "^4.1.11"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./ai": {
"types": "./dist/ai/index.d.ts",
"import": "./dist/ai.mjs",
"require": "./dist/ai.cjs"
},
"./application": {
"types": "./dist/application/index.d.ts",
"import": "./dist/application.mjs",
"require": "./dist/application.cjs"
},
"./constants": {
"types": "./dist/constants/index.d.ts",
"import": "./dist/constants.mjs",
"require": "./dist/constants.cjs"
},
"./database-events": {
"types": "./dist/database-events/index.d.ts",
"import": "./dist/database-events.mjs",
"require": "./dist/database-events.cjs"
},
"./i18n": {
"types": "./dist/i18n/index.d.ts",
"import": "./dist/i18n.mjs",
"require": "./dist/i18n.cjs"
},
"./logic-function": {
"types": "./dist/logic-function/index.d.ts",
"import": "./dist/logic-function.mjs",
"require": "./dist/logic-function.cjs"
},
"./metadata": {
"types": "./dist/metadata/index.d.ts",
"import": "./dist/metadata.mjs",
"require": "./dist/metadata.cjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing.mjs",
"require": "./dist/testing.cjs"
},
"./timeline": {
"types": "./dist/timeline/index.d.ts",
"import": "./dist/timeline.mjs",
"require": "./dist/timeline.cjs"
},
"./translations": {
"types": "./dist/translations/index.d.ts",
"import": "./dist/translations.mjs",
"require": "./dist/translations.cjs"
},
"./types": {
"types": "./dist/types/index.d.ts",
"import": "./dist/types.mjs",
"require": "./dist/types.cjs"
},
"./utils": {
"types": "./dist/utils/index.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
"./vite": {
"types": "./dist/vite/index.d.ts",
"import": "./dist/vite.mjs",
"require": "./dist/vite.cjs"
},
"./workflow": {
"types": "./dist/workflow/index.d.ts",
"import": "./dist/workflow.mjs",
"require": "./dist/workflow.cjs"
},
"./workspace": {
"types": "./dist/workspace/index.d.ts",
"import": "./dist/workspace.mjs",
"require": "./dist/workspace.cjs"
}
},
"files": [
"dist",
"ai",
"application",
"constants",
"database-events",
"i18n",
"logic-function",
"metadata",
"testing",
"timeline",
"translations",
"types",
"utils",
"vite",
"workflow",
"workspace"
],
"typesVersions": {
"*": {
"ai": [
"dist/ai/index.d.ts"
],
"application": [
"dist/application/index.d.ts"
],
"constants": [
"dist/constants/index.d.ts"
],
"database-events": [
"dist/database-events/index.d.ts"
],
"i18n": [
"dist/i18n/index.d.ts"
],
"logic-function": [
"dist/logic-function/index.d.ts"
],
"metadata": [
"dist/metadata/index.d.ts"
],
"testing": [
"dist/testing/index.d.ts"
],
"timeline": [
"dist/timeline/index.d.ts"
],
"translations": [
"dist/translations/index.d.ts"
],
"types": [
"dist/types/index.d.ts"
],
"utils": [
"dist/utils/index.d.ts"
],
"vite": [
"dist/vite/index.d.ts"
],
"workflow": [
"dist/workflow/index.d.ts"
],
"workspace": [
"dist/workspace/index.d.ts"
]
}
}
}

View File

@@ -0,0 +1,82 @@
{
"name": "twenty-shared",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-shared/src",
"projectType": "library",
"tags": ["scope:shared"],
"targets": {
"build": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "^production"],
"dependsOn": ["generateBarrels", "^build"],
"outputs": [
"{projectRoot}/dist",
"{projectRoot}/ai/package.json",
"{projectRoot}/ai/dist",
"{projectRoot}/application/package.json",
"{projectRoot}/application/dist",
"{projectRoot}/constants/package.json",
"{projectRoot}/constants/dist",
"{projectRoot}/database-events/package.json",
"{projectRoot}/database-events/dist",
"{projectRoot}/i18n/package.json",
"{projectRoot}/i18n/dist",
"{projectRoot}/logic-function/package.json",
"{projectRoot}/logic-function/dist",
"{projectRoot}/metadata/package.json",
"{projectRoot}/metadata/dist",
"{projectRoot}/testing/package.json",
"{projectRoot}/testing/dist",
"{projectRoot}/timeline/package.json",
"{projectRoot}/timeline/dist",
"{projectRoot}/translations/package.json",
"{projectRoot}/translations/dist",
"{projectRoot}/types/package.json",
"{projectRoot}/types/dist",
"{projectRoot}/utils/package.json",
"{projectRoot}/utils/dist",
"{projectRoot}/vite/package.json",
"{projectRoot}/vite/dist",
"{projectRoot}/workflow/package.json",
"{projectRoot}/workflow/dist",
"{projectRoot}/workspace/package.json",
"{projectRoot}/workspace/dist"
],
"options": {
"cwd": "{projectRoot}",
"commands": [
"npx vite build",
"tsgo -p tsconfig.lib.json --declaration --emitDeclarationOnly --noEmit false --outDir dist --rootDir src && npx tsc-alias -p tsconfig.lib.json --outDir dist"
],
"parallel": false
}
},
"build:individual": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist/individual"],
"options": {
"cwd": "{projectRoot}",
"command": "npx vite build -c vite.config.individual.ts"
}
},
"generateBarrels": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.ts"],
"outputs": [
"{projectRoot}/src/index.ts",
"{projectRoot}/src/*/index.ts",
"{projectRoot}/package.json"
],
"options": { "command": "tsx {projectRoot}/scripts/generateBarrels.ts" }
},
"typecheck": {},
"test": {},
"lint": { "options": {}, "configurations": { "fix": {} } },
"fmt": { "options": { "files": "src" }, "configurations": { "fix": {} } }
}
}

View File

@@ -0,0 +1,502 @@
import prettier from '@prettier/sync';
import * as fs from 'fs';
import { globSync } from 'glob';
import path from 'path';
import { type Options } from 'prettier';
import slash from 'slash';
import ts from 'typescript';
// TODO prastoin refactor this file in several one into its dedicated package and make it a TypeScript CLI
const INDEX_FILENAME = 'index';
const PACKAGE_JSON_FILENAME = 'package.json';
const NX_PROJECT_CONFIGURATION_FILENAME = 'project.json';
const PACKAGE_PATH = path.resolve('packages/twenty-shared');
const SRC_PATH = path.resolve(`${PACKAGE_PATH}/src`);
const PACKAGE_JSON_PATH = path.join(PACKAGE_PATH, PACKAGE_JSON_FILENAME);
const NX_PROJECT_CONFIGURATION_PATH = path.join(
PACKAGE_PATH,
NX_PROJECT_CONFIGURATION_FILENAME,
);
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {
throw new Error('Prettier config file not found');
}
const prettierConfiguration = prettier.resolveConfig(prettierConfigFile);
const prettierFormat = (str: string, parser: Options['parser']) =>
prettier.format(str, {
...prettierConfiguration,
parser,
});
type createTypeScriptFileArgs = {
path: string;
content: string;
filename: string;
};
const createTypeScriptFile = ({
content,
path: filePath,
filename,
}: createTypeScriptFileArgs) => {
const header = `
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file
* | | \\ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \\_/\\_/ \\___|_| |_|\\__|\\__, |
* |___/
*/
`;
const formattedContent = prettierFormat(
`${header}\n${content}\n`,
'typescript',
);
fs.writeFileSync(
path.join(filePath, `${filename}.ts`),
formattedContent,
'utf-8',
);
};
const getLastPathFolder = (pathStr: string) => path.basename(pathStr);
const getSubDirectoryPaths = (directoryPath: string): string[] => {
const pattern = slash(path.join(directoryPath, '*/'));
return globSync(pattern, {
ignore: [...EXCLUDED_DIRECTORIES],
cwd: SRC_PATH,
nodir: false,
maxDepth: 1,
}).sort((a, b) => a.localeCompare(b));
};
const partitionFileExportsByType = (declarations: DeclarationOccurrence[]) => {
return declarations.reduce<{
typeAndInterfaceDeclarations: DeclarationOccurrence[];
otherDeclarations: DeclarationOccurrence[];
}>(
(acc, { kind, name }) => {
if (kind === 'type' || kind === 'interface') {
return {
...acc,
typeAndInterfaceDeclarations: [
...acc.typeAndInterfaceDeclarations,
{ kind, name },
],
};
}
return {
...acc,
otherDeclarations: [...acc.otherDeclarations, { kind, name }],
};
},
{
typeAndInterfaceDeclarations: [],
otherDeclarations: [],
},
);
};
const generateModuleIndexFiles = (exportByBarrel: ExportByBarrel[]) => {
return exportByBarrel.map<createTypeScriptFileArgs>(
({ barrel: { moduleDirectory }, allFileExports }) => {
const content = allFileExports
.sort((a, b) => a.file.localeCompare(b.file))
.map(({ exports, file }) => {
const { otherDeclarations, typeAndInterfaceDeclarations } =
partitionFileExportsByType(exports);
const fileWithoutExtension = path.parse(file).name;
const pathToImport = slash(
path.relative(
moduleDirectory,
path.join(path.dirname(file), fileWithoutExtension),
),
);
const mapDeclarationNameAndJoin = (
declarations: DeclarationOccurrence[],
) => declarations.map(({ name }) => name).join(', ');
const typeExport =
typeAndInterfaceDeclarations.length > 0
? `export type { ${mapDeclarationNameAndJoin(typeAndInterfaceDeclarations)} } from "./${pathToImport}"`
: '';
const othersExport =
otherDeclarations.length > 0
? `export { ${mapDeclarationNameAndJoin(otherDeclarations)} } from "./${pathToImport}"`
: '';
return [typeExport, othersExport]
.filter((el) => el !== '')
.join('\n');
})
.join('\n');
return {
content,
path: moduleDirectory,
filename: INDEX_FILENAME,
};
},
);
};
type JsonUpdate = Record<string, any>;
type WriteInJsonFileArgs = {
content: JsonUpdate;
file: string;
};
const updateJsonFile = ({ content, file }: WriteInJsonFileArgs) => {
const updatedJsonFile = JSON.stringify(content);
const formattedContent = prettier.format(updatedJsonFile, {
...prettierConfiguration,
filepath: file,
});
fs.writeFileSync(file, formattedContent, 'utf-8');
};
const writeInPackageJson = (update: JsonUpdate) => {
const rawJsonFile = fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8');
const initialJsonFile = JSON.parse(rawJsonFile);
updateJsonFile({
file: PACKAGE_JSON_PATH,
content: {
...initialJsonFile,
...update,
},
});
};
const updateNxProjectConfigurationBuildOutputs = (outputs: JsonUpdate) => {
const rawJsonFile = fs.readFileSync(NX_PROJECT_CONFIGURATION_PATH, 'utf-8');
const initialJsonFile = JSON.parse(rawJsonFile);
updateJsonFile({
file: NX_PROJECT_CONFIGURATION_PATH,
content: {
...initialJsonFile,
targets: {
...initialJsonFile.targets,
build: {
...initialJsonFile.targets.build,
outputs,
},
},
},
});
};
type ExportOccurrence = {
types: string;
import: string;
require: string;
};
type ExportsConfig = Record<string, ExportOccurrence | string>;
const generateModulePackageExports = (moduleDirectories: string[]) => {
return moduleDirectories.reduce<ExportsConfig>((acc, moduleDirectory) => {
const moduleName = getLastPathFolder(moduleDirectory);
if (moduleName === undefined) {
throw new Error(
`Should never occur, moduleName is undefined ${moduleDirectory}`,
);
}
return {
...acc,
[`./${moduleName}`]: {
types: `./dist/${moduleName}/index.d.ts`,
import: `./dist/${moduleName}.mjs`,
require: `./dist/${moduleName}.cjs`,
},
};
}, {});
};
const computePackageJsonFilesAndExportsConfig = (
moduleDirectories: string[],
) => {
const entrypoints = moduleDirectories.map(getLastPathFolder);
const exports = {
'.': {
types: './dist/index.d.ts',
import: './dist/index.mjs',
require: './dist/index.cjs',
},
...generateModulePackageExports(moduleDirectories),
} satisfies ExportsConfig;
const typesVersionsEntries = entrypoints.reduce<Record<string, string[]>>(
(acc, moduleName) => ({
...acc,
[`${moduleName}`]: [`dist/${moduleName}/index.d.ts`],
}),
{},
);
return {
exports,
typesVersions: { '*': typesVersionsEntries },
files: ['dist', ...entrypoints],
};
};
const computeProjectNxBuildOutputsPath = (moduleDirectories: string[]) => {
const dynamicOutputsPath = moduleDirectories
.map(getLastPathFolder)
.flatMap((barrelName) =>
['package.json', 'dist'].map(
(subPath) => `{projectRoot}/${barrelName}/${subPath}`,
),
);
return ['{projectRoot}/dist', ...dynamicOutputsPath];
};
const EXCLUDED_EXTENSIONS = [
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.stories.ts',
'**/*.stories.tsx',
] as const;
const EXCLUDED_DIRECTORIES = [
'**/__tests__/**',
'**/__mocks__/**',
'**/__stories__/**',
'**/internal/**',
] as const;
const EXCLUDED_FILES = ['**/get-function-input-schema.ts'] as const;
const getTypeScriptFiles = (
directoryPath: string,
includeIndex: boolean = false,
): string[] => {
const pattern = slash(path.join(directoryPath, '**', '*.{ts,tsx}'));
const files = globSync(pattern, {
cwd: SRC_PATH,
nodir: true,
ignore: [
...EXCLUDED_EXTENSIONS,
...EXCLUDED_DIRECTORIES,
...EXCLUDED_FILES,
],
});
return files.filter(
(file) =>
!file.endsWith('.d.ts') &&
(includeIndex ? true : !file.endsWith('index.ts')),
);
};
const getKind = (
node: ts.VariableStatement,
): Extract<ExportKind, 'const' | 'let' | 'var'> => {
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
if (isConst) {
return 'const';
}
const isLet = (node.declarationList.flags & ts.NodeFlags.Let) !== 0;
if (isLet) {
return 'let';
}
return 'var';
};
const extractExportsFromSourceFile = (sourceFile: ts.SourceFile) => {
const exports: DeclarationOccurrence[] = [];
const visit = (node: ts.Node): void => {
if (!ts.canHaveModifiers(node)) {
return ts.forEachChild(node, visit);
}
const modifiers = ts.getModifiers(node);
const isExport = modifiers?.some(
(mod) => mod.kind === ts.SyntaxKind.ExportKeyword,
);
if (!isExport && !ts.isExportDeclaration(node)) {
return ts.forEachChild(node, visit);
}
switch (true) {
case ts.isTypeAliasDeclaration(node):
exports.push({
kind: 'type',
name: node.name.text,
});
break;
case ts.isInterfaceDeclaration(node):
exports.push({
kind: 'interface',
name: node.name.text,
});
break;
case ts.isEnumDeclaration(node):
exports.push({
kind: 'enum',
name: node.name.text,
});
break;
case ts.isFunctionDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'function',
name: node.name.text,
});
break;
case ts.isVariableStatement(node):
node.declarationList.declarations.forEach((decl) => {
const kind = getKind(node);
if (ts.isIdentifier(decl.name)) {
exports.push({
kind,
name: decl.name.text,
});
} else if (ts.isObjectBindingPattern(decl.name)) {
decl.name.elements.forEach((element) => {
if (
!ts.isBindingElement(element) ||
!ts.isIdentifier(element.name)
) {
return;
}
exports.push({
kind,
name: element.name.text,
});
});
}
});
break;
case ts.isClassDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'class',
name: node.name.text,
});
break;
case ts.isExportDeclaration(node):
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const exportName = element.name.text;
// Check both the declaration and the individual specifier for type-only exports
const isTypeExport =
node.isTypeOnly || ts.isTypeOnlyExportDeclaration(node);
if (isTypeExport) {
exports.push({
kind: 'type',
name: exportName,
});
return;
}
exports.push({
kind: 'const',
name: exportName,
});
});
}
break;
}
return ts.forEachChild(node, visit);
};
visit(sourceFile);
return exports;
};
type ExportKind =
| 'type'
| 'interface'
| 'enum'
| 'function'
| 'const'
| 'let'
| 'var'
| 'class';
type DeclarationOccurrence = { kind: ExportKind; name: string };
type FileExports = Array<{
file: string;
exports: DeclarationOccurrence[];
}>;
const findAllExports = (directoryPath: string): FileExports => {
const results: FileExports = [];
const files = getTypeScriptFiles(directoryPath);
for (const file of files) {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const exports = extractExportsFromSourceFile(sourceFile);
if (exports.length > 0) {
results.push({
file,
exports,
});
}
}
return results;
};
type ExportByBarrel = {
barrel: {
moduleName: string;
moduleDirectory: string;
};
allFileExports: FileExports;
};
const retrieveExportsByBarrel = (barrelDirectories: string[]) => {
return barrelDirectories.map<ExportByBarrel>((moduleDirectory) => {
const moduleExportsPerFile = findAllExports(moduleDirectory);
const moduleName = getLastPathFolder(moduleDirectory);
if (!moduleName) {
throw new Error(
`Should never occur moduleName not found ${moduleDirectory}`,
);
}
return {
barrel: {
moduleName,
moduleDirectory,
},
allFileExports: moduleExportsPerFile,
};
});
};
const main = () => {
const moduleDirectories = getSubDirectoryPaths(SRC_PATH);
const exportsByBarrel = retrieveExportsByBarrel(moduleDirectories);
const moduleIndexFiles = generateModuleIndexFiles(exportsByBarrel);
const packageJsonConfig =
computePackageJsonFilesAndExportsConfig(moduleDirectories);
const nxBuildOutputsPath =
computeProjectNxBuildOutputsPath(moduleDirectories);
updateNxProjectConfigurationBuildOutputs(nxBuildOutputsPath);
writeInPackageJson(packageJsonConfig);
moduleIndexFiles.forEach(createTypeScriptFile);
};
main();

View File

@@ -0,0 +1,11 @@
import { type AiModelEffort } from './ai-model-effort.const';
export const AI_MODEL_EFFORT_LABELS: Record<AiModelEffort, string> = {
none: 'no reasoning',
minimal: 'minimal reasoning',
low: 'low reasoning',
medium: 'medium reasoning',
high: 'high reasoning',
xhigh: 'very high reasoning',
max: 'max reasoning',
};

View File

@@ -0,0 +1,14 @@
// Every effort level a supported provider accepts, in ascending order. A model
// lists the subset it takes in the catalog, since the set differs per model even
// within one provider.
export const AI_MODEL_EFFORTS = [
'none',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
] as const;
export type AiModelEffort = (typeof AI_MODEL_EFFORTS)[number];

View File

@@ -0,0 +1,11 @@
// Ordered from fastest and cheapest to most capable. The order is what the
// slider walks, so it must stay monotonic in both directions.
export const AI_MODEL_TIERS = [
'extraFast',
'fast',
'balanced',
'smart',
'extraSmart',
] as const;
export type AiModelTier = (typeof AI_MODEL_TIERS)[number];

View File

@@ -0,0 +1,12 @@
import { type AiSdkPackage } from './ai-sdk-packages.const';
export const AI_SDK_PACKAGE_LABELS: Record<AiSdkPackage, string> = {
'@ai-sdk/openai': 'OpenAI',
'@ai-sdk/anthropic': 'Anthropic',
'@ai-sdk/google': 'Google',
'@ai-sdk/mistral': 'Mistral',
'@ai-sdk/xai': 'xAI',
'@ai-sdk/amazon-bedrock': 'AWS Bedrock',
'@ai-sdk/openai-compatible': 'OpenAI-Compatible',
'@ai-sdk/azure': 'Azure OpenAI',
};

View File

@@ -0,0 +1,12 @@
export const AI_SDK_PACKAGES = [
'@ai-sdk/openai',
'@ai-sdk/anthropic',
'@ai-sdk/google',
'@ai-sdk/mistral',
'@ai-sdk/xai',
'@ai-sdk/amazon-bedrock',
'@ai-sdk/openai-compatible',
'@ai-sdk/azure',
] as const;
export type AiSdkPackage = (typeof AI_SDK_PACKAGES)[number];

View File

@@ -0,0 +1 @@
export const ASK_QUESTIONS_TOOL_NAME = 'ask_questions';

View File

@@ -0,0 +1,15 @@
import { type AiModelTier } from './ai-model-tier.const';
// Stored in agent.modelId and sent by the chat client in place of a concrete
// `provider/model` id. The fast and smart values predate tiers and live in
// existing rows, so they keep their original spelling.
export const AUTO_SELECT_MODEL_ID_BY_TIER = {
extraFast: 'default-extra-fast-model',
fast: 'default-fast-model',
balanced: 'default-balanced-model',
smart: 'default-smart-model',
extraSmart: 'default-extra-smart-model',
} as const satisfies Record<AiModelTier, string>;
export type AutoSelectModelId =
(typeof AUTO_SELECT_MODEL_ID_BY_TIER)[AiModelTier];

View File

@@ -0,0 +1,4 @@
// Stored on an agent that follows the workspace's agent tier instead of naming
// one of its own, so changing the workspace setting moves every such agent.
export const AUTO_SELECT_WORKSPACE_DEFAULT_MODEL_ID =
'workspace-default-model' as const;

View File

@@ -0,0 +1 @@
export const COMPLETE_WORKSPACE_SETUP_TOOL_NAME = 'complete_workspace_setup';

View File

@@ -0,0 +1,14 @@
export const DATA_RESIDENCY_KEYS = [
'us',
'eu',
'global',
'uk',
'ap',
'jp',
'au',
'ca',
'de',
'fr',
] as const;
export type DataResidency = (typeof DATA_RESIDENCY_KEYS)[number];

View File

@@ -0,0 +1,14 @@
export const DATABASE_CRUD_OPERATIONS = [
'find_many',
'find_one',
'group_by',
'create_one',
'create_many',
'update_one',
'update_many',
'upsert_many',
'delete_one',
'delete_many',
] as const;
export type DatabaseCrudOperation = (typeof DATABASE_CRUD_OPERATIONS)[number];

View File

@@ -0,0 +1,3 @@
import { type AiModelTier } from './ai-model-tier.const';
export const DEFAULT_AI_AGENT_MODEL_TIER: AiModelTier = 'fast';

View File

@@ -0,0 +1,5 @@
import { type AiModelTier } from './ai-model-tier.const';
// Cheap and quick is the right default for most workspaces; the slider is one
// click away for anyone who wants more.
export const DEFAULT_AI_CHAT_MODEL_TIER: AiModelTier = 'fast';

View File

@@ -0,0 +1,12 @@
export enum ToolCategory {
DATABASE_CRUD = 'DATABASE_CRUD',
ACTION = 'ACTION',
WORKFLOW = 'WORKFLOW',
METADATA = 'METADATA',
VIEW = 'VIEW',
DASHBOARD = 'DASHBOARD',
NAVIGATION_MENU_ITEM = 'NAVIGATION_MENU_ITEM',
WEBHOOK = 'WEBHOOK',
LOGIC_FUNCTION = 'LOGIC_FUNCTION',
ROLE = 'ROLE',
}

View File

@@ -0,0 +1,74 @@
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \_/\_/ \___|_| |_|\__|\__, |
* |___/
*/
export { AI_MODEL_EFFORT_LABELS } from './constants/ai-model-effort-labels.const';
export type { AiModelEffort } from './constants/ai-model-effort.const';
export { AI_MODEL_EFFORTS } from './constants/ai-model-effort.const';
export type { AiModelTier } from './constants/ai-model-tier.const';
export { AI_MODEL_TIERS } from './constants/ai-model-tier.const';
export { AI_SDK_PACKAGE_LABELS } from './constants/ai-sdk-package-labels.const';
export type { AiSdkPackage } from './constants/ai-sdk-packages.const';
export { AI_SDK_PACKAGES } from './constants/ai-sdk-packages.const';
export { ASK_QUESTIONS_TOOL_NAME } from './constants/ask-questions-tool-name.const';
export type { AutoSelectModelId } from './constants/auto-select-model-id-by-tier.const';
export { AUTO_SELECT_MODEL_ID_BY_TIER } from './constants/auto-select-model-id-by-tier.const';
export { AUTO_SELECT_WORKSPACE_DEFAULT_MODEL_ID } from './constants/auto-select-workspace-default-model-id.const';
export { COMPLETE_WORKSPACE_SETUP_TOOL_NAME } from './constants/complete-workspace-setup-tool-name.const';
export type { DataResidency } from './constants/data-residency.const';
export { DATA_RESIDENCY_KEYS } from './constants/data-residency.const';
export type { DatabaseCrudOperation } from './constants/database-crud-operation.const';
export { DATABASE_CRUD_OPERATIONS } from './constants/database-crud-operation.const';
export { DEFAULT_AI_AGENT_MODEL_TIER } from './constants/default-ai-agent-model-tier.const';
export { DEFAULT_AI_CHAT_MODEL_TIER } from './constants/default-ai-chat-model-tier.const';
export { ToolCategory } from './constants/tool-category.const';
export type {
AgentResponseFormatType,
AgentTextResponseFormat,
AgentJsonResponseFormat,
AgentResponseFormat,
} from './types/agent-response-format.type';
export type {
AgentResponseFieldType,
AgentResponseSchema,
} from './types/agent-response-schema.type';
export type { AgentChatSubscriptionEvent } from './types/AgentChatSubscriptionEvent';
export type { AskQuestionAnswer } from './types/AskQuestionAnswer';
export type { AskQuestionItem } from './types/AskQuestionItem';
export type { AskQuestionOption } from './types/AskQuestionOption';
export type { AskQuestionsToolInput } from './types/AskQuestionsToolInput';
export type { AskQuestionsToolResult } from './types/AskQuestionsToolResult';
export type { AskQuestionsToolStatus } from './types/AskQuestionsToolStatus';
export type {
CodeExecutionFile,
ExtendedFileUIPart,
CodeExecutionState,
CodeExecutionData,
DataMessagePart,
} from './types/DataMessagePart';
export { isExtendedFileUIPart } from './types/DataMessagePart';
export type {
AiChatUsageMetadata,
AiChatModelMetadata,
ExtendedUIMessage,
} from './types/ExtendedUIMessage';
export type { ExtendedUIMessagePart } from './types/ExtendedUIMessagePart';
export type { ModelConfiguration } from './types/model-configuration.type';
export type { NavigateAppToolOutput } from './types/NavigateAppToolOutput';
export { formatRecordReference } from './utils/format-record-reference.util';
export { formatSkillReference } from './utils/format-skill-reference.util';
export { getAiModelTierFromModelId } from './utils/get-ai-model-tier-from-model-id.util';
export { inferAiSdkPackage } from './utils/infer-ai-sdk-package.util';
export { isAiModelEffort } from './utils/is-ai-model-effort.util';
export { isAiModelTier } from './utils/is-ai-model-tier.util';
export { isAiSdkPackage } from './utils/is-ai-sdk-package.util';
export { isCompleteWorkspaceSetupToolPart } from './utils/is-complete-workspace-setup-tool-part.util';
export { isDataResidency } from './utils/is-data-residency.util';
export { isSucceededCompleteWorkspaceSetupToolPart } from './utils/is-succeeded-complete-workspace-setup-tool-part.util';
export { isValidAgentResponseSchemaPropertyKey } from './utils/is-valid-agent-response-schema-property-key.util';
export { parseAiModelVariantId } from './utils/parse-ai-model-variant-id.util';

View File

@@ -0,0 +1,11 @@
// `chunk` is typed as Record<string, unknown> rather than UIMessageChunk
// to avoid importing the `ai` package in twenty-shared. The frontend casts
// it back to UIMessageChunk when feeding it into readUIMessageStream.
export type AgentChatSubscriptionEvent =
| { type: 'stream-chunk'; chunk: Record<string, unknown>; seq?: number }
| { type: 'message-persisted'; messageId: string }
| { type: 'queue-updated' }
| { type: 'question-answered' }
| { type: 'stream-error'; code: string; message: string }
| { type: 'credits-exhausted' }
| { type: 'keepalive' };

View File

@@ -0,0 +1,5 @@
export type AskQuestionAnswer = {
questionIndex: number;
selectedOptionIndices: number[];
freeText?: string;
};

View File

@@ -0,0 +1,8 @@
import { type AskQuestionOption } from '@/ai/types/AskQuestionOption';
export type AskQuestionItem = {
header: string;
question: string;
options: AskQuestionOption[];
allowMultiSelect?: boolean;
};

View File

@@ -0,0 +1,5 @@
export type AskQuestionOption = {
label: string;
description?: string;
isRecommended?: boolean;
};

View File

@@ -0,0 +1,5 @@
import { type AskQuestionItem } from '@/ai/types/AskQuestionItem';
export type AskQuestionsToolInput = {
questions: AskQuestionItem[];
};

View File

@@ -0,0 +1,9 @@
import { type AskQuestionAnswer } from '@/ai/types/AskQuestionAnswer';
import { type AskQuestionItem } from '@/ai/types/AskQuestionItem';
import { type AskQuestionsToolStatus } from '@/ai/types/AskQuestionsToolStatus';
export type AskQuestionsToolResult = {
questions: AskQuestionItem[];
status: AskQuestionsToolStatus;
answers?: AskQuestionAnswer[];
};

View File

@@ -0,0 +1 @@
export type AskQuestionsToolStatus = 'pending' | 'answered' | 'skipped';

View File

@@ -0,0 +1,86 @@
import { isDefined } from '@/utils/validation/isDefined';
export type CodeExecutionFile = {
fileId: string;
filename: string;
url: string;
mimeType: string;
};
export type ExtendedFileUIPart = {
type: 'file';
mediaType: string;
filename?: string;
url: string;
fileId: string;
};
export const isExtendedFileUIPart = (
part: Record<string, unknown>,
): part is ExtendedFileUIPart => {
return (
part.type === 'file' &&
isDefined(part.fileId) &&
isDefined(part.url) &&
isDefined(part.mediaType)
);
};
export type CodeExecutionState = 'pending' | 'running' | 'completed' | 'error';
export type CodeExecutionData = {
executionId: string;
state: CodeExecutionState;
code: string;
language: 'python';
stdout: string;
stderr: string;
exitCode?: number;
executionTimeMs?: number;
files: CodeExecutionFile[];
error?: string;
};
export type DataMessagePart = {
'routing-status': {
text: string;
state: string;
debug?: {
routingTimeMs?: number;
contextBuildTimeMs?: number;
agentExecutionStartTimeMs?: number;
agentExecutionTimeMs?: number;
toolGenerationTimeMs?: number;
agentContextBuildTimeMs?: number;
aiRequestPrepTimeMs?: number;
selectedAgentId?: string;
selectedAgentLabel?: string;
availableAgents?: Array<{ id: string; label: string }>;
agentModel?: string;
context?: string;
contextRecordCount?: number;
contextSizeBytes?: number;
toolCallCount?: number;
toolCount?: number;
routingPromptTokens?: number;
routingCompletionTokens?: number;
routingTotalTokens?: number;
agentPromptTokens?: number;
agentCompletionTokens?: number;
agentTotalTokens?: number;
routingCostInCredits?: number;
agentCostInCredits?: number;
totalCostInCredits?: number;
planReasoning?: string;
totalSteps?: number;
steps?: Array<{
stepNumber: number;
agent: string;
task: string;
}>;
};
};
'code-execution': CodeExecutionData;
'thread-title': { title: string };
compaction: Record<string, never>;
};

View File

@@ -0,0 +1,27 @@
import { type DataMessagePart } from '@/ai/types/DataMessagePart';
import { type Nullable } from '@/types';
import { type UIMessage } from 'ai';
export type AiChatUsageMetadata = {
inputTokens: number;
outputTokens: number;
cachedInputTokens: number;
inputCredits: number;
outputCredits: number;
conversationSize: number;
};
export type AiChatModelMetadata = {
contextWindowTokens: number;
};
type Metadata = {
createdAt: string;
usage?: AiChatUsageMetadata;
model?: AiChatModelMetadata;
};
export type ExtendedUIMessage = UIMessage<Metadata, DataMessagePart> & {
threadId?: Nullable<string>;
status?: 'queued' | 'sent';
};

View File

@@ -0,0 +1,4 @@
import { type DataMessagePart } from '@/ai/types/DataMessagePart';
import { type UIMessagePart, type UITools } from 'ai';
export type ExtendedUIMessagePart = UIMessagePart<DataMessagePart, UITools>;

View File

@@ -0,0 +1,20 @@
export type NavigateAppToolOutput =
| {
action: 'navigateToObject';
objectNameSingular: string;
}
| {
action: 'navigateToView';
viewId: string;
viewName: string;
objectNameSingular: string;
}
| {
action: 'navigateToRecord';
objectNameSingular: string;
recordId: string;
}
| {
action: 'wait';
durationMs: number;
};

View File

@@ -0,0 +1,12 @@
import { type AgentResponseSchema } from '@/ai/types/agent-response-schema.type';
export type AgentResponseFormatType = AgentResponseFormat['type'];
export type AgentTextResponseFormat = { type: 'text' };
export type AgentJsonResponseFormat = {
type: 'json';
schema: AgentResponseSchema;
};
export type AgentResponseFormat =
| AgentTextResponseFormat
| AgentJsonResponseFormat;

View File

@@ -0,0 +1,15 @@
export type AgentResponseFieldType = 'string' | 'number' | 'boolean';
// Our simplified schema format (flat object with primitives only)
export type AgentResponseSchema = {
type: 'object';
properties: Record<
string,
{
type: AgentResponseFieldType;
description?: string;
}
>;
required?: string[];
additionalProperties?: false;
};

View File

@@ -0,0 +1,5 @@
export type {
AgentResponseFieldType,
AgentResponseSchema,
} from './agent-response-schema.type';
export type { ModelConfiguration } from './model-configuration.type';

View File

@@ -0,0 +1,10 @@
export type ModelConfiguration = {
webSearch?: {
enabled: boolean;
configuration?: Record<string, unknown>;
};
twitterSearch?: {
enabled: boolean;
configuration?: Record<string, unknown>;
};
};

View File

@@ -0,0 +1,13 @@
import { formatRecordReference } from '@/ai/utils/format-record-reference.util';
describe('formatRecordReference', () => {
it('formats a record reference without altering its display name', () => {
expect(
formatRecordReference({
objectNameSingular: 'company',
recordId: 'record-id',
displayName: 'Acme Corp',
}),
).toBe('[[record:company:record-id:Acme Corp]]');
});
});

View File

@@ -0,0 +1,12 @@
import { formatSkillReference } from '@/ai/utils/format-skill-reference.util';
describe('formatSkillReference', () => {
it('should format a skill reference with its id and label', () => {
expect(
formatSkillReference({
skillId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
label: 'Workflow building',
}),
).toBe('[[skill:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow building]]');
});
});

View File

@@ -0,0 +1,23 @@
import { getAiModelTierFromModelId } from '../get-ai-model-tier-from-model-id.util';
describe('getAiModelTierFromModelId', () => {
it('maps every auto-select id to its tier', () => {
expect(getAiModelTierFromModelId('default-extra-fast-model')).toBe(
'extraFast',
);
expect(getAiModelTierFromModelId('default-fast-model')).toBe('fast');
expect(getAiModelTierFromModelId('default-balanced-model')).toBe(
'balanced',
);
expect(getAiModelTierFromModelId('default-smart-model')).toBe('smart');
expect(getAiModelTierFromModelId('default-extra-smart-model')).toBe(
'extraSmart',
);
});
it('returns undefined for concrete model ids and empty values', () => {
expect(getAiModelTierFromModelId('openai/gpt-5.6-luna')).toBeUndefined();
expect(getAiModelTierFromModelId(null)).toBeUndefined();
expect(getAiModelTierFromModelId(undefined)).toBeUndefined();
});
});

View File

@@ -0,0 +1,25 @@
import { isValidAgentResponseSchemaPropertyKey } from '../is-valid-agent-response-schema-property-key.util';
describe('isValidAgentResponseSchemaPropertyKey', () => {
it.each([
'summary',
'status_2',
'meetings.brief',
'a-b',
'A',
'a'.repeat(64),
])('should accept "%s"', (propertyKey) => {
expect(isValidAgentResponseSchemaPropertyKey(propertyKey)).toBe(true);
});
it.each([
['a name with spaces', 'meetings brief'],
['leading space', ' summary'],
['empty string', ''],
['over 64 characters', 'a'.repeat(65)],
['unsupported symbol', 'meetings@brief'],
['unicode', 'résumé'],
])('should reject %s', (_label, propertyKey) => {
expect(isValidAgentResponseSchemaPropertyKey(propertyKey)).toBe(false);
});
});

View File

@@ -0,0 +1,31 @@
import { parseAiModelVariantId } from '../parse-ai-model-variant-id.util';
describe('parseAiModelVariantId', () => {
it('splits a variant id into its model and effort', () => {
expect(parseAiModelVariantId('anthropic/claude-opus-5@xhigh')).toEqual({
modelId: 'anthropic/claude-opus-5',
effort: 'xhigh',
});
});
it('keeps a Bedrock name that carries a colon intact', () => {
expect(
parseAiModelVariantId('amazon-bedrock/us.deepseek.r1-v1:0@high'),
).toEqual({
modelId: 'amazon-bedrock/us.deepseek.r1-v1:0',
effort: 'high',
});
});
it('returns a plain id without an effort', () => {
expect(parseAiModelVariantId('openai/gpt-5.6-sol')).toEqual({
modelId: 'openai/gpt-5.6-sol',
});
});
it('treats a suffix that is not an effort as part of the id', () => {
expect(parseAiModelVariantId('custom/model@v2')).toEqual({
modelId: 'custom/model@v2',
});
});
});

View File

@@ -0,0 +1,9 @@
export const formatRecordReference = ({
objectNameSingular,
recordId,
displayName,
}: {
objectNameSingular: string;
recordId: string;
displayName: string;
}): string => `[[record:${objectNameSingular}:${recordId}:${displayName}]]`;

View File

@@ -0,0 +1,7 @@
export const formatSkillReference = ({
skillId,
label,
}: {
skillId: string;
label: string;
}): string => `[[skill:${skillId}:${label}]]`;

View File

@@ -0,0 +1,10 @@
import {
AI_MODEL_TIERS,
type AiModelTier,
} from '../constants/ai-model-tier.const';
import { AUTO_SELECT_MODEL_ID_BY_TIER } from '../constants/auto-select-model-id-by-tier.const';
export const getAiModelTierFromModelId = (
modelId: string | null | undefined,
): AiModelTier | undefined =>
AI_MODEL_TIERS.find((tier) => AUTO_SELECT_MODEL_ID_BY_TIER[tier] === modelId);

View File

@@ -0,0 +1,12 @@
import {
AI_SDK_PACKAGES,
type AiSdkPackage,
} from '../constants/ai-sdk-packages.const';
// models.dev names a vendor the way the AI SDK names its package for most of
// the ones we bundle, so a vendor we have a package for is served by it and
// everything else goes through the compatible shim.
export const inferAiSdkPackage = (providerId: string): AiSdkPackage =>
(AI_SDK_PACKAGES as readonly string[]).includes(`@ai-sdk/${providerId}`)
? (`@ai-sdk/${providerId}` as AiSdkPackage)
: '@ai-sdk/openai-compatible';

View File

@@ -0,0 +1,7 @@
import {
AI_MODEL_EFFORTS,
type AiModelEffort,
} from '../constants/ai-model-effort.const';
export const isAiModelEffort = (value: string): value is AiModelEffort =>
(AI_MODEL_EFFORTS as readonly string[]).includes(value);

View File

@@ -0,0 +1,8 @@
import {
AI_MODEL_TIERS,
type AiModelTier,
} from '../constants/ai-model-tier.const';
export const isAiModelTier = (value: unknown): value is AiModelTier =>
typeof value === 'string' &&
(AI_MODEL_TIERS as readonly string[]).includes(value);

View File

@@ -0,0 +1,7 @@
import {
AI_SDK_PACKAGES,
type AiSdkPackage,
} from '../constants/ai-sdk-packages.const';
export const isAiSdkPackage = (value: string): value is AiSdkPackage =>
(AI_SDK_PACKAGES as readonly string[]).includes(value);

View File

@@ -0,0 +1,10 @@
import { getToolName, isToolUIPart, type ToolUIPart } from 'ai';
import { COMPLETE_WORKSPACE_SETUP_TOOL_NAME } from '../constants/complete-workspace-setup-tool-name.const';
import { type ExtendedUIMessagePart } from '../types/ExtendedUIMessagePart';
export const isCompleteWorkspaceSetupToolPart = (
part: ExtendedUIMessagePart,
): part is ToolUIPart =>
isToolUIPart(part) &&
getToolName(part) === COMPLETE_WORKSPACE_SETUP_TOOL_NAME;

View File

@@ -0,0 +1,7 @@
import {
DATA_RESIDENCY_KEYS,
type DataResidency,
} from '../constants/data-residency.const';
export const isDataResidency = (value: string): value is DataResidency =>
(DATA_RESIDENCY_KEYS as readonly string[]).includes(value);

View File

@@ -0,0 +1,21 @@
import { type ToolUIPart } from 'ai';
import { type ExtendedUIMessagePart } from '../types/ExtendedUIMessagePart';
import { isCompleteWorkspaceSetupToolPart } from './is-complete-workspace-setup-tool-part.util';
export const isSucceededCompleteWorkspaceSetupToolPart = (
part: ExtendedUIMessagePart,
): part is ToolUIPart => {
if (!isCompleteWorkspaceSetupToolPart(part)) {
return false;
}
const output: unknown = part.output;
return (
typeof output === 'object' &&
output !== null &&
'success' in output &&
output.success === true
);
};

View File

@@ -0,0 +1,5 @@
const AGENT_RESPONSE_SCHEMA_PROPERTY_KEY_PATTERN = /^[a-zA-Z0-9_.-]{1,64}$/;
export const isValidAgentResponseSchemaPropertyKey = (
propertyKey: string,
): boolean => AGENT_RESPONSE_SCHEMA_PROPERTY_KEY_PATTERN.test(propertyKey);

View File

@@ -0,0 +1,24 @@
import { type AiModelEffort } from '../constants/ai-model-effort.const';
import { isAiModelEffort } from './is-ai-model-effort.util';
const EFFORT_SEPARATOR = '@';
// Catalog model names never contain `@` (Bedrock ids use `:`), so the last one
// delimits the effort. Any other suffix is part of the id itself.
export const parseAiModelVariantId = (
modelId: string,
): { modelId: string; effort?: AiModelEffort } => {
const separatorIndex = modelId.lastIndexOf(EFFORT_SEPARATOR);
if (separatorIndex === -1) {
return { modelId };
}
const effort = modelId.slice(separatorIndex + 1);
if (!isAiModelEffort(effort)) {
return { modelId };
}
return { modelId: modelId.slice(0, separatorIndex), effort };
};

View File

@@ -0,0 +1,640 @@
import { normalizePageLayoutTabManifest } from '../normalizePageLayoutTabManifest';
import {
type PageLayoutTabManifest,
type PageLayoutWidgetManifest,
} from '../pageLayoutManifestType';
import {
PageLayoutTabLayoutMode,
PageLayoutType,
PageLayoutWidgetVerticalListHeightBehavior,
} from '../../types';
import { DEFAULT_WIDGET_SIZE } from '../../constants';
const widget: PageLayoutWidgetManifest = {
universalIdentifier: 'widget',
title: 'App',
type: 'FRONT_COMPONENT',
configuration: {
configurationType: 'FRONT_COMPONENT',
frontComponentUniversalIdentifier: 'component',
},
};
const tab: PageLayoutTabManifest = {
universalIdentifier: 'tab',
title: 'Details',
position: 0,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
widgets: [widget],
};
describe('normalizePageLayoutTabManifest', () => {
describe.each(['UNSUPPORTED', '', 42, {}])(
'with invalid layoutMode %s in JSON input',
(layoutMode) => {
it.each([{ widgets: [] }, { widgets: [widget] }])(
'returns a validation error with widgets $widgets',
({ widgets }) => {
const pageLayoutTabManifest = JSON.parse(
JSON.stringify({ ...tab, layoutMode, widgets }),
);
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest,
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
`Page layout tab "Details" defines unsupported layoutMode "${layoutMode}". Expected GRID, VERTICAL_LIST or CANVAS.`,
],
});
},
);
},
);
it('derives omitted positions and preserves explicit indices without changing its input', () => {
const input: PageLayoutTabManifest = {
...tab,
widgets: [
{ ...widget, heightBehavior: 'FIT_CONTENT' as const },
{
...widget,
universalIdentifier: 'second-widget',
heightBehavior: 'TAB_VIEWPORT' as const,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 99,
heightBehavior:
PageLayoutWidgetVerticalListHeightBehavior.FIT_CONTENT,
},
},
],
};
const original = JSON.stringify(input);
const result = normalizePageLayoutTabManifest({
pageLayoutTabManifest: input,
pageLayoutType: 'RECORD_PAGE',
});
expect(result).toEqual({
status: 'success',
pageLayoutTab: {
...tab,
widgets: [
{
...widget,
position: {
layoutMode: 'VERTICAL_LIST',
index: 0,
heightBehavior: 'FIT_CONTENT',
},
},
{
...widget,
universalIdentifier: 'second-widget',
position: {
layoutMode: 'VERTICAL_LIST',
index: 99,
heightBehavior: 'TAB_VIEWPORT',
},
},
],
},
});
expect(JSON.stringify(input)).toBe(original);
});
it('preserves a legacy nested height behavior and index', () => {
const result = normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
widgets: [
{
...widget,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 99,
heightBehavior:
PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
},
},
],
},
pageLayoutType: undefined,
});
expect(result).toMatchObject({
status: 'success',
pageLayoutTab: {
widgets: [
{
position: {
layoutMode: 'VERTICAL_LIST',
index: 99,
heightBehavior: 'TAB_VIEWPORT',
},
},
],
},
});
});
it('uses explicit indices rather than array order for legacy viewport placement', () => {
const pageLayoutTabManifest: PageLayoutTabManifest = {
...tab,
widgets: [
{
...widget,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 99,
heightBehavior:
PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
},
},
{
...widget,
universalIdentifier: 'second-widget',
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 2,
},
},
],
};
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest,
pageLayoutType: undefined,
}),
).toEqual({
status: 'success',
pageLayoutTab: pageLayoutTabManifest,
});
});
it('keeps omitted height behavior implicit', () => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: tab,
pageLayoutType: undefined,
}),
).toEqual({
status: 'success',
pageLayoutTab: {
...tab,
widgets: [
{ ...widget, position: { layoutMode: 'VERTICAL_LIST', index: 0 } },
],
},
});
});
describe.each<{ name: string; viewport: Partial<PageLayoutWidgetManifest> }>([
{ name: 'explicit height', viewport: { heightBehavior: 'TAB_VIEWPORT' } },
{
name: 'legacy nested height',
viewport: {
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
heightBehavior:
PageLayoutWidgetVerticalListHeightBehavior.TAB_VIEWPORT,
},
},
},
{
name: 'widget type default',
viewport: {
type: 'TIMELINE',
configuration: { configurationType: 'TIMELINE' },
},
},
])('viewport constraints with $name', ({ viewport }) => {
it('rejects a fit-content sibling with the same explicit index', () => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
widgets: [
{
...widget,
universalIdentifier: 'fit-content-widget',
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
},
{
...widget,
...viewport,
position: {
...viewport.position,
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
},
],
},
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
'Page layout tab "Details" must place its TAB_VIEWPORT widget last.',
],
});
});
it.each([
{ duplicate: true, error: 'can contain only one TAB_VIEWPORT widget' },
{ duplicate: false, error: 'must place its TAB_VIEWPORT widget last' },
])('rejects a layout that $error', ({ duplicate, error }) => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
widgets: [
{ ...widget, ...viewport },
{
...widget,
...(duplicate ? viewport : {}),
universalIdentifier: 'second-widget',
},
],
},
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [`Page layout tab "Details" ${error}.`],
});
});
});
it('accepts an explicit fit-content override followed by a default viewport widget', () => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
widgets: [
{
...widget,
type: 'TIMELINE',
configuration: { configurationType: 'TIMELINE' },
heightBehavior: 'FIT_CONTENT',
},
{
...widget,
universalIdentifier: 'second-widget',
type: 'TIMELINE',
configuration: { configurationType: 'TIMELINE' },
},
],
},
pageLayoutType: undefined,
}),
).toMatchObject({ status: 'success' });
});
it.each([PageLayoutTabLayoutMode.GRID, PageLayoutTabLayoutMode.CANVAS])(
'rejects a vertical-list position in an authored %s tab',
(layoutMode) => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode,
widgets: [
{
...widget,
position: {
layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST,
index: 0,
},
},
{ ...widget, universalIdentifier: 'second-widget' },
],
},
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
`Page layout widget "App" uses a VERTICAL_LIST position, but its parent tab "Details" uses ${layoutMode}.`,
],
});
},
);
it('rejects a conflicting grid position instead of changing its layout', () => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
widgets: [
{
...widget,
position: {
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 12,
},
},
],
},
pageLayoutType: undefined,
}),
).toMatchObject({
status: 'fail',
errors: [
'Page layout widget "App" uses a GRID position, but its parent tab "Details" uses VERTICAL_LIST.',
],
});
});
it.each([
undefined,
PageLayoutType.DASHBOARD,
PageLayoutType.RECORD_PAGE,
PageLayoutType.STANDALONE_PAGE,
])('resolves omitted layout mode for page type %s', (pageLayoutType) => {
const result = normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode: undefined,
widgets: undefined,
},
pageLayoutType,
});
expect(result).toMatchObject({
status: 'success',
pageLayoutTab: {
layoutMode:
pageLayoutType === PageLayoutType.STANDALONE_PAGE
? 'VERTICAL_LIST'
: 'GRID',
widgets: [],
},
});
});
it('keeps an explicit grid mode and supplies its default position', () => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode: PageLayoutTabLayoutMode.GRID,
},
pageLayoutType: 'STANDALONE_PAGE',
}),
).toMatchObject({
status: 'success',
pageLayoutTab: {
layoutMode: 'GRID',
widgets: [
{
position: {
layoutMode: 'GRID',
row: 0,
column: 0,
rowSpan: DEFAULT_WIDGET_SIZE.default.h,
columnSpan: DEFAULT_WIDGET_SIZE.default.w,
},
},
],
},
});
});
it.each(['position', 'gridPosition'])(
'preserves legacy grid coordinates in %s',
(positionKey) => {
const gridPosition = { row: 2, column: 6, rowSpan: 4, columnSpan: 6 };
const result = normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode: PageLayoutTabLayoutMode.GRID,
widgets: [
{
...widget,
[positionKey]:
positionKey === 'position'
? {
layoutMode: PageLayoutTabLayoutMode.GRID,
...gridPosition,
}
: gridPosition,
},
],
},
pageLayoutType: undefined,
});
expect(result).toEqual({
status: 'success',
pageLayoutTab: {
...tab,
layoutMode: 'GRID',
widgets: [
{ ...widget, position: { layoutMode: 'GRID', ...gridPosition } },
],
},
});
},
);
it.each<PageLayoutWidgetManifest['position']>([
{
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 1,
column: 2,
rowSpan: 3,
columnSpan: 4,
},
{ layoutMode: PageLayoutTabLayoutMode.VERTICAL_LIST, index: 0 },
])(
'rejects a conflicting position %j on a single-widget Canvas tab',
(position) => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [{ ...widget, position }],
},
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
`Page layout widget "App" uses a ${position?.layoutMode} position, but its parent tab "Details" uses CANVAS.`,
],
});
},
);
it.each<PageLayoutWidgetManifest['position']>([
undefined,
{ layoutMode: PageLayoutTabLayoutMode.CANVAS },
])('preserves single-widget Canvas with position %s', (position) => {
const result = normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: [{ ...widget, position }],
},
pageLayoutType: undefined,
});
expect(result).toMatchObject({
status: 'success',
pageLayoutTab: {
layoutMode: 'CANVAS',
widgets: [
{
position: {
layoutMode: 'CANVAS',
},
},
],
},
});
});
it.each([0, 2])(
'keeps a Canvas tab with %i widgets unchanged',
(widgetCount) => {
const result = normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode: PageLayoutTabLayoutMode.CANVAS,
widgets: Array.from({ length: widgetCount }, (_, index) => ({
...widget,
universalIdentifier: `widget-${index}`,
})),
},
pageLayoutType: undefined,
});
expect(result).toMatchObject({
status: 'success',
pageLayoutTab: {
layoutMode: 'CANVAS',
widgets: Array.from({ length: widgetCount }, () => ({
position: { layoutMode: 'CANVAS' },
})),
},
});
},
);
it.each([PageLayoutTabLayoutMode.GRID, PageLayoutTabLayoutMode.CANVAS])(
'rejects heightBehavior on authored %s before normalization',
(layoutMode) => {
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest: {
...tab,
layoutMode,
widgets: [{ ...widget, heightBehavior: 'TAB_VIEWPORT' }],
},
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
`Page layout widget "App" defines heightBehavior, but its parent tab "Details" uses ${layoutMode}. heightBehavior is only supported for VERTICAL_LIST tabs.`,
],
});
},
);
it.each(['TAB_VIEPORT', '', 42, {}])(
'rejects invalid heightBehavior %s in JSON input',
(heightBehavior) => {
const pageLayoutTabManifest = JSON.parse(
JSON.stringify({ ...tab, widgets: [{ ...widget, heightBehavior }] }),
);
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest,
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
`Page layout widget "App" defines unsupported heightBehavior "${heightBehavior}". Expected FIT_CONTENT or TAB_VIEWPORT.`,
],
});
},
);
it.each([
{
layoutMode: PageLayoutTabLayoutMode.GRID,
row: 0,
column: 0,
rowSpan: 1,
columnSpan: 1,
},
{ layoutMode: PageLayoutTabLayoutMode.CANVAS },
])(
'rejects nested heightBehavior in $layoutMode JSON positions',
(position) => {
const pageLayoutTabManifest = JSON.parse(
JSON.stringify({
...tab,
layoutMode: position.layoutMode,
widgets: [
{
...widget,
position: { ...position, heightBehavior: 'TAB_VIEWPORT' },
},
],
}),
);
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest,
pageLayoutType: undefined,
}),
).toEqual({
status: 'fail',
errors: [
`Page layout widget "App" defines heightBehavior, but its parent tab "Details" uses ${position.layoutMode}. heightBehavior is only supported for VERTICAL_LIST tabs.`,
],
});
},
);
it('rejects an invalid nested height behavior', () => {
const pageLayoutTabManifest = JSON.parse(
JSON.stringify({
...tab,
widgets: [
{
...widget,
position: {
layoutMode: 'VERTICAL_LIST',
index: 0,
heightBehavior: 'INVALID',
},
},
],
}),
);
expect(
normalizePageLayoutTabManifest({
pageLayoutTabManifest,
pageLayoutType: undefined,
}),
).toMatchObject({
status: 'fail',
errors: [expect.stringContaining('unsupported heightBehavior "INVALID"')],
});
});
});

View File

@@ -0,0 +1,13 @@
import { type AgentResponseFormat } from '@/ai/types/agent-response-format.type';
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
export type AgentManifest = SyncableEntityOptions & {
name: string;
label: string;
icon?: string;
description?: string;
prompt: string;
modelId?: string;
responseFormat?: AgentResponseFormat;
roleUniversalIdentifier?: string;
};

View File

@@ -0,0 +1,46 @@
// Wire shape exchanged between an app's logic-function runtime and the
// `/apps/connections/list` and `/apps/connections/get` endpoints.
//
// Lives in twenty-shared so both sides — the SDK helpers (`listConnections`,
// `getConnection`) and the server controller / DTO — pick from one source
// of truth. Adding a field on either side without updating this type will
// fail typecheck.
export type AppConnection = {
id: string;
// The app-developer-facing provider name (e.g. "linear"), as declared on
// `defineConnectionProvider({ name })`. Useful when listing across providers
// without a filter.
providerName: string;
// User-given (or auto-derived from the OAuth handle) display label.
// Falls back to `handle` when the user never set one. Suitable for logs
// and end-user UI.
name: string;
// OAuth-derived identifier (typically email or login). Stays stable across
// reconnects of the same upstream account.
handle: string;
// Connection-row visibility:
// 'user' = visible only to the user who created it.
// 'workspace' = visible to every user in the workspace.
// Named `visibility` (not `scope`) to disambiguate from the `scopes`
// array below, which is the unrelated set of OAuth permissions granted
// by the upstream provider.
visibility: 'user' | 'workspace';
// The userWorkspace that originally created the credential (also the owner
// for `scope: 'user'` credentials). Match against `event.userWorkspaceId`
// to resolve the request user's connection.
userWorkspaceId: string;
// The workspace member who created the credential. null when the creating
// user is no longer a member of the workspace.
workspaceMemberId: string | null;
accessToken: string;
// OAuth scopes actually granted by the upstream provider on the most recent
// token issuance (may be a subset of what the app requested).
scopes: string[];
// Set when the most recent refresh attempt failed permanently
// (4xx invalid_grant), or when the app reported the credential dead via
// `reportConnectionAuthFailure`; the user must reconnect from the app's
// settings tab. Apps should surface this so users know to take action.
authFailedAt: string | null;
// Human-readable explanation for `authFailedAt`, when the reporter gave one.
authFailedReason: string | null;
};

View File

@@ -0,0 +1 @@
export type AppKeyValueScope = 'WORKSPACE' | 'SERVER';

View File

@@ -0,0 +1,7 @@
import { type AppKeyValueScope } from '@/application/appKeyValueScopeType';
export type AppKeyValue = {
key: string;
value: unknown;
scope: AppKeyValueScope;
};

View File

@@ -0,0 +1,29 @@
import { type MessageChannelVisibility } from '../types/MessageChannelVisibility';
// One messaging channel an app owns, bound to one of its connections.
// Returned from `listMessageChannels` and `createMessageChannel`.
//
// The shape lives in twenty-shared so this and the server-side
// `MessageChannelDTO` projection always agree.
export type AppMessageChannel = {
id: string;
// The account's identity on the provider, as the provider names it (a
// LinkedIn member URN, a WhatsApp number). Opaque to Twenty: it is what
// the app matches inbound payloads against, and what participant handles
// on this channel are compared to when deciding message direction.
handle: string;
// End-user label for the channel. Falls back to `handle` in the UI when null.
displayName: string | null;
// How much of a message on this channel other workspace members may read:
// 'METADATA' = participants and dates only.
// 'SUBJECT' = the above plus the subject.
// 'SHARE_EVERYTHING' = the above plus the body.
// The owner of the underlying connection always reads their own messages
// in full regardless of this value.
visibility: MessageChannelVisibility;
// The app connection whose credential this channel speaks through.
connectedAccountId: string;
// False pauses the channel without deleting it: ingestion is rejected
// while it is off, and existing messages stay readable.
isSyncEnabled: boolean;
};

View File

@@ -0,0 +1,73 @@
import { type BillableOperations } from '@/application/billableOperationsType';
export const RECURRING_CHARGE_PERIODS = ['MONTH'] as const;
export type RecurringChargePeriod = (typeof RECURRING_CHARGE_PERIODS)[number];
export const isRecurringChargePeriod = (
value: unknown,
): value is RecurringChargePeriod =>
RECURRING_CHARGE_PERIODS.includes(value as RecurringChargePeriod);
// Omitted for a flat fee. WORKSPACE_MEMBER multiplies the amount by the number
// of members in the workspace at the moment the period is charged.
export const RECURRING_CHARGE_UNITS = ['WORKSPACE_MEMBER'] as const;
export type RecurringChargeUnit = (typeof RECURRING_CHARGE_UNITS)[number];
export const isRecurringChargeUnit = (
value: unknown,
): value is RecurringChargeUnit =>
RECURRING_CHARGE_UNITS.includes(value as RecurringChargeUnit);
export type RecurringCharge = {
period: RecurringChargePeriod;
amountMicroCredits: number;
per?: RecurringChargeUnit;
label: string;
};
// 1 USD = 1_000_000 micro-credits. Unlike a chargeCredits call, which the app
// makes and the server validates per request, a recurring charge is raised by
// the platform from a jsonb column, so these bound what the platform is willing
// to debit on an app's behalf. PER_UNIT bounds the declared rate, which a
// WORKSPACE_MEMBER charge multiplies by the member count, and PER_PERIOD bounds
// the resulting total.
export const MAX_RECURRING_CHARGE_MICRO_CREDITS_PER_UNIT = 100_000_000;
export const MAX_RECURRING_CHARGE_MICRO_CREDITS_PER_PERIOD = 1_000_000_000;
export const isRecurringChargeAmount = (value: unknown): value is number =>
typeof value === 'number' &&
Number.isSafeInteger(value) &&
value > 0 &&
value <= MAX_RECURRING_CHARGE_MICRO_CREDITS_PER_UNIT;
// The declaration reaches the raise path as untrusted jsonb, so its shape is
// checked at runtime rather than trusted from the RecurringCharge type.
export const isRecurringCharge = (value: unknown): value is RecurringCharge => {
if (typeof value !== 'object' || value === null) {
return false;
}
const { period, amountMicroCredits, per, label } =
value as Partial<RecurringCharge>;
return (
isRecurringChargePeriod(period) &&
isRecurringChargeAmount(amountMicroCredits) &&
(per === undefined || isRecurringChargeUnit(per)) &&
typeof label === 'string' &&
label.trim().length > 0
);
};
// Keyed by a name the application chooses. Unlike operations, the platform
// raises these itself once per billing period, so the app declares the amount
// rather than calling chargeCredits.
export type RecurringCharges = Partial<Record<string, RecurringCharge>>;
export type ApplicationBilling = {
description?: string;
recurring?: RecurringCharges;
operations?: BillableOperations;
};

View File

@@ -0,0 +1,20 @@
export const APPLICATION_CATEGORIES = [
'Communication',
'Productivity',
'Product management',
'Sales',
'Marketing',
'Enrichment',
'Data',
'Search',
'Other',
] as const;
export type KnownApplicationCategory = (typeof APPLICATION_CATEGORIES)[number];
export type ApplicationCategory = KnownApplicationCategory | (string & {});
export const isKnownApplicationCategory = (
category: string,
): category is KnownApplicationCategory =>
(APPLICATION_CATEGORIES as readonly string[]).includes(category);

View File

@@ -0,0 +1,49 @@
import { type PostInstallLogicFunctionApplicationManifest } from '@/application/postInstallLogicFunctionApplicationType';
import { type PreInstallLogicFunctionApplicationManifest } from '@/application/preInstallLogicFunctionApplicationType';
import { type SettingsFrontComponentApplicationManifest } from '@/application/settingsFrontComponentApplicationType';
import { type UninstallLogicFunctionApplicationManifest } from '@/application/uninstallLogicFunctionApplicationType';
import { type FrontComponentSharedDependenciesManifest } from '@/application/frontComponentSharedDependenciesManifestType';
import { type ApplicationBilling } from './applicationBillingType';
import { type ApplicationCategory } from './applicationCategoryType';
import { type ApplicationVariables } from './applicationVariablesType';
import { type ServerVariables } from './server-variables.type';
import { type SyncableEntityOptions } from './syncableEntityOptionsType';
export type ApplicationManifest = SyncableEntityOptions & {
defaultRoleUniversalIdentifier: string;
displayName: string;
description: string;
applicationVariables?: ApplicationVariables;
billing?: ApplicationBilling;
serverVariables?: ServerVariables;
author?: string;
category?: ApplicationCategory;
/**
* @deprecated Use `logo` instead.
*/
logoUrl?: string;
logo?: string;
/**
* @deprecated Use `galleryImages` instead.
*/
screenshots?: string[];
galleryImages?: string[];
aboutDescription?: string;
websiteUrl?: string;
termsUrl?: string;
emailSupport?: string;
issueReportUrl?: string;
postInstallLogicFunction?: PostInstallLogicFunctionApplicationManifest;
preInstallLogicFunction?: PreInstallLogicFunctionApplicationManifest;
uninstallLogicFunction?: UninstallLogicFunctionApplicationManifest;
settingsFrontComponent?: SettingsFrontComponentApplicationManifest;
frontComponentSharedDependencies?: FrontComponentSharedDependenciesManifest;
/**
* @deprecated Use `defineSettingsFrontComponent()` (exposed on the manifest
* as `settingsFrontComponent`) instead. This property is ignored.
*/
settingsCustomTabFrontComponentUniversalIdentifier?: string;
packageJsonChecksum: string | null;
yarnLockChecksum: string | null;
requiredServerVersionRange?: string | null;
};

View File

@@ -0,0 +1,58 @@
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
import { FieldMetadataType } from '@/types/FieldMetadataType';
export const APPLICATION_VARIABLE_FIELD_METADATA_TYPES = [
FieldMetadataType.TEXT,
FieldMetadataType.ARRAY,
FieldMetadataType.BOOLEAN,
FieldMetadataType.DATE,
FieldMetadataType.DATE_TIME,
FieldMetadataType.NUMBER,
FieldMetadataType.NUMERIC,
FieldMetadataType.RAW_JSON,
FieldMetadataType.RICH_TEXT,
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
] as const;
export type ApplicationVariableType =
(typeof APPLICATION_VARIABLE_FIELD_METADATA_TYPES)[number];
export type ApplicationVariableOption = {
label: string;
value: string;
};
export type ApplicationVariableValue =
| string
| number
| boolean
| string[]
| Record<string, unknown>
| null;
type TypedApplicationVariable = {
label?: string;
type?: ApplicationVariableType;
options?: ApplicationVariableOption[];
isDeprecated?: boolean;
};
type SecretApplicationVariable = SyncableEntityOptions &
TypedApplicationVariable & {
description?: string;
isSecret: true;
};
type NonSecretApplicationVariable = SyncableEntityOptions &
TypedApplicationVariable & {
value?: ApplicationVariableValue;
description?: string;
isSecret?: false;
};
export type ApplicationVariable =
| SecretApplicationVariable
| NonSecretApplicationVariable;
export type ApplicationVariables = Record<string, ApplicationVariable>;

View File

@@ -0,0 +1,6 @@
export type AssetManifest = {
filePath: string;
fileName: string;
fileType: string;
checksum: string | null;
};

View File

@@ -0,0 +1,11 @@
import { type UsageOperationTypeValue } from '@/application/usageOperationTypesType';
export type BillableOperationManifest = {
operationType: UsageOperationTypeValue;
label: string;
};
// Keyed by the operation name the application charges against.
export type BillableOperations = Partial<
Record<string, BillableOperationManifest>
>;

View File

@@ -0,0 +1,12 @@
import { type OAuthConnectionProviderConfig } from '@/application/oauthConnectionProviderConfigType';
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
export type ConnectionProviderManifest = SyncableEntityOptions & {
name: string;
displayName: string;
logo?: string;
type: 'oauth';
oauth: OAuthConnectionProviderConfig;
onConnectLogicFunction?: SyncableEntityOptions;
onDisconnectLogicFunction?: SyncableEntityOptions;
};

View File

@@ -0,0 +1,12 @@
// Discriminator on `connectionProvider.type`. Each value pairs with a
// typed sub-config column on the entity (today only `oauthConfig`).
//
// Anticipated future values:
// - 'apiKey' — single long-lived token in an applicationVariable
// - 'pat' — per-user personal access token entered at connect time
// - 'basicAuth' — username + password
//
// Adding a value is purely additive: define the new sub-config column on
// the entity, the manifest type, and the SDK; existing 'oauth' callers
// keep working.
export type ConnectionProviderType = 'oauth';

View File

@@ -0,0 +1 @@
export const APPLICATION_FILE_UPLOAD_BATCH_SIZE = 100;

View File

@@ -0,0 +1 @@
export const ASSETS_DIR = 'public';

View File

@@ -0,0 +1 @@
export const DEFAULT_API_KEY_NAME = 'TWENTY_API_KEY';

View File

@@ -0,0 +1 @@
export const DEFAULT_API_URL_NAME = 'TWENTY_API_URL';

View File

@@ -0,0 +1 @@
export const DEFAULT_APP_ACCESS_TOKEN_NAME = 'TWENTY_APP_ACCESS_TOKEN';

View File

@@ -0,0 +1,2 @@
export const DEFAULT_APP_APPLICATION_ACCESS_TOKEN_NAME =
'TWENTY_APP_APPLICATION_ACCESS_TOKEN';

View File

@@ -0,0 +1 @@
export const DEFAULT_FUNCTIONS_URL_NAME = 'TWENTY_FUNCTIONS_URL';

View File

@@ -0,0 +1,2 @@
export const FRONT_COMPONENT_SHARED_DEPENDENCIES_BUILT_PATH =
'front-component-shared-dependencies.mjs';

View File

@@ -0,0 +1,2 @@
export const FRONT_COMPONENT_SHARED_DEPENDENCIES_IMPORT_SPECIFIER =
'twenty:front-component-shared-dependencies';

View File

@@ -0,0 +1 @@
export const GENERATED_DIR = 'generated';

View File

@@ -0,0 +1,3 @@
export const NODE_ESM_CJS_BANNER = {
js: "import { createRequire as __createRequire } from 'module';\nconst require = __createRequire(import.meta.url);",
};

View File

@@ -0,0 +1 @@
export const OUTPUT_DIR = '.twenty/output';

View File

@@ -0,0 +1 @@
export const TWENTY_STANDARD_APPLICATION_NAME = 'Standard';

View File

@@ -0,0 +1,2 @@
export const TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER =
'20202020-64aa-4b6f-b003-9c74b97cee20';

View File

@@ -0,0 +1,17 @@
export const CREDIT_UNAVAILABLE_REASONS = [
'workspace-suspended',
'no-subscription',
'no-credits',
] as const;
export type CreditUnavailableReason =
(typeof CREDIT_UNAVAILABLE_REASONS)[number];
// What an application is told about its workspace's ability to spend. Mirrors
// the gate the platform already applies to AI, workflows and email, so an app
// can make the same decision up front instead of discovering the refusal
// through a downstream call it did not write. Carries no balance: an app learns
// whether it may spend, not how much the workspace has or what it pays.
export type CreditAvailability =
| { hasAvailableCredits: true }
| { hasAvailableCredits: false; reason: CreditUnavailableReason };

View File

@@ -0,0 +1,14 @@
import { getAgentUniversalIdentifier } from '@/application/deterministic-identifier/get-agent-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getAgentUniversalIdentifier', () => {
it('derives a deterministic id from the agent name within its application', () => {
expect(
getAgentUniversalIdentifier({
applicationUniversalIdentifier: APP,
name: 'My Agent',
}),
).toBe('3d16dbdf-98c9-59c6-b770-81161144fc78');
});
});

View File

@@ -0,0 +1,14 @@
import { getApplicationVariableUniversalIdentifier } from '@/application/deterministic-identifier/get-application-variable-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getApplicationVariableUniversalIdentifier', () => {
it('derives a deterministic id from the variable key within its application', () => {
expect(
getApplicationVariableUniversalIdentifier({
applicationUniversalIdentifier: APP,
key: 'API_KEY',
}),
).toBe('83da1579-61d6-55f5-8b6c-c6ef698608bf');
});
});

View File

@@ -0,0 +1,14 @@
import { getConnectionProviderUniversalIdentifier } from '@/application/deterministic-identifier/get-connection-provider-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getConnectionProviderUniversalIdentifier', () => {
it('derives a deterministic id from the provider name within its application', () => {
expect(
getConnectionProviderUniversalIdentifier({
applicationUniversalIdentifier: APP,
name: 'Gmail',
}),
).toBe('69c2371f-465c-57cd-9d36-3fbba8ea8510');
});
});

View File

@@ -0,0 +1,17 @@
import { getFieldPermissionUniversalIdentifier } from '@/application/deterministic-identifier/get-field-permission-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const ROLE = '55555555-5555-4555-8555-555555555555';
const FIELD = '33333333-3333-4333-8333-333333333333';
describe('getFieldPermissionUniversalIdentifier', () => {
it('derives a deterministic id from the field within its role', () => {
expect(
getFieldPermissionUniversalIdentifier({
applicationUniversalIdentifier: APP,
roleUniversalIdentifier: ROLE,
fieldUniversalIdentifier: FIELD,
}),
).toBe('d94e301d-96c3-559b-a92c-a1f0dbc218e7');
});
});

View File

@@ -0,0 +1,16 @@
import { getFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-field-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const OBJECT = '22222222-2222-4222-8222-222222222222';
describe('getFieldUniversalIdentifier', () => {
it('derives a deterministic id from the field name within its object', () => {
expect(
getFieldUniversalIdentifier({
applicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
name: 'createdAt',
}),
).toBe('b5cc95b6-e78f-531f-b74e-25bf891e8ec7');
});
});

View File

@@ -0,0 +1,14 @@
import { getFrontComponentUniversalIdentifier } from '@/application/deterministic-identifier/get-front-component-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getFrontComponentUniversalIdentifier', () => {
it('derives a deterministic id from the component name within its application', () => {
expect(
getFrontComponentUniversalIdentifier({
applicationUniversalIdentifier: APP,
componentName: 'HelloWorld',
}),
).toBe('a81d9442-d9ff-557b-b5b7-494b9c629b23');
});
});

View File

@@ -0,0 +1,35 @@
import { getIndexFieldUniversalIdentifier } from '@/application/deterministic-identifier/get-index-field-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const INDEX = '33333333-3333-4333-8333-333333333333';
const FIELD = '55555555-5555-4555-8555-555555555555';
describe('getIndexFieldUniversalIdentifier', () => {
it('derives a deterministic id from the field and sub-field within its index', () => {
expect(
getIndexFieldUniversalIdentifier({
applicationUniversalIdentifier: APP,
indexUniversalIdentifier: INDEX,
fieldUniversalIdentifier: FIELD,
subFieldName: 'addressCity',
}),
).toBe('221a0c4c-6e1a-5f2f-9f78-6fda4027e1f4');
});
it('treats a missing sub-field like an empty one', () => {
expect(
getIndexFieldUniversalIdentifier({
applicationUniversalIdentifier: APP,
indexUniversalIdentifier: INDEX,
fieldUniversalIdentifier: FIELD,
subFieldName: null,
}),
).toBe(
getIndexFieldUniversalIdentifier({
applicationUniversalIdentifier: APP,
indexUniversalIdentifier: INDEX,
fieldUniversalIdentifier: FIELD,
}),
);
});
});

View File

@@ -0,0 +1,16 @@
import { getIndexUniversalIdentifier } from '@/application/deterministic-identifier/get-index-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const OBJECT = '22222222-2222-4222-8222-222222222222';
describe('getIndexUniversalIdentifier', () => {
it('derives a deterministic id from the generated index name within its object', () => {
expect(
getIndexUniversalIdentifier({
applicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
name: 'IDX_abc',
}),
).toBe('f3668ea9-452b-514f-905f-07375db25906');
});
});

View File

@@ -0,0 +1,14 @@
import { getLogicFunctionUniversalIdentifier } from '@/application/deterministic-identifier/get-logic-function-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getLogicFunctionUniversalIdentifier', () => {
it('derives a deterministic id from the function name within its application', () => {
expect(
getLogicFunctionUniversalIdentifier({
applicationUniversalIdentifier: APP,
name: 'sendEmail',
}),
).toBe('4a470da0-dd48-5b7f-a1be-4e24fcc5b6c5');
});
});

View File

@@ -0,0 +1,54 @@
import {
getFolderNavigationMenuItemUniversalIdentifier,
getLinkNavigationMenuItemUniversalIdentifier,
getObjectNavigationMenuItemUniversalIdentifier,
getViewNavigationMenuItemUniversalIdentifier,
} from '@/application/deterministic-identifier/get-navigation-menu-item-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const OBJECT = '22222222-2222-4222-8222-222222222222';
const VIEW = '44444444-4444-4444-8444-444444444444';
describe('getFolderNavigationMenuItemUniversalIdentifier', () => {
it('derives a deterministic id from the folder name within its application', () => {
expect(
getFolderNavigationMenuItemUniversalIdentifier({
applicationUniversalIdentifier: APP,
name: 'Workflows',
}),
).toBe('ca043c51-ac75-515d-902b-dd836626bba4');
});
});
describe('getObjectNavigationMenuItemUniversalIdentifier', () => {
it('derives a deterministic id from the object it targets', () => {
expect(
getObjectNavigationMenuItemUniversalIdentifier({
applicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
}),
).toBe('071361bd-47c8-5513-be33-998dd287f8c7');
});
});
describe('getViewNavigationMenuItemUniversalIdentifier', () => {
it('derives a deterministic id from the view it targets', () => {
expect(
getViewNavigationMenuItemUniversalIdentifier({
applicationUniversalIdentifier: APP,
viewUniversalIdentifier: VIEW,
}),
).toBe('6504fd6d-ed38-5f34-87fc-26025130260f');
});
});
describe('getLinkNavigationMenuItemUniversalIdentifier', () => {
it('derives a deterministic id from its target URL', () => {
expect(
getLinkNavigationMenuItemUniversalIdentifier({
applicationUniversalIdentifier: APP,
link: 'https://example.com',
}),
).toBe('635cef97-cc61-53a3-873e-38fecf3ecbd7');
});
});

View File

@@ -0,0 +1,17 @@
import { getObjectPermissionUniversalIdentifier } from '@/application/deterministic-identifier/get-object-permission-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const ROLE = '55555555-5555-4555-8555-555555555555';
const OBJECT = '22222222-2222-4222-8222-222222222222';
describe('getObjectPermissionUniversalIdentifier', () => {
it('derives a deterministic id from the object within its role', () => {
expect(
getObjectPermissionUniversalIdentifier({
applicationUniversalIdentifier: APP,
roleUniversalIdentifier: ROLE,
objectUniversalIdentifier: OBJECT,
}),
).toBe('8d78f0a2-6ad6-5ee1-a859-a33bddd42324');
});
});

View File

@@ -0,0 +1,14 @@
import { getObjectUniversalIdentifier } from '@/application/deterministic-identifier/get-object-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getObjectUniversalIdentifier', () => {
it('derives a deterministic id from the object nameSingular within its application', () => {
expect(
getObjectUniversalIdentifier({
applicationUniversalIdentifier: APP,
nameSingular: 'rocket',
}),
).toBe('676819ee-d700-5936-9d6f-58dd7f71ae07');
});
});

View File

@@ -0,0 +1,37 @@
import { getPageLayoutUniversalIdentifier } from '@/application/deterministic-identifier/get-page-layout-universal-identifier.util';
import { getSystemRecordPageLayoutUniversalIdentifier } from '@/application/deterministic-identifier/get-system-record-page-layout-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
const OBJECT = '22222222-2222-4222-8222-222222222222';
describe('getPageLayoutUniversalIdentifier', () => {
it('derives a deterministic id from the layout name within its object', () => {
expect(
getPageLayoutUniversalIdentifier({
applicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
name: 'My Dashboard',
}),
).toBe('dd8a6bdb-4f8e-57c7-8a34-120d5e6430ae');
});
it('derives a deterministic id from the layout name alone when standalone', () => {
expect(
getPageLayoutUniversalIdentifier({
applicationUniversalIdentifier: APP,
name: 'Standalone',
}),
).toBe('d382c85c-204a-5648-a101-e65575c3a853');
});
});
describe('getSystemRecordPageLayoutUniversalIdentifier', () => {
it('derives a deterministic id from the RECORD_PAGE role within its object', () => {
expect(
getSystemRecordPageLayoutUniversalIdentifier({
objectMetadataApplicationUniversalIdentifier: APP,
objectUniversalIdentifier: OBJECT,
}),
).toBe('80289fa4-d714-5883-bd1d-bc59c7b9a1c2');
});
});

View File

@@ -0,0 +1,14 @@
import { getPermissionFlagUniversalIdentifier } from '@/application/deterministic-identifier/get-permission-flag-universal-identifier.util';
const APP = '11111111-1111-4111-8111-111111111111';
describe('getPermissionFlagUniversalIdentifier', () => {
it('derives a deterministic id from the flag key within its application', () => {
expect(
getPermissionFlagUniversalIdentifier({
applicationUniversalIdentifier: APP,
key: 'WORKFLOWS',
}),
).toBe('e9f8bad8-54da-51f7-b7d7-77ed600a589d');
});
});

Some files were not shown because too many files have changed in this diff Show More