Make ConnectionProvider a true SyncableEntity (#20232)

## Summary

PR #20181 left `ConnectionProvider` in the `SyncableEntity` enum but
bypassing the standard sync pipeline — manifest sync called the bespoke
`ApplicationOAuthProviderService.upsertManyFromManifest()` instead of
going through the workspace-migration orchestrator like every other
SyncableEntity. Anything that assumed *"all SyncableEntity values flow
through the same pipeline"* (dev UI sync tracking, verification tooling)
was wrong about ConnectionProvider — that's the inconsistency this PR
closes.

This PR follows the `.cursor/skills/syncable-entity-*` guides
religiously, all six steps.

## What changes

**Step 1 — Types & Constants** (`@syncable-entity-types-and-constants`)
- Add `connectionProvider` to `ALL_METADATA_NAME` (twenty-shared)
- Make `ApplicationOAuthProviderEntity` extend `SyncableEntity` (drops
the ad-hoc columns since the base class provides them, adds `deletedAt`,
drops the old `(applicationId, universalIdentifier)` unique in favour of
SyncableEntity's `(workspaceId, universalIdentifier)`)
- `FlatConnectionProvider`, `FlatConnectionProviderMaps`,
`FLAT_CONNECTION_PROVIDER_EDITABLE_PROPERTIES`,
`UniversalFlatConnectionProvider`, six action types
- Register in **all** the central registries:
`AllFlatEntityTypesByMetadataName`,
`ALL_METADATA_ENTITY_BY_METADATA_NAME`,
`ALL_ENTITY_PROPERTIES_CONFIGURATION`, `ALL_MANY_TO_ONE_*`,
`ALL_ONE_TO_MANY_*`, `ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION`,
`ALL_METADATA_SERIALIZED_RELATION`,
`ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION`,
`WORKSPACE_CACHE_KEYS_V2` (`flatConnectionProviderMaps`),
`METADATA_EVENTS_TO_EMIT`
- `case 'connectionProvider':` in seven discriminated-union switches
(`derive-metadata-events-*`, `optimistically-apply-*`,
`enrich-create-*`)

**Step 2 — Cache & Transform** (`@syncable-entity-cache-and-transform`)
- `WorkspaceFlatConnectionProviderMapCacheService` (extends
`WorkspaceCacheProvider`, decorated with `@WorkspaceCache`,
soft-delete-aware)
- `fromConnectionProviderEntityToFlatConnectionProvider` util
- `fromConnectionProviderManifestToUniversalFlatConnectionProvider` util
- `FlatConnectionProviderModule` wires the cache service
- Wired the manifest converter into
`compute-application-manifest-all-universal-flat-entity-maps`

**Step 3 — Builder & Validation**
(`@syncable-entity-builder-and-validation`)
- `FlatConnectionProviderValidatorService` — never throws, returns error
arrays; uses indexed `byUniversalIdentifier` for the (name,
applicationUniversalIdentifier) uniqueness check (no
`Object.values().find()` on the hot path)
- `WorkspaceMigrationConnectionProviderActionsBuilderService`
- Registered in both validators-module + builder-module
- **Wired into the orchestrator** (the most-commonly-forgotten step per
the rule) — constructor inject, destructure
`flatConnectionProviderMaps`, `validateAndBuild`, append actions to the
final migration

**Step 4 — Runner & Actions** (`@syncable-entity-runner-and-actions`)
- Three handlers (create / update / delete) using the canonical
`WorkspaceMigrationRunnerActionHandler` mixin
- Registered in `WorkspaceSchemaMigrationRunnerActionHandlersModule`

**Step 5 — Integration** (`@syncable-entity-integration`)
- Delete the `upsertManyFromManifest` bypass on
`ApplicationOAuthProviderService`
- Remove the bypass call from `ApplicationSyncService` — manifest sync
now flows through the standard pipeline
- Drop `ApplicationOAuthProviderModule` from `ApplicationManifestModule`
(no longer needed)
- Import `FlatConnectionProviderModule` from
`ApplicationOAuthProviderModule` to keep the cache discoverable
- 3 new exception codes: `INVALID_CONNECTION_PROVIDER_INPUT`,
`CONNECTION_PROVIDER_NOT_FOUND`,
`CONNECTION_PROVIDER_NAME_ALREADY_EXISTS`

**Migration**
- Generated via `database:migrate:generate` (instance command
`1777896012579`): drops the old `(applicationId, universalIdentifier)`
unique constraint, adds `deletedAt` column, adds the `(workspaceId,
universalIdentifier)` unique index that `SyncableEntity` requires.
- Verified clean — a second `migrate:generate` pass produces zero drift.

**Step 6 — Tests** (`@syncable-entity-testing`)
- 3 new specs for the manifest converter (defaults, optional fields,
all-fields)
- All 32 existing OAuth-provider tests still pass
- ConnectionProvider has no end-user GraphQL CRUD (it's manifest-driven
only), so the GraphQL integration suite that other SyncableEntities ship
doesn't apply here

**Codegen**
- Regenerated GraphQL artifacts (twenty-front + twenty-client-sdk)
against the live schema

## Why this matters

Before:
- `ConnectionProvider` claimed to be a `SyncableEntity` (in the enum)
- But the entity didn't extend `SyncableEntity`
- And the manifest sync bypassed the standard pipeline
- → Verification tooling, dev UI sync tracking, anything iterating over
`ALL_METADATA_NAME` got inconsistent behaviour

After:
- `ConnectionProvider` is a `SyncableEntity` end-to-end
- Single sync path through the workspace-migration orchestrator (same as
`agent`, `skill`, `frontComponent`, `webhook`, …)
- One mental model

## Out of scope (deliberate)

- **Renaming the table** from `applicationOAuthProvider` to
`connectionProvider` — the `metadataName` is `connectionProvider` (what
consumers see in code); the table name is internal. A rename would
balloon this PR with mechanical churn unrelated to the sync-pipeline
wiring. Worth doing as a follow-up.
- **`applicationVariable` SyncableEntity conversion** — the other
manifest-sync holdout. Tracked in #20215.

## Test plan

- [ ] Migration up/down clean against fresh DB
- [ ] Install an app whose manifest declares connection providers —
providers appear in the workspace
- [ ] Re-deploy the app with one provider added, one removed, one
renamed → all reconciled correctly via the sync pipeline
- [ ] Verify the dev-UI sync-tracking page shows ConnectionProvider
entries the same way it shows agents/skills/etc
- [ ] OAuth flow still works (existing connections, new connections,
reconnect, list/get from SDK) — should be unchanged since the runtime
code path didn't move

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-05-05 14:04:15 +02:00
committed by GitHub
parent 59107b5b23
commit e3be1f4971
106 changed files with 2076 additions and 1746 deletions
@@ -170,7 +170,8 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
if (!isDefined(idByUniversalIdentifier)) {
return action;
}
@@ -16,6 +16,7 @@ import { crossEntityTransversalValidation } from 'src/engine/workspace-manager/w
import { mergeOrchestratorFailureReports } from 'src/engine/workspace-manager/workspace-migration/utils/merge-orchestrator-failure-reports.util';
import { WorkspaceMigrationAgentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/agent/workspace-migration-agent-actions-builder.service';
import { WorkspaceMigrationCommandMenuItemActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/command-menu-item/workspace-migration-command-menu-item-actions-builder.service';
import { WorkspaceMigrationConnectionProviderActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/workspace-migration-connection-provider-actions-builder.service';
import { WorkspaceMigrationFieldPermissionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field-permission/workspace-migration-field-permission-actions-builder.service';
import { WorkspaceMigrationFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/field/workspace-migration-field-actions-builder.service';
import { WorkspaceMigrationFrontComponentActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/front-component/workspace-migration-front-component-actions-builder.service';
@@ -72,6 +73,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService: WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
private readonly workspaceMigrationFrontComponentActionsBuilderService: WorkspaceMigrationFrontComponentActionsBuilderService,
private readonly workspaceMigrationWebhookActionsBuilderService: WorkspaceMigrationWebhookActionsBuilderService,
private readonly workspaceMigrationConnectionProviderActionsBuilderService: WorkspaceMigrationConnectionProviderActionsBuilderService,
) {}
private setupOptimisticCache({
@@ -164,6 +166,7 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatPageLayoutTabMaps,
flatFrontComponentMaps,
flatWebhookMaps,
flatConnectionProviderMaps,
} = fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
@@ -823,6 +826,34 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatConnectionProviderMaps)) {
const {
from: fromFlatConnectionProviderMaps,
to: toFlatConnectionProviderMaps,
} = flatConnectionProviderMaps;
const connectionProviderResult =
await this.workspaceMigrationConnectionProviderActionsBuilderService.validateAndBuild(
{
additionalCacheDataMaps,
from: fromFlatConnectionProviderMaps,
to: toFlatConnectionProviderMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: optimisticAllFlatEntityMaps,
workspaceId,
},
);
if (connectionProviderResult.status === 'fail') {
orchestratorFailureReport.connectionProvider.push(
...connectionProviderResult.errors,
);
} else {
orchestratorActionsReport.connectionProvider =
connectionProviderResult.actions;
}
}
const crossEntityFailureReport = crossEntityTransversalValidation({
optimisticUniversalFlatMaps: optimisticAllFlatEntityMaps,
orchestratorActionsReport,
@@ -999,6 +1030,12 @@ export class WorkspaceMigrationBuildOrchestratorService {
...aggregatedOrchestratorActionsReport.webhook.create,
...aggregatedOrchestratorActionsReport.webhook.update,
///
// Connection providers
...aggregatedOrchestratorActionsReport.connectionProvider.delete,
...aggregatedOrchestratorActionsReport.connectionProvider.create,
...aggregatedOrchestratorActionsReport.connectionProvider.update,
///
],
},
};
@@ -1,9 +1,10 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ALL_UNIVERSAL_FLAT_ENTITY_FOREIGN_KEY_AGGREGATOR_PROPERTIES should match snapshot 1`] = `
{
"agent": [],
"commandMenuItem": [],
"connectionProvider": [],
"fieldMetadata": [
"viewSortUniversalIdentifiers",
"fieldPermissionUniversalIdentifiers",
@@ -42,6 +42,7 @@ export const ALL_JSONB_PROPERTIES_WITH_SERIALIZED_RELATION_BY_METADATA_NAME = {
objectPermission: {},
fieldPermission: {},
frontComponent: {},
connectionProvider: {},
} as const satisfies {
[P in AllMetadataName]: Partial<{
// TODO prastoin: improve strict typing to recursively serach for nested SerializedRelation
@@ -0,0 +1,7 @@
import { type ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
import { type UniversalFlatEntityFrom } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
export type UniversalFlatConnectionProvider = UniversalFlatEntityFrom<
ConnectionProviderEntity,
'connectionProvider'
>;
@@ -0,0 +1,24 @@
import { type BaseFlatCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-create-workspace-migration-action.type';
import { type BaseFlatDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-delete-workspace-migration-action.type';
import { type BaseFlatUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-flat-update-workspace-migration-action.type';
import { type BaseUniversalCreateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-create-workspace-migration-action.type';
import { type BaseUniversalDeleteWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-delete-workspace-migration-action.type';
import { type BaseUniversalUpdateWorkspaceMigrationAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/base-universal-update-workspace-migration-action.type';
export type FlatCreateConnectionProviderAction =
BaseFlatCreateWorkspaceMigrationAction<'connectionProvider'>;
export type UniversalCreateConnectionProviderAction =
BaseUniversalCreateWorkspaceMigrationAction<'connectionProvider'>;
export type FlatUpdateConnectionProviderAction =
BaseFlatUpdateWorkspaceMigrationAction<'connectionProvider'>;
export type UniversalUpdateConnectionProviderAction =
BaseUniversalUpdateWorkspaceMigrationAction<'connectionProvider'>;
export type UniversalDeleteConnectionProviderAction =
BaseUniversalDeleteWorkspaceMigrationAction<'connectionProvider'>;
export type FlatDeleteConnectionProviderAction =
BaseFlatDeleteWorkspaceMigrationAction<'connectionProvider'>;
@@ -0,0 +1,122 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UniversalUpdateConnectionProviderAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/types/workspace-migration-connection-provider-action.type';
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/services/workspace-entity-migration-builder.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
import { UniversalFlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-result.type';
import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-connection-provider-validator.service';
@Injectable()
export class WorkspaceMigrationConnectionProviderActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
typeof ALL_METADATA_NAME.connectionProvider
> {
constructor(
private readonly flatConnectionProviderValidatorService: FlatConnectionProviderValidatorService,
) {
super(ALL_METADATA_NAME.connectionProvider);
}
protected validateFlatEntityCreation(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.connectionProvider
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.connectionProvider,
'create'
> {
const validationResult =
this.flatConnectionProviderValidatorService.validateFlatConnectionProviderCreation(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatConnectionProviderToValidate } = args;
return {
status: 'success',
action: {
type: 'create',
metadataName: 'connectionProvider',
flatEntity: flatConnectionProviderToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.connectionProvider
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.connectionProvider,
'delete'
> {
const validationResult =
this.flatConnectionProviderValidatorService.validateFlatConnectionProviderDeletion(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatConnectionProviderToValidate } = args;
return {
status: 'success',
action: {
type: 'delete',
metadataName: 'connectionProvider',
universalIdentifier:
flatConnectionProviderToValidate.universalIdentifier,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.connectionProvider
>,
): UniversalFlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.connectionProvider,
'update'
> {
const validationResult =
this.flatConnectionProviderValidatorService.validateFlatConnectionProviderUpdate(
args,
);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { universalIdentifier, flatEntityUpdate } = args;
const updateConnectionProviderAction: UniversalUpdateConnectionProviderAction =
{
type: 'update',
metadataName: 'connectionProvider',
universalIdentifier,
update: flatEntityUpdate,
};
return {
status: 'success',
action: updateConnectionProviderAction,
};
}
}
@@ -0,0 +1,173 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { ConnectionProviderExceptionCode } from 'src/engine/core-modules/application/connection-provider/connection-provider-exception-code.enum';
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/types/failed-flat-entity-validation.type';
import { getEmptyFlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/utils/get-flat-entity-validation-error.util';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-update-validation-args.type';
import { type UniversalFlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/universal-flat-entity-validation-args.type';
@Injectable()
export class FlatConnectionProviderValidatorService {
public validateFlatConnectionProviderCreation({
flatEntityToValidate: flatConnectionProvider,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatConnectionProviderMaps: optimisticFlatConnectionProviderMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.connectionProvider
>): FailedFlatEntityValidation<'connectionProvider', 'create'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatConnectionProvider.universalIdentifier,
name: flatConnectionProvider.name,
},
metadataName: 'connectionProvider',
type: 'create',
});
if (!isNonEmptyString(flatConnectionProvider.name)) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.INVALID_CONNECTION_PROVIDER_INPUT,
message: t`Connection provider name is required`,
userFriendlyMessage: msg`Connection provider name is required`,
});
}
if (!isNonEmptyString(flatConnectionProvider.displayName)) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.INVALID_CONNECTION_PROVIDER_INPUT,
message: t`Connection provider displayName is required`,
userFriendlyMessage: msg`Connection provider display name is required`,
});
}
if (flatConnectionProvider.type === 'oauth') {
const oauthConfig = flatConnectionProvider.oauthConfig;
if (!isDefined(oauthConfig)) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.INVALID_CONNECTION_PROVIDER_INPUT,
message: t`Connection provider with type 'oauth' is missing oauthConfig`,
userFriendlyMessage: msg`OAuth connection provider is missing its oauth config block`,
});
} else {
const requiredOAuthFields: Array<{
key: keyof typeof oauthConfig;
label: string;
}> = [
{ key: 'authorizationEndpoint', label: 'authorizationEndpoint' },
{ key: 'tokenEndpoint', label: 'tokenEndpoint' },
{ key: 'clientIdVariable', label: 'clientIdVariable' },
{ key: 'clientSecretVariable', label: 'clientSecretVariable' },
];
for (const { key, label } of requiredOAuthFields) {
if (!isNonEmptyString(oauthConfig[key])) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.INVALID_CONNECTION_PROVIDER_INPUT,
message: t`Connection provider oauthConfig.${label} is required`,
userFriendlyMessage: msg`OAuth ${label} is required`,
});
}
}
}
}
const existingByName = Object.values(
optimisticFlatConnectionProviderMaps.byUniversalIdentifier,
).find(
(existing) =>
isDefined(existing) &&
existing.name === flatConnectionProvider.name &&
existing.applicationUniversalIdentifier ===
flatConnectionProvider.applicationUniversalIdentifier &&
existing.universalIdentifier !==
flatConnectionProvider.universalIdentifier,
);
if (isDefined(existingByName)) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.CONNECTION_PROVIDER_NAME_ALREADY_EXISTS,
message: t`Connection provider with name ${flatConnectionProvider.name} already exists for this application`,
userFriendlyMessage: msg`A connection provider with this name already exists for this application`,
});
}
return validationResult;
}
public validateFlatConnectionProviderDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatConnectionProviderMaps: optimisticFlatConnectionProviderMaps,
},
}: UniversalFlatEntityValidationArgs<
typeof ALL_METADATA_NAME.connectionProvider
>): FailedFlatEntityValidation<'connectionProvider', 'delete'> {
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier: flatEntityToValidate.universalIdentifier,
name: flatEntityToValidate.name,
},
metadataName: 'connectionProvider',
type: 'delete',
});
const existingConnectionProvider = findFlatEntityByUniversalIdentifier({
universalIdentifier: flatEntityToValidate.universalIdentifier,
flatEntityMaps: optimisticFlatConnectionProviderMaps,
});
if (!isDefined(existingConnectionProvider)) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.CONNECTION_PROVIDER_NOT_FOUND,
message: t`Connection provider not found`,
userFriendlyMessage: msg`Connection provider not found`,
});
return validationResult;
}
return validationResult;
}
public validateFlatConnectionProviderUpdate({
universalIdentifier,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatConnectionProviderMaps: optimisticFlatConnectionProviderMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.connectionProvider
>): FailedFlatEntityValidation<'connectionProvider', 'update'> {
const fromFlatConnectionProvider = findFlatEntityByUniversalIdentifier({
universalIdentifier,
flatEntityMaps: optimisticFlatConnectionProviderMaps,
});
const validationResult = getEmptyFlatEntityValidationError({
flatEntityMinimalInformation: {
universalIdentifier,
},
metadataName: 'connectionProvider',
type: 'update',
});
if (!isDefined(fromFlatConnectionProvider)) {
validationResult.errors.push({
code: ConnectionProviderExceptionCode.CONNECTION_PROVIDER_NOT_FOUND,
message: t`Connection provider not found`,
userFriendlyMessage: msg`Connection provider not found`,
});
return validationResult;
}
return validationResult;
}
}
@@ -30,6 +30,7 @@ import { FlatViewGroupValidatorService } from 'src/engine/workspace-manager/work
import { FlatViewSortValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-sort-validator.service';
import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-view-validator.service';
import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-webhook-validator.service';
import { FlatConnectionProviderValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-connection-provider-validator.service';
@Module({
imports: [FeatureFlagModule],
@@ -63,6 +64,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp
FlatRowLevelPermissionPredicateGroupValidatorService,
FlatFrontComponentValidatorService,
FlatWebhookValidatorService,
FlatConnectionProviderValidatorService,
],
exports: [
FlatViewValidatorService,
@@ -93,6 +95,7 @@ import { FlatWebhookValidatorService } from 'src/engine/workspace-manager/worksp
FlatRowLevelPermissionPredicateGroupValidatorService,
FlatFrontComponentValidatorService,
FlatWebhookValidatorService,
FlatConnectionProviderValidatorService,
],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
@@ -29,6 +29,7 @@ import { WorkspaceMigrationViewGroupActionsBuilderService } from 'src/engine/wor
import { WorkspaceMigrationViewSortActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view-sort/workspace-migration-view-sort-actions.builder.service';
import { WorkspaceMigrationViewActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/view/workspace-migration-view-actions-builder.service';
import { WorkspaceMigrationWebhookActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/webhook/workspace-migration-webhook-actions-builder.service';
import { WorkspaceMigrationConnectionProviderActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/workspace-migration-connection-provider-actions-builder.service';
import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module';
@Module({
@@ -62,6 +63,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationRowLevelPermissionPredicateGroupActionsBuilderService,
WorkspaceMigrationFrontComponentActionsBuilderService,
WorkspaceMigrationWebhookActionsBuilderService,
WorkspaceMigrationConnectionProviderActionsBuilderService,
],
exports: [
WorkspaceMigrationViewActionsBuilderService,
@@ -92,6 +94,7 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
FlatFieldMetadataTypeValidatorService,
WorkspaceMigrationFrontComponentActionsBuilderService,
WorkspaceMigrationWebhookActionsBuilderService,
WorkspaceMigrationConnectionProviderActionsBuilderService,
],
})
export class WorkspaceMigrationBuilderModule {}
@@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common';
import { v4 } from 'uuid';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { getUniversalFlatEntityEmptyForeignKeyAggregators } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/reset-universal-flat-entity-foreign-key-aggregators.util';
import {
FlatCreateConnectionProviderAction,
UniversalCreateConnectionProviderAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/types/workspace-migration-connection-provider-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateConnectionProviderActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create',
'connectionProvider',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction({
action,
flatApplication,
workspaceId,
}: WorkspaceMigrationActionRunnerArgs<UniversalCreateConnectionProviderAction>): Promise<FlatCreateConnectionProviderAction> {
const emptyUniversalForeignKeyAggregators =
getUniversalFlatEntityEmptyForeignKeyAggregators({
metadataName: 'connectionProvider',
});
return {
...action,
flatEntity: {
...action.flatEntity,
applicationId: flatApplication.id,
id: action.id ?? v4(),
workspaceId,
...emptyUniversalForeignKeyAggregators,
},
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatCreateConnectionProviderAction>,
): Promise<void> {
const { flatAction, queryRunner } = context;
const { flatEntity } = flatAction;
await this.insertFlatEntitiesInRepository({
queryRunner,
flatEntities: [flatEntity],
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatCreateConnectionProviderAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
import {
FlatDeleteConnectionProviderAction,
UniversalDeleteConnectionProviderAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/types/workspace-migration-connection-provider-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteConnectionProviderActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete',
'connectionProvider',
) {
constructor() {
super();
}
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalDeleteConnectionProviderAction>,
): Promise<FlatDeleteConnectionProviderAction> {
return this.transpileUniversalDeleteActionToFlatDeleteAction(context);
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatDeleteConnectionProviderAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const connectionProviderRepository =
queryRunner.manager.getRepository<ConnectionProviderEntity>(
ConnectionProviderEntity,
);
await connectionProviderRepository.delete({
id: flatAction.entityId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatDeleteConnectionProviderAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,68 @@
import { Injectable } from '@nestjs/common';
import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface';
import { ConnectionProviderEntity } from 'src/engine/core-modules/application/connection-provider/connection-provider.entity';
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-update-relation-identifiers-to-ids.util';
import {
FlatUpdateConnectionProviderAction,
UniversalUpdateConnectionProviderAction,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/connection-provider/types/workspace-migration-connection-provider-action.type';
import {
WorkspaceMigrationActionRunnerArgs,
WorkspaceMigrationActionRunnerContext,
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type';
@Injectable()
export class UpdateConnectionProviderActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update',
'connectionProvider',
) {
override async transpileUniversalActionToFlatAction(
context: WorkspaceMigrationActionRunnerArgs<UniversalUpdateConnectionProviderAction>,
): Promise<FlatUpdateConnectionProviderAction> {
const { action, allFlatEntityMaps } = context;
const flatConnectionProvider = findFlatEntityByUniversalIdentifierOrThrow({
flatEntityMaps: allFlatEntityMaps.flatConnectionProviderMaps,
universalIdentifier: action.universalIdentifier,
});
const update = resolveUniversalUpdateRelationIdentifiersToIds({
metadataName: 'connectionProvider',
universalUpdate: action.update,
allFlatEntityMaps,
});
return {
type: 'update',
metadataName: 'connectionProvider',
entityId: flatConnectionProvider.id,
update,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerContext<FlatUpdateConnectionProviderAction>,
): Promise<void> {
const { flatAction, queryRunner, workspaceId } = context;
const { entityId, update } = flatAction;
const connectionProviderRepository =
queryRunner.manager.getRepository<ConnectionProviderEntity>(
ConnectionProviderEntity,
);
await connectionProviderRepository.update(
{ id: entityId, workspaceId },
update,
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerContext<FlatUpdateConnectionProviderAction>,
): Promise<void> {
return;
}
}
@@ -84,6 +84,9 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
import { CreateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/create-webhook-action-handler.service';
import { DeleteWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/delete-webhook-action-handler.service';
import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/webhook/services/update-webhook-action-handler.service';
import { CreateConnectionProviderActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/connection-provider/services/create-connection-provider-action-handler.service';
import { DeleteConnectionProviderActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/connection-provider/services/delete-connection-provider-action-handler.service';
import { UpdateConnectionProviderActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/connection-provider/services/update-connection-provider-action-handler.service';
@Module({
imports: [
@@ -198,6 +201,9 @@ import { UpdateWebhookActionHandlerService } from 'src/engine/workspace-manager/
CreateWebhookActionHandlerService,
UpdateWebhookActionHandlerService,
DeleteWebhookActionHandlerService,
CreateConnectionProviderActionHandlerService,
UpdateConnectionProviderActionHandlerService,
DeleteConnectionProviderActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -28,4 +28,5 @@ export const METADATA_EVENTS_TO_EMIT = {
logicFunction: true,
viewSort: true,
webhook: false,
connectionProvider: true,
} as const satisfies { [P in AllMetadataName]: boolean };
@@ -93,7 +93,8 @@ const deriveAllMetadataEventsFromCreateAction = (
case 'objectPermission':
case 'fieldPermission':
case 'viewSort':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
return [
{
type: 'created',
@@ -57,7 +57,8 @@ const deriveAllMetadataEventsFromDeleteAction = ({
case 'objectPermission':
case 'fieldPermission':
case 'viewSort':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
MetadataFlatEntity<typeof flatAction.metadataName>
>({
@@ -135,7 +135,8 @@ const deriveAllMetadataEventsFromUpdateAction = ({
case 'objectPermission':
case 'fieldPermission':
case 'viewSort':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
flatAction.metadataName,
);
@@ -71,7 +71,8 @@ export const optimisticallyApplyCreateActionOnAllFlatEntityMaps = ({
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
addFlatEntityToFlatEntityAndRelatedEntityMapsThroughMutationOrThrow({
flatEntity: flatAction.flatEntity,
flatEntityAndRelatedMapsToMutate: allFlatEntityMaps,
@@ -43,7 +43,8 @@ export const optimisticallyApplyDeleteActionOnAllFlatEntityMaps = ({
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
const flatEntityToDelete = findFlatEntityByIdInFlatEntityMapsOrThrow<
MetadataFlatEntity<typeof flatAction.metadataName>
>({
@@ -64,7 +64,8 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
case 'permissionFlag':
case 'objectPermission':
case 'fieldPermission':
case 'webhook': {
case 'webhook':
case 'connectionProvider': {
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
flatAction.metadataName,
);