Refactor application module architecture for clarity and explicitness (#18432)

## Summary

- **Module reorganization**: Moved `ApplicationUpgradeService` and cron
jobs to `application-upgrade/`, `ApplicationSyncService` to
`application-manifest/`, and
`runWorkspaceMigration`/`uninstallApplication` mutations to the manifest
resolver — each module now has a single clear responsibility.
- **Explicit install flow**: Removed implicit `ApplicationEntity`
creation from `ApplicationSyncService`. The install service and dev
resolver now explicitly create the `ApplicationEntity` before syncing.
npm packages are resolved at registration time to extract manifest
metadata (universalIdentifier, name, description, etc.), eliminating the
`reconcileUniversalIdentifier` hack.
- **Better error handling**: Frontend hooks now surface actual server
error messages in snackbars instead of swallowing them. Replaced the
ugly `ConfirmationModal` for transfer ownership with a proper form
modal. Fixed `SettingsAdminTableCard` row height overflow and corrected
the `yarn-engine` asset path.

## Test plan
- [ ] Register an npm package — verify manifest metadata (name,
description, universalIdentifier) is extracted correctly
- [ ] Install a registered npm app on a workspace — verify
ApplicationEntity is created and sync succeeds
- [ ] Test `app:dev` CLI flow — verify local app registration and sync
work
- [ ] Upload a tarball — verify registration and install flow
- [ ] Transfer ownership — verify the new modal UX works
- [ ] Verify error messages appear correctly in snackbars when
operations fail


Made with [Cursor](https://cursor.com)
This commit is contained in:
Félix Malfait
2026-03-06 08:45:08 +01:00
committed by GitHub
parent 90cced0e74
commit 514d0017ea
184 changed files with 3179 additions and 1382 deletions
@@ -4,7 +4,7 @@ import { type CommonResponseBody } from 'test/integration/metadata/types/common-
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type ApplicationTokenPairDTO } from 'src/engine/core-modules/application/dtos/application-token-pair.dto';
import { type ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
export const generateApplicationToken = async ({
applicationId,
@@ -1,13 +1,8 @@
import gql from 'graphql-tag';
export type InstallApplicationFactoryInput = {
workspaceMigration: {
actions: {
type: 'delete';
metadataName: string;
universalIdentifier: string;
}[];
};
appRegistrationId: string;
version?: string;
};
export const installApplicationQueryFactory = ({
@@ -16,11 +11,15 @@ export const installApplicationQueryFactory = ({
input: InstallApplicationFactoryInput;
}) => ({
query: gql`
mutation InstallApplication($workspaceMigration: WorkspaceMigrationInput!) {
installApplication(workspaceMigration: $workspaceMigration)
mutation InstallApplication($appRegistrationId: String!, $version: String) {
installApplication(
appRegistrationId: $appRegistrationId
version: $version
)
}
`,
variables: {
workspaceMigration: input.workspaceMigration,
appRegistrationId: input.appRegistrationId,
version: input.version,
},
});
@@ -1,6 +1,9 @@
import { createOneApplication } from 'test/integration/metadata/suites/application/utils/create-one-application.util';
import crypto from 'crypto';
import { uploadApplicationFile } from 'test/integration/metadata/suites/application/utils/upload-application-file.util';
const TEST_WORKSPACE_ID = '20202020-1c25-4d02-bf25-6aeccf7ea419';
export const setupApplicationForSync = async ({
applicationUniversalIdentifier,
name,
@@ -12,16 +15,46 @@ export const setupApplicationForSync = async ({
description: string;
sourcePath: string;
}) => {
await createOneApplication({
universalIdentifier: applicationUniversalIdentifier,
name,
description,
version: '1.0.0',
sourcePath,
expectToFail: false,
});
const registrationId = crypto.randomUUID();
const applicationId = crypto.randomUUID();
const oAuthClientId = crypto.randomUUID();
await globalThis.testDataSource.query(
`INSERT INTO core."applicationRegistration"
(id, "universalIdentifier", name, description, "oAuthClientId",
"oAuthRedirectUris", "oAuthScopes", "workspaceId", "sourceType")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
registrationId,
applicationUniversalIdentifier,
name,
description,
oAuthClientId,
[],
[],
TEST_WORKSPACE_ID,
'local',
],
);
await globalThis.testDataSource.query(
`INSERT INTO core."application"
(id, "universalIdentifier", name, description, version, "sourcePath",
"sourceType", "workspaceId", "applicationRegistrationId")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
applicationId,
applicationUniversalIdentifier,
name,
description,
'1.0.0',
sourcePath,
'local',
TEST_WORKSPACE_ID,
registrationId,
],
);
// File upload uses multipart which requires real timers
jest.useRealTimers();
const packageJson = JSON.stringify({