Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82f9a193f3 | |||
| 19bca23043 | |||
| 90fb67c90b | |||
| e75057a1df | |||
| 1b900a5f32 | |||
| 6e913c2f91 | |||
| e676c3fea6 | |||
| 5dfe9b7b49 | |||
| 670d88d373 | |||
| c5af23afc2 | |||
| 2f4cd688fd | |||
| 0ca6325f0d | |||
| 26b7e0a97d | |||
| 868c019089 | |||
| 90658a8476 | |||
| 43678cb782 | |||
| b52ba1b3e3 | |||
| 4f48a5d001 | |||
| a2b727cd7f | |||
| 1d26940960 | |||
| 16aa8fc72a | |||
| cf0ad46587 | |||
| 9cea7c47e9 |
@@ -27,5 +27,5 @@
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "**/*.integration-test.ts"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
env: {
|
||||
NODE_ENV: 'integration',
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -115,6 +115,7 @@ export class CreateAppCommand {
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleSkill: false,
|
||||
includeIntegrationTest: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +128,7 @@ export class CreateAppCommand {
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeIntegrationTest: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,19 +173,32 @@ export class CreateAppCommand {
|
||||
value: 'skill',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Integration test (vitest test verifying app installation)',
|
||||
value: 'integrationTest',
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const includeField = selectedExamples.includes('field');
|
||||
const includeView = selectedExamples.includes('view');
|
||||
const includeIntegrationTest =
|
||||
selectedExamples.includes('integrationTest');
|
||||
const includeObject =
|
||||
selectedExamples.includes('object') || includeField || includeView;
|
||||
selectedExamples.includes('object') ||
|
||||
includeField ||
|
||||
includeView ||
|
||||
includeIntegrationTest;
|
||||
|
||||
if ((includeField || includeView) && !selectedExamples.includes('object')) {
|
||||
if (
|
||||
(includeField || includeView || includeIntegrationTest) &&
|
||||
!selectedExamples.includes('object')
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Note: Example object auto-included because example field/view depends on it.',
|
||||
'Note: Example object auto-included because example field/view/integration test depends on it.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -197,6 +212,7 @@ export class CreateAppCommand {
|
||||
includeExampleNavigationMenuItem:
|
||||
selectedExamples.includes('navigationMenuItem'),
|
||||
includeExampleSkill: selectedExamples.includes('skill'),
|
||||
includeIntegrationTest,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,4 +8,5 @@ export type ExampleOptions = {
|
||||
includeExampleView: boolean;
|
||||
includeExampleNavigationMenuItem: boolean;
|
||||
includeExampleSkill: boolean;
|
||||
includeIntegrationTest: boolean;
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ const ALL_EXAMPLES: ExampleOptions = {
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
includeIntegrationTest: true,
|
||||
};
|
||||
|
||||
const NO_EXAMPLES: ExampleOptions = {
|
||||
@@ -34,6 +35,7 @@ const NO_EXAMPLES: ExampleOptions = {
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeIntegrationTest: false,
|
||||
};
|
||||
|
||||
describe('copyBaseApplicationProject', () => {
|
||||
@@ -352,6 +354,12 @@ describe('copyBaseApplicationProject', () => {
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// Install functions should always exist
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
@@ -425,6 +433,11 @@ describe('copyBaseApplicationProject', () => {
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, '__tests__', 'app-install.integration-test.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -443,6 +456,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeIntegrationTest: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -480,6 +494,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeIntegrationTest: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -735,6 +750,76 @@ describe('copyBaseApplicationProject', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration test', () => {
|
||||
it('should create app-install.integration-test.ts with correct structure when enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const testPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'__tests__',
|
||||
'app-install.integration-test.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(testPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(testPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { appBuild, appUninstall } from 'twenty-sdk/cli'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { MetadataApiClient } from 'twenty-sdk/generated'",
|
||||
);
|
||||
expect(content).toContain('assertServerIsReachable');
|
||||
expect(content).toContain('appBuild');
|
||||
expect(content).toContain('appUninstall');
|
||||
expect(content).toContain('findManyApplications');
|
||||
});
|
||||
|
||||
it('should include vitest and test scripts in package.json when enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
join(testAppDirectory, 'package.json'),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.test).toBe('vitest run');
|
||||
expect(packageJson.scripts['test:watch']).toBe('vitest');
|
||||
expect(packageJson.devDependencies.vitest).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not include vitest or test scripts when disabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: NO_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJson = await fs.readJson(
|
||||
join(testAppDirectory, 'package.json'),
|
||||
);
|
||||
|
||||
expect(packageJson.scripts.test).toBeUndefined();
|
||||
expect(packageJson.scripts['test:watch']).toBeUndefined();
|
||||
expect(packageJson.devDependencies.vitest).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('post-install logic function', () => {
|
||||
it('should create post-install.ts with definePostInstallLogicFunction and typed payload', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
|
||||
@@ -22,7 +22,11 @@ export const copyBaseApplicationProject = async ({
|
||||
}) => {
|
||||
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
|
||||
|
||||
await createPackageJson({ appName, appDirectory });
|
||||
await createPackageJson({
|
||||
appName,
|
||||
appDirectory,
|
||||
includeIntegrationTest: exampleOptions.includeIntegrationTest,
|
||||
});
|
||||
|
||||
await createGitignore(appDirectory);
|
||||
|
||||
@@ -97,6 +101,14 @@ export const copyBaseApplicationProject = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeIntegrationTest) {
|
||||
await createIntegrationTest({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: '__tests__',
|
||||
fileName: 'app-install.integration-test.ts',
|
||||
});
|
||||
}
|
||||
|
||||
await createDefaultPreInstallFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
@@ -496,6 +508,113 @@ export default defineSkill({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createIntegrationTest = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const content = `import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = path.resolve(__dirname, '../..');
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
|
||||
const readApiKeyFromConfig = (): string | undefined => {
|
||||
const configPath = path.join(os.homedir(), '.twenty', 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
const defaultProfile = config.profiles?.default;
|
||||
|
||||
return defaultProfile?.apiKey ?? config.apiKey;
|
||||
};
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
try {
|
||||
const response = await fetch(\`\${TWENTY_API_URL}/healthz\`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(\`Server returned \${response.status}\`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
\`Twenty server is not reachable at \${TWENTY_API_URL}. \` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
describe('App installation', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(\`[build] \${message}\`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
\`App build failed: \${buildResult.error?.message ?? 'Unknown error'}\`,
|
||||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
\`App uninstall failed: \${uninstallResult.error?.message ?? 'Unknown error'}\`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should find the installed app in the applications list', async () => {
|
||||
const apiKey = readApiKeyFromConfig();
|
||||
const metadataClient = new MetadataApiClient({
|
||||
url: \`\${TWENTY_API_URL}/metadata\`,
|
||||
headers: {
|
||||
Authorization: \`Bearer \${apiKey}\`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await metadataClient.query({
|
||||
findManyApplications: {
|
||||
id: true,
|
||||
name: true,
|
||||
universalIdentifier: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.findManyApplications.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
@@ -527,10 +646,33 @@ export default defineApplication({
|
||||
const createPackageJson = async ({
|
||||
appName,
|
||||
appDirectory,
|
||||
includeIntegrationTest,
|
||||
}: {
|
||||
appName: string;
|
||||
appDirectory: string;
|
||||
includeIntegrationTest: boolean;
|
||||
}) => {
|
||||
const scripts: Record<string, string> = {
|
||||
twenty: 'twenty',
|
||||
lint: 'eslint',
|
||||
'lint:fix': 'eslint --fix',
|
||||
};
|
||||
|
||||
const devDependencies: Record<string, string> = {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
};
|
||||
|
||||
if (includeIntegrationTest) {
|
||||
scripts.test = 'vitest run';
|
||||
scripts['test:watch'] = 'vitest';
|
||||
devDependencies.vitest = '^3.1.1';
|
||||
}
|
||||
|
||||
const packageJson = {
|
||||
name: appName,
|
||||
version: '0.1.0',
|
||||
@@ -541,22 +683,11 @@ const createPackageJson = async ({
|
||||
yarn: '>=4.0.2',
|
||||
},
|
||||
packageManager: 'yarn@4.9.2',
|
||||
scripts: {
|
||||
twenty: 'twenty',
|
||||
lint: 'eslint',
|
||||
'lint:fix': 'eslint --fix',
|
||||
},
|
||||
scripts,
|
||||
dependencies: {
|
||||
'twenty-sdk': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
typescript: '^5.9.3',
|
||||
'@types/node': '^24.7.2',
|
||||
'@types/react': '^18.2.0',
|
||||
react: '^18.2.0',
|
||||
eslint: '^9.32.0',
|
||||
'typescript-eslint': '^8.50.0',
|
||||
},
|
||||
devDependencies,
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
.yarn/install-state.gz
|
||||
.env
|
||||
.twenty
|
||||
|
||||
@@ -13,12 +13,15 @@
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"auth": "twenty auth login"
|
||||
"auth": "twenty auth login",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "0.2.4"
|
||||
"twenty-sdk": "portal:../../twenty-sdk"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"@types/node": "^24.7.2",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { appBuild, appUninstall } from 'twenty-sdk/cli';
|
||||
import { MetadataApiClient } from 'twenty-sdk/generated';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const APP_PATH = path.resolve(__dirname, '../..');
|
||||
const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:3000';
|
||||
|
||||
const POST_CARD_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'54b589ca-eeed-4950-a176-358418b85c05';
|
||||
|
||||
const readApiKeyFromConfig = (): string | undefined => {
|
||||
const configPath = path.join(os.homedir(), '.twenty', 'config.json');
|
||||
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
const defaultProfile = config.profiles?.default;
|
||||
|
||||
return defaultProfile?.apiKey ?? config.apiKey;
|
||||
};
|
||||
|
||||
const assertServerIsReachable = async () => {
|
||||
try {
|
||||
const response = await fetch(`${TWENTY_API_URL}/healthz`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server returned ${response.status}`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Twenty server is not reachable at ${TWENTY_API_URL}. ` +
|
||||
'Make sure the server is running before executing integration tests.',
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
describe('Hello World app installation', () => {
|
||||
let appInstalled = false;
|
||||
|
||||
beforeAll(async () => {
|
||||
await assertServerIsReachable();
|
||||
|
||||
const buildResult = await appBuild({
|
||||
appPath: APP_PATH,
|
||||
onProgress: (message: string) => console.log(`[build] ${message}`),
|
||||
});
|
||||
|
||||
if (!buildResult.success) {
|
||||
throw new Error(
|
||||
`App build failed: ${buildResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
appInstalled = true;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!appInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uninstallResult = await appUninstall({ appPath: APP_PATH });
|
||||
|
||||
if (!uninstallResult.success) {
|
||||
console.warn(
|
||||
`App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should have the postCard object in object metadata after installation', async () => {
|
||||
const apiKey = readApiKeyFromConfig();
|
||||
const metadataClient = new MetadataApiClient({
|
||||
url: `${TWENTY_API_URL}/metadata`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await metadataClient.query({
|
||||
objects: {
|
||||
__args: {
|
||||
paging: { first: 200 },
|
||||
filter: {},
|
||||
},
|
||||
edges: {
|
||||
node: {
|
||||
id: true,
|
||||
universalIdentifier: true,
|
||||
nameSingular: true,
|
||||
namePlural: true,
|
||||
isActive: true,
|
||||
isCustom: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postCardObject = result.objects.edges.find(
|
||||
(edge) =>
|
||||
edge.node.universalIdentifier ===
|
||||
POST_CARD_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
);
|
||||
|
||||
expect(postCardObject).toMatchObject({
|
||||
node: {
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,67 +1,55 @@
|
||||
import type {
|
||||
FunctionConfig,
|
||||
DatabaseEventPayload,
|
||||
ObjectRecordCreateEvent,
|
||||
CronPayload,
|
||||
import {
|
||||
defineLogicFunction,
|
||||
type CronPayload,
|
||||
type DatabaseEventPayload,
|
||||
type ObjectRecordCreateEvent,
|
||||
} from 'twenty-sdk';
|
||||
import Twenty, { type Person } from '../../generated';
|
||||
import { CoreApiClient as Twenty, type CoreSchema } from 'twenty-sdk/generated';
|
||||
|
||||
type CreateNewPostCardParams =
|
||||
| { name?: string }
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<Person>>
|
||||
| DatabaseEventPayload<ObjectRecordCreateEvent<CoreSchema.Person>>
|
||||
| CronPayload;
|
||||
|
||||
export const main = async (params: CreateNewPostCardParams) => {
|
||||
try {
|
||||
const client = new Twenty();
|
||||
const handler = async (params: CreateNewPostCardParams) => {
|
||||
const client = new Twenty();
|
||||
|
||||
const name =
|
||||
'name' in params
|
||||
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
const name =
|
||||
'name' in params
|
||||
? params.name ?? process.env.DEFAULT_RECIPIENT_NAME ?? 'Hello world'
|
||||
: 'Hello world';
|
||||
|
||||
const createPostCard = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: {
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
const createPostCard = await client.mutation({
|
||||
createPostCard: {
|
||||
__args: {
|
||||
data: {
|
||||
name,
|
||||
},
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
name: true,
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('createPostCard result', createPostCard);
|
||||
console.log('createPostCard result', createPostCard);
|
||||
|
||||
return createPostCard;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
return createPostCard;
|
||||
};
|
||||
|
||||
export const config: FunctionConfig = {
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'e56d363b-0bdc-4d8a-a393-6f0d1c75bdcf',
|
||||
name: 'create-new-post-card',
|
||||
timeoutSeconds: 2,
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: 'c9f84c8d-b26d-40d1-95dd-4f834ae5a2c6',
|
||||
type: 'route',
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'dd802808-0695-49e1-98c9-d5c9e2704ce2',
|
||||
type: 'cron',
|
||||
pattern: '0 0 1 1 *', // Every year 1st of January
|
||||
},
|
||||
{
|
||||
universalIdentifier: '203f1df3-4a82-4d06-a001-b8cf22a31156',
|
||||
type: 'databaseEvent',
|
||||
eventName: 'person.created',
|
||||
},
|
||||
],
|
||||
};
|
||||
handler,
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/post-card/create',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
cronTriggerSettings: {
|
||||
pattern: '0 0 1 1 *',
|
||||
},
|
||||
databaseEventTriggerSettings: {
|
||||
eventName: 'person.created',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type ApplicationConfig } from 'twenty-sdk';
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
|
||||
const config: ApplicationConfig = {
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
displayName: 'Hello World',
|
||||
description: 'A simple hello world app',
|
||||
@@ -14,6 +14,4 @@ const config: ApplicationConfig = {
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
};
|
||||
|
||||
export default config;
|
||||
});
|
||||
|
||||
@@ -1,112 +1,89 @@
|
||||
import { type Note } from '../../generated';
|
||||
import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
import {
|
||||
type AddressField,
|
||||
Field,
|
||||
FieldType,
|
||||
type FullNameField,
|
||||
Object,
|
||||
OnDeleteAction,
|
||||
Relation,
|
||||
RelationType,
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
||||
} from 'twenty-sdk';
|
||||
const POST_CARD_STATUS = {
|
||||
DRAFT: 'DRAFT',
|
||||
SENT: 'SENT',
|
||||
DELIVERED: 'DELIVERED',
|
||||
RETURNED: 'RETURNED',
|
||||
} as const;
|
||||
|
||||
enum PostCardStatus {
|
||||
DRAFT = 'DRAFT',
|
||||
SENT = 'SENT',
|
||||
DELIVERED = 'DELIVERED',
|
||||
RETURNED = 'RETURNED',
|
||||
}
|
||||
|
||||
@Object({
|
||||
export default defineObject({
|
||||
universalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
nameSingular: 'postCard',
|
||||
namePlural: 'postCards',
|
||||
labelSingular: 'Post card',
|
||||
labelPlural: 'Post cards',
|
||||
description: ' A post card object',
|
||||
description: 'A post card object',
|
||||
icon: 'IconMail',
|
||||
})
|
||||
export class PostCard {
|
||||
@Field({
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
})
|
||||
content: string;
|
||||
|
||||
@Field({
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
type: FieldType.FULL_NAME,
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
})
|
||||
recipientName: FullNameField;
|
||||
|
||||
@Field({
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
type: FieldType.ADDRESS,
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
})
|
||||
recipientAddress: AddressField;
|
||||
|
||||
@Field({
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
type: FieldType.SELECT,
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${PostCardStatus.DRAFT}'`,
|
||||
options: [
|
||||
{
|
||||
value: PostCardStatus.DRAFT,
|
||||
label: 'Draft',
|
||||
position: 0,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
value: PostCardStatus.SENT,
|
||||
label: 'Sent',
|
||||
position: 1,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
value: PostCardStatus.DELIVERED,
|
||||
label: 'Delivered',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
value: PostCardStatus.RETURNED,
|
||||
label: 'Returned',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
})
|
||||
status: PostCardStatus;
|
||||
|
||||
@Relation({
|
||||
universalIdentifier: 'c9e2b4f4-b9ad-4427-9b42-9971b785edfe',
|
||||
type: RelationType.ONE_TO_MANY,
|
||||
label: 'Notes',
|
||||
icon: 'IconComment',
|
||||
inverseSideTargetUniversalIdentifier:
|
||||
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.note.universalIdentifier,
|
||||
onDelete: OnDeleteAction.CASCADE,
|
||||
})
|
||||
notes: Note[];
|
||||
|
||||
@Field({
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
type: FieldType.DATE_TIME,
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
})
|
||||
deliveredAt?: Date;
|
||||
}
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
type: FieldType.TEXT,
|
||||
name: 'content',
|
||||
label: 'Content',
|
||||
description: "Postcard's content",
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c6aa31f3-da76-4ac6-889f-475e226009ac',
|
||||
type: FieldType.FULL_NAME,
|
||||
name: 'recipientName',
|
||||
label: 'Recipient name',
|
||||
icon: 'IconUser',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '95045777-a0ad-49ec-98f9-22f9fc0c8266',
|
||||
type: FieldType.ADDRESS,
|
||||
name: 'recipientAddress',
|
||||
label: 'Recipient address',
|
||||
icon: 'IconHome',
|
||||
},
|
||||
{
|
||||
universalIdentifier: '87b675b8-dd8c-4448-b4ca-20e5a2234a1e',
|
||||
type: FieldType.SELECT,
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
icon: 'IconSend',
|
||||
defaultValue: `'${POST_CARD_STATUS.DRAFT}'`,
|
||||
options: [
|
||||
{
|
||||
id: 'a1b2c3d4-0001-4000-8000-000000000001',
|
||||
value: POST_CARD_STATUS.DRAFT,
|
||||
label: 'Draft',
|
||||
position: 0,
|
||||
color: 'gray',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0002-4000-8000-000000000002',
|
||||
value: POST_CARD_STATUS.SENT,
|
||||
label: 'Sent',
|
||||
position: 1,
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0003-4000-8000-000000000003',
|
||||
value: POST_CARD_STATUS.DELIVERED,
|
||||
label: 'Delivered',
|
||||
position: 2,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: 'a1b2c3d4-0004-4000-8000-000000000004',
|
||||
value: POST_CARD_STATUS.RETURNED,
|
||||
label: 'Returned',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e06abe72-5b44-4e7f-93be-afc185a3c433',
|
||||
type: FieldType.DATE_TIME,
|
||||
name: 'deliveredAt',
|
||||
label: 'Delivered at',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PermissionFlag, type RoleConfig } from 'twenty-sdk';
|
||||
import { defineRole, PermissionFlag } from 'twenty-sdk';
|
||||
|
||||
export const functionRole: RoleConfig = {
|
||||
export default defineRole({
|
||||
universalIdentifier: 'b648f87b-1d26-4961-b974-0908fd991061',
|
||||
label: 'Default function role',
|
||||
description: 'Default role for function Twenty client',
|
||||
@@ -14,7 +14,7 @@ export const functionRole: RoleConfig = {
|
||||
canBeAssignedToApiKeys: false,
|
||||
objectPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
canReadObjectRecords: true,
|
||||
canUpdateObjectRecords: true,
|
||||
canSoftDeleteObjectRecords: false,
|
||||
@@ -23,11 +23,11 @@ export const functionRole: RoleConfig = {
|
||||
],
|
||||
fieldPermissions: [
|
||||
{
|
||||
objectUniversalIdentifier: '9f9882af-170c-4879-b013-f9628b77c050',
|
||||
fieldUniversalIdentifier: 'b2c37dc0-8ae7-470e-96cd-1476b47dfaff',
|
||||
objectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
fieldUniversalIdentifier: '58a0a314-d7ea-4865-9850-7fb84e72f30b',
|
||||
canReadFieldValue: false,
|
||||
canUpdateFieldValue: false,
|
||||
},
|
||||
],
|
||||
permissionFlags: [PermissionFlag.APPLICATIONS],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
testTimeout: 120_000,
|
||||
hookTimeout: 120_000,
|
||||
include: ['src/**/*.integration-test.ts'],
|
||||
env: {
|
||||
// The SDK's ConfigService reads credentials from ~/.twenty/config.json
|
||||
// but falls back to a temp dir when NODE_ENV=test.
|
||||
NODE_ENV: 'integration',
|
||||
// MetadataApiClient defaults to TWENTY_API_URL for its GraphQL endpoint.
|
||||
TWENTY_API_URL: 'http://localhost:3000',
|
||||
},
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,11 @@
|
||||
"import": "./dist/ui/index.mjs",
|
||||
"require": "./dist/ui/index.cjs"
|
||||
},
|
||||
"./cli": {
|
||||
"types": "./dist/cli/public-operations/index.d.ts",
|
||||
"import": "./dist/operations.mjs",
|
||||
"require": "./dist/operations.cjs"
|
||||
},
|
||||
"./front-component-renderer": {
|
||||
"types": "./dist/front-component-renderer/index.d.ts",
|
||||
"import": "./dist/front-component-renderer/index.mjs",
|
||||
@@ -120,6 +125,9 @@
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"cli": [
|
||||
"dist/cli/public-operations/index.d.ts"
|
||||
],
|
||||
"ui": [
|
||||
"dist/ui/index.d.ts"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { formatPath } from '@/cli/utilities/file/file-path';
|
||||
import chalk from 'chalk';
|
||||
import type { Command } from 'commander';
|
||||
import { AppBuildCommand } from './app/app-build';
|
||||
import { AppDevCommand } from './app/app-dev';
|
||||
import { AppTypecheckCommand } from './app/app-typecheck';
|
||||
import { AppUninstallCommand } from './app/app-uninstall';
|
||||
@@ -60,6 +61,7 @@ export const registerCommands = (program: Command): void => {
|
||||
});
|
||||
|
||||
// App commands
|
||||
const buildCommand = new AppBuildCommand();
|
||||
const devCommand = new AppDevCommand();
|
||||
const typecheckCommand = new AppTypecheckCommand();
|
||||
const uninstallCommand = new AppUninstallCommand();
|
||||
@@ -67,6 +69,15 @@ export const registerCommands = (program: Command): void => {
|
||||
const logsCommand = new LogicFunctionLogsCommand();
|
||||
const executeCommand = new LogicFunctionExecuteCommand();
|
||||
|
||||
program
|
||||
.command('app:build [appPath]')
|
||||
.description('Build the application without watching for changes')
|
||||
.action(async (appPath) => {
|
||||
await buildCommand.execute({
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
});
|
||||
|
||||
program
|
||||
.command('app:dev [appPath]')
|
||||
.description('Watch and sync local application changes')
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { appBuild } from '@/cli/public-operations/app-build';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type AppBuildCommandOptions = {
|
||||
appPath?: string;
|
||||
};
|
||||
|
||||
export class AppBuildCommand {
|
||||
async execute(options: AppBuildCommandOptions): Promise<void> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
console.log(chalk.blue('Building and syncing application...'));
|
||||
console.log(chalk.gray(`App path: ${appPath}`));
|
||||
console.log('');
|
||||
|
||||
const result = await appBuild({
|
||||
appPath,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error(chalk.red(result.error.message));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`✓ Build and sync succeeded (${result.data.fileCount} file${result.data.fileCount === 1 ? '' : 's'})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { appUninstall } from '@/cli/public-operations/app-uninstall';
|
||||
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
|
||||
export class AppUninstallCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute({
|
||||
appPath = CURRENT_EXECUTION_DIRECTORY,
|
||||
askForConfirmation,
|
||||
@@ -25,23 +22,15 @@ export class AppUninstallCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifest = await readManifestFromFile(appPath);
|
||||
const result = await appUninstall({ appPath });
|
||||
|
||||
if (!manifest) {
|
||||
return { success: false, error: 'Build failed' };
|
||||
if (!result.success) {
|
||||
console.error(chalk.red('❌ Uninstall failed:'), result.error.message);
|
||||
return { success: false, error: result.error.message };
|
||||
}
|
||||
|
||||
const result = await this.apiService.uninstallApplication(
|
||||
manifest.application.universalIdentifier,
|
||||
);
|
||||
|
||||
if (result.success === false) {
|
||||
console.error(chalk.red('❌ Uninstall failed:'), result.error);
|
||||
} else {
|
||||
console.log(chalk.green('✅ Application uninstalled successfully'));
|
||||
}
|
||||
|
||||
return result;
|
||||
console.log(chalk.green('✅ Application uninstalled successfully'));
|
||||
return { success: true, data: undefined };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
chalk.red('Uninstall failed:'),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export class AuthListCommand {
|
||||
private configService = new ConfigService();
|
||||
@@ -21,18 +21,20 @@ export class AuthListCommand {
|
||||
|
||||
console.log(chalk.blue('Available workspaces:\n'));
|
||||
|
||||
for (const workspace of availableWorkspaces) {
|
||||
for (const workspaceName of availableWorkspaces) {
|
||||
const config =
|
||||
await this.configService.getConfigForWorkspace(workspace);
|
||||
await this.configService.getConfigForWorkspace(workspaceName);
|
||||
const hasCredentials = !!config.apiKey;
|
||||
const isDefault = workspace === currentDefault;
|
||||
const isDefault = workspaceName === currentDefault;
|
||||
|
||||
const defaultIndicator = isDefault ? chalk.green(' (default)') : '';
|
||||
const credentialStatus = hasCredentials
|
||||
? chalk.green('●')
|
||||
: chalk.gray('○');
|
||||
|
||||
console.log(` ${credentialStatus} ${workspace}${defaultIndicator}`);
|
||||
console.log(
|
||||
` ${credentialStatus} ${workspaceName}${defaultIndicator}`,
|
||||
);
|
||||
console.log(chalk.gray(` API URL: ${config.apiUrl}`));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { authLogin } from '@/cli/public-operations/auth-login';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
|
||||
export class AuthLoginCommand {
|
||||
private configService = new ConfigService();
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute(options: { apiKey?: string; apiUrl?: string }): Promise<void> {
|
||||
try {
|
||||
let { apiKey, apiUrl } = options;
|
||||
|
||||
// Get current config
|
||||
const config = await this.configService.getConfig();
|
||||
|
||||
// Prompt for missing values
|
||||
if (!apiUrl) {
|
||||
const urlAnswer = await inquirer.prompt([
|
||||
{
|
||||
@@ -48,16 +45,9 @@ export class AuthLoginCommand {
|
||||
apiKey = keyAnswer.apiKey;
|
||||
}
|
||||
|
||||
// Update config
|
||||
await this.configService.setConfig({
|
||||
apiUrl,
|
||||
apiKey,
|
||||
});
|
||||
const result = await authLogin({ apiKey: apiKey!, apiUrl: apiUrl! });
|
||||
|
||||
// Validate authentication
|
||||
const validateAuth = await this.apiService.validateAuth();
|
||||
|
||||
if (validateAuth.authValid) {
|
||||
if (result.success) {
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
console.log(
|
||||
chalk.green(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import chalk from 'chalk';
|
||||
import { authLogout } from '@/cli/public-operations/auth-logout';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export class AuthLogoutCommand {
|
||||
private configService = new ConfigService();
|
||||
|
||||
async execute(): Promise<void> {
|
||||
try {
|
||||
await this.configService.clearConfig();
|
||||
await authLogout();
|
||||
const activeWorkspace = ConfigService.getActiveWorkspace();
|
||||
console.log(
|
||||
chalk.green(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import chalk from 'chalk';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export class AuthStatusCommand {
|
||||
private configService = new ConfigService();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
export class AuthSwitchCommand {
|
||||
private configService = new ConfigService();
|
||||
@@ -24,9 +24,7 @@ export class AuthSwitchCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// If workspace is not provided, show interactive selection
|
||||
if (!workspace) {
|
||||
// Build choices with indicators for current default
|
||||
const choices = availableWorkspaces.map((ws) => ({
|
||||
name: ws === currentDefault ? `${ws} (current default)` : ws,
|
||||
value: ws,
|
||||
@@ -45,7 +43,6 @@ export class AuthSwitchCommand {
|
||||
workspace = answer.workspace as string;
|
||||
}
|
||||
|
||||
// Validate that the workspace exists (workspace is guaranteed to be defined here)
|
||||
if (!availableWorkspaces.includes(workspace!)) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
@@ -55,7 +52,6 @@ export class AuthSwitchCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// If already the default, inform and exit
|
||||
if (workspace === currentDefault) {
|
||||
console.log(
|
||||
chalk.blue(`ℹ "${workspace}" is already the default workspace.`),
|
||||
@@ -63,13 +59,9 @@ export class AuthSwitchCommand {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the new default workspace
|
||||
await this.configService.setDefaultWorkspace(workspace!);
|
||||
|
||||
// Also set it as active for the current session to validate
|
||||
ConfigService.setActiveWorkspace(workspace);
|
||||
|
||||
// Check authentication status for the new workspace
|
||||
const config = await this.configService.getConfig();
|
||||
const hasCredentials = !!config.apiKey;
|
||||
|
||||
@@ -79,6 +71,7 @@ export class AuthSwitchCommand {
|
||||
|
||||
if (hasCredentials) {
|
||||
const validateAuth = await this.apiService.validateAuth();
|
||||
|
||||
if (validateAuth.authValid) {
|
||||
console.log(chalk.green('✓ Authentication is valid'));
|
||||
} else {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { functionExecute } from '@/cli/public-operations/function-execute';
|
||||
import {
|
||||
APP_ERROR_CODES,
|
||||
FUNCTION_ERROR_CODES,
|
||||
} from '@/cli/public-operations/types';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
|
||||
export class LogicFunctionExecuteCommand {
|
||||
private apiService = new ApiService();
|
||||
|
||||
async execute({
|
||||
appPath = CURRENT_EXECUTION_DIRECTORY,
|
||||
postInstall = false,
|
||||
@@ -32,90 +32,73 @@ export class LogicFunctionExecuteCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifest = await readManifestFromFile(appPath);
|
||||
const identifier = postInstall
|
||||
? 'post install'
|
||||
: (functionUniversalIdentifier ?? functionName);
|
||||
|
||||
if (!manifest) {
|
||||
console.error(chalk.red('Failed to build manifest.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const functionsResult = await this.apiService.findLogicFunctions();
|
||||
if (!functionsResult.success) {
|
||||
console.error(
|
||||
chalk.red('Failed to fetch functions:'),
|
||||
functionsResult.error instanceof Error
|
||||
? functionsResult.error.message
|
||||
: functionsResult.error,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const appFunctions = functionsResult.data.filter(
|
||||
(fn) =>
|
||||
fn.universalIdentifier && this.belongsToApplication(fn, manifest),
|
||||
);
|
||||
|
||||
const targetFunction = appFunctions.find((fn) => {
|
||||
if (postInstall) {
|
||||
return (
|
||||
fn.universalIdentifier ===
|
||||
manifest.application.postInstallLogicFunctionUniversalIdentifier
|
||||
);
|
||||
}
|
||||
if (functionUniversalIdentifier) {
|
||||
return fn.universalIdentifier === functionUniversalIdentifier;
|
||||
}
|
||||
if (functionName) {
|
||||
return fn.name === functionName;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!targetFunction) {
|
||||
const identifier = postInstall
|
||||
? 'post install'
|
||||
: functionUniversalIdentifier || functionName;
|
||||
console.error(
|
||||
chalk.red(`Function "${identifier}" not found in application.`),
|
||||
);
|
||||
console.log('');
|
||||
if (appFunctions.length > 0) {
|
||||
console.log(chalk.cyan('Available functions:'));
|
||||
appFunctions.forEach((fn) => {
|
||||
console.log(
|
||||
` - ${chalk.white(fn.name)} (${fn.universalIdentifier})`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'No functions found for this application. Have you synced your app with `yarn app:dev`?',
|
||||
),
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.blue(`🚀 Executing function "${targetFunction.name}"...`),
|
||||
);
|
||||
console.log(chalk.blue(`🚀 Executing function "${identifier}"...`));
|
||||
console.log(chalk.gray(` Payload: ${JSON.stringify(parsedPayload)}`));
|
||||
console.log('');
|
||||
|
||||
const result = await this.apiService.executeLogicFunction({
|
||||
functionId: targetFunction.id,
|
||||
payload: parsedPayload,
|
||||
});
|
||||
const executeOptions = postInstall
|
||||
? { appPath, postInstall: true as const, payload: parsedPayload }
|
||||
: functionUniversalIdentifier
|
||||
? { appPath, functionUniversalIdentifier, payload: parsedPayload }
|
||||
: { appPath, functionName: functionName!, payload: parsedPayload };
|
||||
|
||||
const result = await functionExecute(executeOptions);
|
||||
|
||||
if (!result.success) {
|
||||
console.error(
|
||||
chalk.red('Execution failed:'),
|
||||
result.error instanceof Error ? result.error.message : result.error,
|
||||
);
|
||||
switch (result.error.code) {
|
||||
case APP_ERROR_CODES.MANIFEST_NOT_FOUND: {
|
||||
console.error(chalk.red('Failed to build manifest.'));
|
||||
break;
|
||||
}
|
||||
case FUNCTION_ERROR_CODES.FETCH_FUNCTIONS_FAILED: {
|
||||
console.error(
|
||||
chalk.red('Failed to fetch functions:'),
|
||||
result.error.message,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FUNCTION_ERROR_CODES.FUNCTION_NOT_FOUND: {
|
||||
console.error(chalk.red(result.error.message));
|
||||
console.log('');
|
||||
|
||||
const availableFunctions = (result.error.details
|
||||
?.availableFunctions ?? []) as Array<{
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
}>;
|
||||
|
||||
if (availableFunctions.length > 0) {
|
||||
console.log(chalk.cyan('Available functions:'));
|
||||
availableFunctions.forEach((logicFunction) => {
|
||||
console.log(
|
||||
` - ${chalk.white(logicFunction.name)} (${logicFunction.universalIdentifier})`,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'No functions found for this application. Have you synced your app with `yarn app:dev`?',
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FUNCTION_ERROR_CODES.EXECUTION_FAILED: {
|
||||
console.error(chalk.red('Execution failed:'), result.error.message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.error(chalk.red(result.error.message));
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const executionResult = result.data!;
|
||||
const executionResult = result.data;
|
||||
|
||||
console.log(chalk.cyan('─'.repeat(60)));
|
||||
console.log(chalk.cyan('Execution Result'));
|
||||
@@ -168,13 +151,4 @@ export class LogicFunctionExecuteCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private belongsToApplication(
|
||||
fn: { universalIdentifier: string; applicationId: string | null },
|
||||
manifest: Manifest,
|
||||
): boolean {
|
||||
return manifest.logicFunctions.some(
|
||||
(manifestFn) => manifestFn.universalIdentifier === fn.universalIdentifier,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { buildApplication } from '@/cli/utilities/build/common/build-application';
|
||||
import { synchronizeBuiltApplication } from '@/cli/utilities/build/common/synchronize-built-application';
|
||||
import { runTypecheck } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest';
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
import { APP_ERROR_CODES, type CommandResult } from './types';
|
||||
|
||||
export type AppBuildOptions = {
|
||||
appPath: string;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type AppBuildResult = {
|
||||
fileCount: number;
|
||||
};
|
||||
|
||||
export const appBuild = async (
|
||||
options: AppBuildOptions,
|
||||
): Promise<CommandResult<AppBuildResult>> => {
|
||||
const { appPath, onProgress } = options;
|
||||
|
||||
onProgress?.('Building manifest...');
|
||||
|
||||
const manifestResult = await buildAndValidateManifest(appPath);
|
||||
|
||||
if (!manifestResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_BUILD_FAILED,
|
||||
message: manifestResult.errors.join('\n'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const { manifest, filePaths } = manifestResult;
|
||||
const clientService = new ClientService();
|
||||
|
||||
await clientService.ensureGeneratedClientStub({ appPath });
|
||||
|
||||
onProgress?.('Building application files...');
|
||||
|
||||
const firstBuildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing application schema...');
|
||||
|
||||
const firstSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: firstBuildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!firstSyncResult.success) {
|
||||
return firstSyncResult;
|
||||
}
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
await clientService.generate({ appPath });
|
||||
|
||||
onProgress?.('Running typecheck...');
|
||||
|
||||
const typecheckErrors = await runTypecheck(appPath);
|
||||
|
||||
if (typecheckErrors.length > 0) {
|
||||
const errorMessages = typecheckErrors.map(
|
||||
(error) => `${error.file}(${error.line},${error.column}): ${error.text}`,
|
||||
);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: `Typecheck failed:\n${errorMessages.join('\n')}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Rebuilding with generated client...');
|
||||
|
||||
const finalBuildResult = await buildApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
filePaths,
|
||||
});
|
||||
|
||||
onProgress?.('Syncing built files...');
|
||||
|
||||
const finalSyncResult = await synchronizeBuiltApplication({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos: finalBuildResult.builtFileInfos,
|
||||
});
|
||||
|
||||
if (!finalSyncResult.success) {
|
||||
return finalSyncResult;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
fileCount: finalBuildResult.builtFileInfos.size,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { APP_ERROR_CODES, type CommandResult } from './types';
|
||||
|
||||
export type AppUninstallOptions = {
|
||||
appPath: string;
|
||||
workspace?: string;
|
||||
};
|
||||
|
||||
export const appUninstall = async (
|
||||
options: AppUninstallOptions,
|
||||
): Promise<CommandResult> => {
|
||||
if (options.workspace) {
|
||||
ConfigService.setActiveWorkspace(options.workspace);
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
const manifest = await readManifestFromFile(options.appPath);
|
||||
|
||||
if (!manifest) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message: 'Failed to build manifest.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await apiService.uninstallApplication(
|
||||
manifest.application.universalIdentifier,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage =
|
||||
result.error instanceof Error
|
||||
? result.error.message
|
||||
: String(result.error ?? 'Unknown error');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.UNINSTALL_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { AUTH_ERROR_CODES, type CommandResult } from './types';
|
||||
|
||||
export type AuthLoginOptions = {
|
||||
apiKey: string;
|
||||
apiUrl: string;
|
||||
workspace?: string;
|
||||
};
|
||||
|
||||
export const authLogin = async (
|
||||
options: AuthLoginOptions,
|
||||
): Promise<CommandResult> => {
|
||||
const { apiKey, apiUrl, workspace } = options;
|
||||
|
||||
if (workspace) {
|
||||
ConfigService.setActiveWorkspace(workspace);
|
||||
}
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.setConfig({ apiUrl, apiKey });
|
||||
|
||||
const apiService = new ApiService();
|
||||
const validateAuth = await apiService.validateAuth();
|
||||
|
||||
if (!validateAuth.authValid) {
|
||||
await configService.clearConfig();
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: AUTH_ERROR_CODES.AUTH_FAILED,
|
||||
message: 'Authentication failed. Please check your credentials.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { type CommandResult } from './types';
|
||||
|
||||
export type AuthLogoutOptions = {
|
||||
workspace?: string;
|
||||
};
|
||||
|
||||
export const authLogout = async (
|
||||
options?: AuthLogoutOptions,
|
||||
): Promise<CommandResult> => {
|
||||
if (options?.workspace) {
|
||||
ConfigService.setActiveWorkspace(options.workspace);
|
||||
}
|
||||
|
||||
const configService = new ConfigService();
|
||||
|
||||
await configService.clearConfig();
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { readManifestFromFile } from '@/cli/utilities/build/manifest/manifest-reader';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import {
|
||||
APP_ERROR_CODES,
|
||||
FUNCTION_ERROR_CODES,
|
||||
type CommandResult,
|
||||
type FunctionExecutionResult,
|
||||
} from './types';
|
||||
|
||||
export type FunctionExecuteOptions = {
|
||||
appPath: string;
|
||||
workspace?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
} & (
|
||||
| { postInstall: true }
|
||||
| { functionUniversalIdentifier: string }
|
||||
| { functionName: string }
|
||||
);
|
||||
|
||||
type LogicFunction = {
|
||||
id: string;
|
||||
name: string;
|
||||
universalIdentifier: string;
|
||||
applicationId: string | null;
|
||||
};
|
||||
|
||||
const belongsToApplication = (
|
||||
logicFunction: LogicFunction,
|
||||
manifest: Manifest,
|
||||
): boolean => {
|
||||
return manifest.logicFunctions.some(
|
||||
(manifestFn) =>
|
||||
manifestFn.universalIdentifier === logicFunction.universalIdentifier,
|
||||
);
|
||||
};
|
||||
|
||||
const resolveIdentifier = (options: FunctionExecuteOptions): string => {
|
||||
if ('postInstall' in options) return 'post install';
|
||||
if ('functionUniversalIdentifier' in options)
|
||||
return options.functionUniversalIdentifier;
|
||||
if ('functionName' in options) return options.functionName;
|
||||
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
export const functionExecute = async (
|
||||
options: FunctionExecuteOptions,
|
||||
): Promise<CommandResult<FunctionExecutionResult>> => {
|
||||
if (options.workspace) {
|
||||
ConfigService.setActiveWorkspace(options.workspace);
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
const manifest = await readManifestFromFile(options.appPath);
|
||||
|
||||
if (!manifest) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.MANIFEST_NOT_FOUND,
|
||||
message: 'Failed to build manifest.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const functionsResult = await apiService.findLogicFunctions();
|
||||
|
||||
if (!functionsResult.success) {
|
||||
const errorMessage =
|
||||
functionsResult.error instanceof Error
|
||||
? functionsResult.error.message
|
||||
: String(functionsResult.error ?? 'Failed to fetch functions');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: FUNCTION_ERROR_CODES.FETCH_FUNCTIONS_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const appFunctions = functionsResult.data.filter(
|
||||
(logicFunction) =>
|
||||
logicFunction.universalIdentifier &&
|
||||
belongsToApplication(logicFunction, manifest),
|
||||
);
|
||||
|
||||
const targetFunction = appFunctions.find((logicFunction) => {
|
||||
if ('postInstall' in options && options.postInstall) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
manifest.application.postInstallLogicFunctionUniversalIdentifier
|
||||
);
|
||||
}
|
||||
if ('functionUniversalIdentifier' in options) {
|
||||
return (
|
||||
logicFunction.universalIdentifier ===
|
||||
options.functionUniversalIdentifier
|
||||
);
|
||||
}
|
||||
if ('functionName' in options) {
|
||||
return logicFunction.name === options.functionName;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!targetFunction) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: FUNCTION_ERROR_CODES.FUNCTION_NOT_FOUND,
|
||||
message: `Function "${resolveIdentifier(options)}" not found in application.`,
|
||||
details: {
|
||||
identifier: resolveIdentifier(options),
|
||||
availableFunctions: appFunctions.map((logicFunction) => ({
|
||||
name: logicFunction.name,
|
||||
universalIdentifier: logicFunction.universalIdentifier,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const result = await apiService.executeLogicFunction({
|
||||
functionId: targetFunction.id,
|
||||
payload: options.payload ?? {},
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage =
|
||||
result.error instanceof Error
|
||||
? result.error.message
|
||||
: String(result.error ?? 'Execution failed');
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: FUNCTION_ERROR_CODES.EXECUTION_FAILED,
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
functionName: targetFunction.name,
|
||||
...result.data!,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
// Auth
|
||||
export { authLogin } from './auth-login';
|
||||
export type { AuthLoginOptions } from './auth-login';
|
||||
export { authLogout } from './auth-logout';
|
||||
export type { AuthLogoutOptions } from './auth-logout';
|
||||
|
||||
// App
|
||||
export { appBuild } from './app-build';
|
||||
export type { AppBuildOptions, AppBuildResult } from './app-build';
|
||||
export { appUninstall } from './app-uninstall';
|
||||
export type { AppUninstallOptions } from './app-uninstall';
|
||||
|
||||
// Functions
|
||||
export { functionExecute } from './function-execute';
|
||||
export type { FunctionExecuteOptions } from './function-execute';
|
||||
|
||||
// Shared types and error codes
|
||||
export {
|
||||
APP_ERROR_CODES,
|
||||
AUTH_ERROR_CODES,
|
||||
FUNCTION_ERROR_CODES,
|
||||
} from './types';
|
||||
export type {
|
||||
AuthListWorkspace,
|
||||
AuthStatusResult,
|
||||
CommandError,
|
||||
CommandResult,
|
||||
FunctionExecutionResult,
|
||||
TypecheckResult,
|
||||
} from './types';
|
||||
@@ -0,0 +1,65 @@
|
||||
export type CommandError = {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CommandResult<T = void> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: CommandError };
|
||||
|
||||
export const AUTH_ERROR_CODES = {
|
||||
AUTH_FAILED: 'AUTH_FAILED',
|
||||
NO_WORKSPACES: 'NO_WORKSPACES',
|
||||
WORKSPACE_NOT_FOUND: 'WORKSPACE_NOT_FOUND',
|
||||
} as const;
|
||||
|
||||
export const APP_ERROR_CODES = {
|
||||
MANIFEST_NOT_FOUND: 'MANIFEST_NOT_FOUND',
|
||||
MANIFEST_BUILD_FAILED: 'MANIFEST_BUILD_FAILED',
|
||||
UNINSTALL_FAILED: 'UNINSTALL_FAILED',
|
||||
SYNC_FAILED: 'SYNC_FAILED',
|
||||
} as const;
|
||||
|
||||
export const FUNCTION_ERROR_CODES = {
|
||||
FETCH_FUNCTIONS_FAILED: 'FETCH_FUNCTIONS_FAILED',
|
||||
FUNCTION_NOT_FOUND: 'FUNCTION_NOT_FOUND',
|
||||
EXECUTION_FAILED: 'EXECUTION_FAILED',
|
||||
} as const;
|
||||
|
||||
export type AuthStatusResult = {
|
||||
workspace: string;
|
||||
apiUrl: string;
|
||||
apiKeyMasked: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isValid: boolean;
|
||||
};
|
||||
|
||||
export type AuthListWorkspace = {
|
||||
name: string;
|
||||
apiUrl: string;
|
||||
hasCredentials: boolean;
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
export type TypecheckResult = {
|
||||
errors: Array<{
|
||||
text: string;
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type FunctionExecutionResult = {
|
||||
functionName: string;
|
||||
data: unknown;
|
||||
logs: string;
|
||||
duration: number;
|
||||
status: string;
|
||||
error?: {
|
||||
errorType: string;
|
||||
errorMessage: string;
|
||||
stackTrace: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { type ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export type EnsureApplicationSuccess = {
|
||||
success: true;
|
||||
applicationId: string;
|
||||
universalIdentifier: string;
|
||||
created: boolean;
|
||||
};
|
||||
|
||||
export type EnsureApplicationFailure = {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type EnsureApplicationResult =
|
||||
| EnsureApplicationSuccess
|
||||
| EnsureApplicationFailure;
|
||||
|
||||
export const findOrCreateApplication = async ({
|
||||
apiService,
|
||||
manifest,
|
||||
applicationRegistrationId,
|
||||
}: {
|
||||
apiService: ApiService;
|
||||
manifest: Manifest;
|
||||
applicationRegistrationId?: string;
|
||||
}): Promise<EnsureApplicationResult> => {
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
|
||||
const findResult = await apiService.findOneApplication(universalIdentifier);
|
||||
|
||||
if (!findResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to resolve application: ${serializeError(findResult.error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (findResult.data) {
|
||||
return {
|
||||
success: true,
|
||||
applicationId: findResult.data.id,
|
||||
universalIdentifier: findResult.data.universalIdentifier,
|
||||
created: false,
|
||||
};
|
||||
}
|
||||
|
||||
let registrationId = applicationRegistrationId;
|
||||
|
||||
if (!registrationId) {
|
||||
const registerResult =
|
||||
await apiService.findApplicationRegistrationByUniversalIdentifier(
|
||||
universalIdentifier,
|
||||
);
|
||||
|
||||
if (registerResult.success && registerResult.data) {
|
||||
registrationId = registerResult.data.id;
|
||||
}
|
||||
}
|
||||
|
||||
const createResult = await apiService.createApplication(manifest, {
|
||||
applicationRegistrationId: registrationId,
|
||||
});
|
||||
|
||||
if (!createResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to create application: ${serializeError(createResult.error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
applicationId: createResult.data!.id,
|
||||
universalIdentifier: createResult.data!.universalIdentifier,
|
||||
created: true,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
import { esbuildOneShotBuild } from '@/cli/utilities/build/common/esbuild-one-shot-build';
|
||||
import {
|
||||
LOGIC_FUNCTION_EXTERNAL_MODULES,
|
||||
createSdkGeneratedResolverPlugin,
|
||||
} from '@/cli/utilities/build/common/esbuild-watcher';
|
||||
import { FRONT_COMPONENT_EXTERNAL_MODULES } from '@/cli/utilities/build/common/front-component-build/constants/front-component-external-modules';
|
||||
import { getFrontComponentBuildPlugins } from '@/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
|
||||
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
|
||||
import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
|
||||
import crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import { dirname, join } from 'path';
|
||||
import {
|
||||
NODE_ESM_CJS_BANNER,
|
||||
OUTPUT_DIR,
|
||||
type Manifest,
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export type AppBuildOptions = {
|
||||
appPath: string;
|
||||
manifest: Manifest;
|
||||
filePaths: EntityFilePaths;
|
||||
};
|
||||
|
||||
export type BuiltFileInfo = {
|
||||
checksum: string;
|
||||
builtPath: string;
|
||||
sourcePath: string;
|
||||
fileFolder: FileFolder;
|
||||
};
|
||||
|
||||
export type AppBuildResult = {
|
||||
builtFileInfos: Map<string, BuiltFileInfo>;
|
||||
};
|
||||
|
||||
export const buildApplication = async (
|
||||
options: AppBuildOptions,
|
||||
): Promise<AppBuildResult> => {
|
||||
const outputDir = join(options.appPath, OUTPUT_DIR);
|
||||
|
||||
await fs.ensureDir(outputDir);
|
||||
await fs.emptyDir(outputDir);
|
||||
|
||||
const builtFileInfos = new Map<string, BuiltFileInfo>();
|
||||
|
||||
const collectFileBuilt: OnFileBuiltCallback = (event) => {
|
||||
builtFileInfos.set(event.builtPath, {
|
||||
checksum: event.checksum,
|
||||
builtPath: event.builtPath,
|
||||
sourcePath: event.sourcePath,
|
||||
fileFolder: event.fileFolder,
|
||||
});
|
||||
};
|
||||
|
||||
const { logicFunctions, frontComponents } = options.filePaths;
|
||||
|
||||
await esbuildOneShotBuild({
|
||||
appPath: options.appPath,
|
||||
sourcePaths: logicFunctions,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
buildOptions: {
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
outdir: join(options.appPath, OUTPUT_DIR),
|
||||
outExtension: { '.js': '.mjs' },
|
||||
external: LOGIC_FUNCTION_EXTERNAL_MODULES,
|
||||
tsconfig: join(options.appPath, 'tsconfig.json'),
|
||||
sourcemap: true,
|
||||
metafile: true,
|
||||
logLevel: 'silent',
|
||||
banner: NODE_ESM_CJS_BANNER,
|
||||
plugins: [createSdkGeneratedResolverPlugin(options.appPath)],
|
||||
},
|
||||
onFileBuilt: collectFileBuilt,
|
||||
});
|
||||
|
||||
await esbuildOneShotBuild({
|
||||
appPath: options.appPath,
|
||||
sourcePaths: frontComponents,
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
buildOptions: {
|
||||
bundle: true,
|
||||
splitting: false,
|
||||
format: 'esm',
|
||||
outdir: join(options.appPath, OUTPUT_DIR),
|
||||
outExtension: { '.js': '.mjs' },
|
||||
external: FRONT_COMPONENT_EXTERNAL_MODULES,
|
||||
tsconfig: join(options.appPath, 'tsconfig.json'),
|
||||
jsx: 'automatic',
|
||||
sourcemap: true,
|
||||
metafile: true,
|
||||
logLevel: 'silent',
|
||||
plugins: [
|
||||
createSdkGeneratedResolverPlugin(options.appPath),
|
||||
...getFrontComponentBuildPlugins(),
|
||||
],
|
||||
},
|
||||
onFileBuilt: collectFileBuilt,
|
||||
});
|
||||
|
||||
await copyStaticFiles({
|
||||
appPath: options.appPath,
|
||||
fileFolder: FileFolder.PublicAsset,
|
||||
filePaths: options.filePaths.publicAssets,
|
||||
collectFileBuilt,
|
||||
});
|
||||
|
||||
await copyStaticFiles({
|
||||
appPath: options.appPath,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
filePaths: ['package.json', 'yarn.lock'].filter((filePath) =>
|
||||
fs.pathExistsSync(join(options.appPath, filePath)),
|
||||
),
|
||||
collectFileBuilt,
|
||||
});
|
||||
|
||||
return { builtFileInfos };
|
||||
};
|
||||
|
||||
const copyStaticFiles = async ({
|
||||
appPath,
|
||||
fileFolder,
|
||||
filePaths,
|
||||
collectFileBuilt,
|
||||
}: {
|
||||
appPath: string;
|
||||
fileFolder: FileFolder;
|
||||
filePaths: string[];
|
||||
collectFileBuilt: OnFileBuiltCallback;
|
||||
}) => {
|
||||
for (const sourcePath of filePaths) {
|
||||
const absoluteSourcePath = join(appPath, sourcePath);
|
||||
|
||||
if (!(await fs.pathExists(absoluteSourcePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const builtPath = join(OUTPUT_DIR, sourcePath);
|
||||
const absoluteBuiltPath = join(appPath, builtPath);
|
||||
|
||||
await fs.ensureDir(dirname(absoluteBuiltPath));
|
||||
await fs.copy(absoluteSourcePath, absoluteBuiltPath);
|
||||
|
||||
const content = await fs.readFile(absoluteBuiltPath);
|
||||
const checksum = crypto.createHash('md5').update(content).digest('hex');
|
||||
|
||||
collectFileBuilt({
|
||||
fileFolder,
|
||||
builtPath,
|
||||
sourcePath,
|
||||
checksum,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { processEsbuildResult } from '@/cli/utilities/build/common/esbuild-result-processor';
|
||||
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
|
||||
import * as esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export type EsbuildOneShotBuildOptions = {
|
||||
appPath: string;
|
||||
sourcePaths: string[];
|
||||
fileFolder: FileFolder;
|
||||
buildOptions: esbuild.BuildOptions;
|
||||
onFileBuilt: OnFileBuiltCallback;
|
||||
};
|
||||
|
||||
export const esbuildOneShotBuild = async ({
|
||||
appPath,
|
||||
sourcePaths,
|
||||
fileFolder,
|
||||
buildOptions,
|
||||
onFileBuilt,
|
||||
}: EsbuildOneShotBuildOptions): Promise<void> => {
|
||||
if (sourcePaths.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPoints: Record<string, string> = {};
|
||||
|
||||
for (const sourcePath of sourcePaths) {
|
||||
const entryName = sourcePath.replace(/\.tsx?$/, '');
|
||||
entryPoints[entryName] = path.join(appPath, sourcePath);
|
||||
}
|
||||
|
||||
const result = await esbuild.build({
|
||||
...buildOptions,
|
||||
entryPoints,
|
||||
});
|
||||
|
||||
await processEsbuildResult({
|
||||
result,
|
||||
appPath,
|
||||
fileFolder,
|
||||
lastChecksums: new Map(),
|
||||
onFileBuilt,
|
||||
});
|
||||
};
|
||||
@@ -12,9 +12,9 @@ import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-pl
|
||||
import * as esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import {
|
||||
OUTPUT_DIR,
|
||||
NODE_ESM_CJS_BANNER,
|
||||
GENERATED_DIR,
|
||||
NODE_ESM_CJS_BANNER,
|
||||
OUTPUT_DIR,
|
||||
} from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
@@ -195,7 +195,9 @@ export class EsbuildWatcher implements RestartableWatcher {
|
||||
|
||||
// Resolves twenty-sdk/generated to the actual file path so esbuild
|
||||
// bundles it instead of treating it as external (via twenty-sdk/*)
|
||||
const createSdkGeneratedResolverPlugin = (appPath: string): esbuild.Plugin => ({
|
||||
export const createSdkGeneratedResolverPlugin = (
|
||||
appPath: string,
|
||||
): esbuild.Plugin => ({
|
||||
name: 'sdk-generated-resolver',
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /^twenty-sdk\/generated/ }, () => ({
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
APP_ERROR_CODES,
|
||||
type CommandResult,
|
||||
} from '@/cli/public-operations/types';
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { findOrCreateApplication } from '@/cli/utilities/application/find-or-create-application';
|
||||
import { type BuiltFileInfo } from '@/cli/utilities/build/common/build-application';
|
||||
import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest-update-checksums';
|
||||
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { FileUploader } from '@/cli/utilities/file/file-uploader';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export type AppSyncOptions = {
|
||||
appPath: string;
|
||||
workspace?: string;
|
||||
};
|
||||
|
||||
export const synchronizeBuiltApplication = async ({
|
||||
appPath,
|
||||
manifest,
|
||||
builtFileInfos,
|
||||
}: {
|
||||
appPath: string;
|
||||
manifest: Manifest;
|
||||
builtFileInfos: Map<string, BuiltFileInfo>;
|
||||
}): Promise<CommandResult> => {
|
||||
const apiService = new ApiService();
|
||||
|
||||
const ensureResult = await findOrCreateApplication({
|
||||
apiService,
|
||||
manifest,
|
||||
});
|
||||
|
||||
if (!ensureResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: ensureResult.error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const universalIdentifier = manifest.application.universalIdentifier;
|
||||
|
||||
const fileUploader = new FileUploader({
|
||||
applicationUniversalIdentifier: universalIdentifier,
|
||||
appPath,
|
||||
});
|
||||
|
||||
const uploadPromises = Array.from(builtFileInfos.values()).map((fileInfo) =>
|
||||
fileUploader.uploadFile({
|
||||
builtPath: fileInfo.builtPath,
|
||||
fileFolder: fileInfo.fileFolder,
|
||||
}),
|
||||
);
|
||||
|
||||
const uploadResults = await Promise.all(uploadPromises);
|
||||
const failedUploads = uploadResults.filter((result) => !result.success);
|
||||
|
||||
if (failedUploads.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: `Failed to upload ${failedUploads.length} file(s).`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const updatedManifest = manifestUpdateChecksums({
|
||||
manifest,
|
||||
builtFileInfos,
|
||||
});
|
||||
|
||||
await writeManifestToOutput(appPath, updatedManifest);
|
||||
|
||||
const syncResult = await apiService.syncApplication(updatedManifest);
|
||||
|
||||
if (!syncResult.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message: `Sync failed: ${serializeError(syncResult.error)}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { success: true, data: undefined };
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
|
||||
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
|
||||
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
export type BuildAndValidateManifestSuccess = {
|
||||
success: true;
|
||||
manifest: Manifest;
|
||||
filePaths: EntityFilePaths;
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
export type BuildAndValidateManifestFailure = {
|
||||
success: false;
|
||||
errors: string[];
|
||||
};
|
||||
|
||||
export type BuildAndValidateManifestResult =
|
||||
| BuildAndValidateManifestSuccess
|
||||
| BuildAndValidateManifestFailure;
|
||||
|
||||
export const buildAndValidateManifest = async (
|
||||
appPath: string,
|
||||
): Promise<BuildAndValidateManifestResult> => {
|
||||
const result = await buildManifest(appPath);
|
||||
|
||||
if (result.errors.length > 0 || !result.manifest) {
|
||||
return {
|
||||
success: false,
|
||||
errors:
|
||||
result.errors.length > 0
|
||||
? result.errors
|
||||
: ['Failed to build manifest.'],
|
||||
};
|
||||
}
|
||||
|
||||
const validation = manifestValidate(result.manifest);
|
||||
|
||||
if (!validation.isValid) {
|
||||
return {
|
||||
success: false,
|
||||
errors: validation.errors,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
manifest: result.manifest,
|
||||
filePaths: result.filePaths,
|
||||
warnings: validation.warnings,
|
||||
};
|
||||
};
|
||||
+1
-1
@@ -6,7 +6,7 @@ import os from 'os';
|
||||
import path from 'path';
|
||||
import { isDefined, isPlainObject } from 'twenty-shared/utils';
|
||||
|
||||
const MANIFEST_MOCK_MODULES = ['twenty-sdk/ui'];
|
||||
const MANIFEST_MOCK_MODULES = ['twenty-sdk/ui', 'twenty-sdk/generated'];
|
||||
|
||||
const manifestMockPlugin: esbuild.Plugin = {
|
||||
name: 'manifest-mock',
|
||||
|
||||
@@ -163,6 +163,8 @@ export class ClientService {
|
||||
private async writeBarrelIndex(outputDir: string): Promise<void> {
|
||||
const barrelContent = `export { CoreApiClient } from './core/index';
|
||||
export { MetadataApiClient } from './metadata/index';
|
||||
export * as CoreSchema from './core/schema';
|
||||
export * as MetadataSchema from './metadata/schema';
|
||||
`;
|
||||
|
||||
await fs.writeFile(join(outputDir, 'index.ts'), barrelContent);
|
||||
|
||||
+12
-23
@@ -1,6 +1,5 @@
|
||||
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
|
||||
import { buildAndValidateManifest } from '@/cli/utilities/build/manifest/build-and-validate-manifest';
|
||||
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums';
|
||||
import { manifestValidate } from '@/cli/utilities/build/manifest/manifest-validate';
|
||||
import {
|
||||
type OrchestratorState,
|
||||
type OrchestratorStateStepEvent,
|
||||
@@ -37,9 +36,9 @@ export class BuildManifestOrchestratorStep {
|
||||
{ message: 'Building manifest', status: 'info' },
|
||||
];
|
||||
|
||||
const result = await buildManifest(input.appPath);
|
||||
const result = await buildAndValidateManifest(input.appPath);
|
||||
|
||||
if (result.errors.length > 0 || !result.manifest) {
|
||||
if (!result.success) {
|
||||
for (const error of result.errors) {
|
||||
events.push({ message: error, status: 'error' });
|
||||
}
|
||||
@@ -52,23 +51,8 @@ export class BuildManifestOrchestratorStep {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validation = manifestValidate(result.manifest);
|
||||
|
||||
if (!validation.isValid) {
|
||||
for (const validationError of validation.errors) {
|
||||
events.push({ message: validationError, status: 'error' });
|
||||
}
|
||||
|
||||
step.output = { result: null };
|
||||
step.status = 'error';
|
||||
this.state.updatePipeline({ status: 'error' });
|
||||
this.state.applyStepEvents(events);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
for (const warning of validation.warnings) {
|
||||
if (result.warnings.length > 0) {
|
||||
for (const warning of result.warnings) {
|
||||
events.push({ message: `⚠ ${warning}`, status: 'warning' });
|
||||
}
|
||||
}
|
||||
@@ -78,7 +62,12 @@ export class BuildManifestOrchestratorStep {
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
step.output = { result };
|
||||
const buildResult: ManifestBuildResult = {
|
||||
manifest: result.manifest,
|
||||
filePaths: result.filePaths,
|
||||
};
|
||||
|
||||
step.output = { result: buildResult };
|
||||
step.status = 'done';
|
||||
this.state.updatePipeline({
|
||||
appName: result.manifest.application.displayName,
|
||||
@@ -86,6 +75,6 @@ export class BuildManifestOrchestratorStep {
|
||||
this.state.updateEntitiesFromManifest(result.filePaths);
|
||||
this.state.applyStepEvents(events);
|
||||
|
||||
return result;
|
||||
return buildResult;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-37
@@ -1,4 +1,5 @@
|
||||
import { type ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { findOrCreateApplication } from '@/cli/utilities/application/find-or-create-application';
|
||||
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
@@ -35,15 +36,16 @@ export class ResolveApplicationOrchestratorStep {
|
||||
step.status = 'in_progress';
|
||||
this.notify();
|
||||
|
||||
const universalIdentifier = input.manifest.application.universalIdentifier;
|
||||
const result = await findOrCreateApplication({
|
||||
apiService: this.apiService,
|
||||
manifest: input.manifest,
|
||||
applicationRegistrationId: input.applicationRegistrationId,
|
||||
});
|
||||
|
||||
const findResult =
|
||||
await this.apiService.findOneApplication(universalIdentifier);
|
||||
|
||||
if (!findResult.success) {
|
||||
if (!result.success) {
|
||||
this.state.applyStepEvents([
|
||||
{
|
||||
message: `Failed to find application ${universalIdentifier}`,
|
||||
message: result.error,
|
||||
status: 'error',
|
||||
},
|
||||
]);
|
||||
@@ -53,44 +55,17 @@ export class ResolveApplicationOrchestratorStep {
|
||||
return step.output;
|
||||
}
|
||||
|
||||
if (findResult.data) {
|
||||
step.output = {
|
||||
applicationId: findResult.data.id,
|
||||
universalIdentifier: findResult.data.universalIdentifier,
|
||||
};
|
||||
step.status = 'done';
|
||||
this.notify();
|
||||
|
||||
return step.output;
|
||||
}
|
||||
|
||||
const createResult = await this.apiService.createApplication(
|
||||
input.manifest,
|
||||
{ applicationRegistrationId: input.applicationRegistrationId },
|
||||
);
|
||||
|
||||
if (!createResult.success) {
|
||||
if (result.created) {
|
||||
this.state.applyStepEvents([
|
||||
{ message: 'Creating application', status: 'info' },
|
||||
{
|
||||
message: `Application creation failed with error ${JSON.stringify(createResult.error, null, 2)}`,
|
||||
status: 'error',
|
||||
},
|
||||
{ message: 'Application created', status: 'success' },
|
||||
]);
|
||||
step.status = 'error';
|
||||
this.state.updatePipeline({ status: 'error' });
|
||||
|
||||
return step.output;
|
||||
}
|
||||
|
||||
step.output = {
|
||||
applicationId: createResult.data!.id,
|
||||
universalIdentifier: createResult.data!.universalIdentifier,
|
||||
applicationId: result.applicationId,
|
||||
universalIdentifier: result.universalIdentifier,
|
||||
};
|
||||
this.state.applyStepEvents([
|
||||
{ message: 'Creating application', status: 'info' },
|
||||
{ message: 'Application created', status: 'success' },
|
||||
]);
|
||||
step.status = 'done';
|
||||
this.notify();
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@ export default defineConfig(() => {
|
||||
emptyOutDir: false,
|
||||
outDir: 'dist',
|
||||
lib: {
|
||||
entry: ['src/sdk/index.ts', 'src/cli/cli.ts'],
|
||||
entry: {
|
||||
index: 'src/sdk/index.ts',
|
||||
cli: 'src/cli/cli.ts',
|
||||
operations: 'src/cli/public-operations/index.ts',
|
||||
},
|
||||
name: 'twenty-sdk',
|
||||
},
|
||||
rollupOptions: {
|
||||
|
||||
@@ -22418,15 +22418,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/plugin-emotion@npm:14.6.0":
|
||||
version: 14.6.0
|
||||
resolution: "@swc/plugin-emotion@npm:14.6.0"
|
||||
dependencies:
|
||||
"@swc/counter": "npm:^0.1.3"
|
||||
checksum: 10c0/0bc0dd199d46a42d19d429ca823ec9cd1666fe76b8d130cf50c631bf0e22a4e616bb7366e505b62bbd6159172bc50e5c396a3990c8a617a94b40b4f8c4d2c9fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@swc/types@npm:^0.1.24, @swc/types@npm:^0.1.25":
|
||||
version: 0.1.25
|
||||
resolution: "@swc/types@npm:0.1.25"
|
||||
@@ -58173,14 +58164,10 @@ __metadata:
|
||||
dependencies:
|
||||
"@babel/preset-env": "npm:^7.26.9"
|
||||
"@babel/preset-react": "npm:^7.26.3"
|
||||
"@emotion/is-prop-valid": "npm:^1.3.0"
|
||||
"@emotion/react": "npm:^11.11.1"
|
||||
"@emotion/styled": "npm:^11.11.0"
|
||||
"@linaria/react": "npm:^6.2.1"
|
||||
"@monaco-editor/react": "npm:^4.7.0"
|
||||
"@prettier/sync": "npm:^0.5.2"
|
||||
"@sniptt/guards": "npm:^0.2.0"
|
||||
"@swc/plugin-emotion": "npm:14.6.0"
|
||||
"@tabler/icons-react": "npm:^3.31.0"
|
||||
"@types/babel__preset-env": "npm:^7"
|
||||
"@types/react": "npm:^18.2.39"
|
||||
|
||||
Reference in New Issue
Block a user