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:
+2
-1
@@ -170,7 +170,8 @@ export const enrichCreateWorkspaceMigrationActionsWithIds = ({
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook': {
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
if (!isDefined(idByUniversalIdentifier)) {
|
||||
return action;
|
||||
}
|
||||
|
||||
+37
@@ -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,
|
||||
///
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
+2
-1
@@ -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",
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
+7
@@ -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'
|
||||
>;
|
||||
+24
@@ -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'>;
|
||||
+122
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+173
@@ -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;
|
||||
}
|
||||
}
|
||||
+3
@@ -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 {}
|
||||
|
||||
+3
@@ -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 {}
|
||||
|
||||
+65
@@ -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;
|
||||
}
|
||||
}
|
||||
+51
@@ -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;
|
||||
}
|
||||
}
|
||||
+68
@@ -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;
|
||||
}
|
||||
}
|
||||
+6
@@ -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 {}
|
||||
|
||||
+1
@@ -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 };
|
||||
|
||||
+2
-1
@@ -93,7 +93,8 @@ const deriveAllMetadataEventsFromCreateAction = (
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
return [
|
||||
{
|
||||
type: 'created',
|
||||
|
||||
+2
-1
@@ -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>
|
||||
>({
|
||||
|
||||
+2
-1
@@ -135,7 +135,8 @@ const deriveAllMetadataEventsFromUpdateAction = ({
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'viewSort':
|
||||
case 'webhook': {
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
flatAction.metadataName,
|
||||
);
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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>
|
||||
>({
|
||||
|
||||
+2
-1
@@ -64,7 +64,8 @@ export const optimisticallyApplyUpdateActionOnAllFlatEntityMaps = ({
|
||||
case 'permissionFlag':
|
||||
case 'objectPermission':
|
||||
case 'fieldPermission':
|
||||
case 'webhook': {
|
||||
case 'webhook':
|
||||
case 'connectionProvider': {
|
||||
const flatEntityMapsKey = getMetadataFlatEntityMapsKey(
|
||||
flatAction.metadataName,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user