Compare commits
11 Commits
e386303fd3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3830ad7b24 | |||
| 34ae24b7db | |||
| 7cf778b579 | |||
| 400dd6d969 | |||
| ae47157818 | |||
| c59eb20886 | |||
| 8ea816b7ef | |||
| 9380a1386a | |||
| d1b11bafe6 | |||
| 523d0ac17c | |||
| 13bed8e4d2 |
@ -1,5 +1,5 @@
|
|||||||
VITE_SERVER_BASE_URL=https://api.twenty.com
|
VITE_SERVER_BASE_URL=https://crm.rootxwire.com
|
||||||
VITE_FRONT_BASE_URL=https://app.twenty.com
|
VITE_FRONT_BASE_URL=https://crm.rootxwire.com
|
||||||
VITE_MODE=production
|
VITE_MODE=production
|
||||||
|
|
||||||
# Used to generate packages/twenty-chrome-extension/src/generated/graphql.tsx
|
# Used to generate packages/twenty-chrome-extension/src/generated/graphql.tsx
|
||||||
|
|||||||
@ -6,7 +6,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "9026:3000"
|
||||||
environment:
|
environment:
|
||||||
NODE_PORT: 3000
|
NODE_PORT: 3000
|
||||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||||
|
|||||||
@ -6,10 +6,10 @@
|
|||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
type="image/x-icon"
|
type="image/x-icon"
|
||||||
href="/images/icons/android/android-launchericon-48-48.png"
|
href="/images/icons/android/48B.png"
|
||||||
data-rh="true"
|
data-rh="true"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="/images/icons/ios/192.png" />
|
<link rel="apple-touch-icon" href="/images/icons/android/192B" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
@ -19,7 +19,7 @@
|
|||||||
content="https://raw.githubusercontent.com/twentyhq/twenty/main/docs/static/img/social-card.png"
|
content="https://raw.githubusercontent.com/twentyhq/twenty/main/docs/static/img/social-card.png"
|
||||||
/>
|
/>
|
||||||
<meta property="og:description" content="A modern open-source CRM" />
|
<meta property="og:description" content="A modern open-source CRM" />
|
||||||
<meta property="og:title" content="Twenty" />
|
<meta property="og:title" content="MessageKnot" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:image"
|
name="twitter:image"
|
||||||
@ -27,13 +27,13 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<meta name="twitter:description" content="A modern open-source CRM" />
|
<meta name="twitter:description" content="A modern open-source CRM" />
|
||||||
<meta name="twitter:title" content="Twenty" />
|
<meta name="twitter:title" content="MessageKnot" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<title>Twenty</title>
|
<title>MessageKnot</title>
|
||||||
<!-- BEGIN: Twenty Config -->
|
<!-- BEGIN: Twenty Config -->
|
||||||
<script id="twenty-env-config">
|
<script id="twenty-env-config">
|
||||||
window._env_ = {
|
window._env_ = {
|
||||||
|
|||||||
BIN
packages/twenty-front/public/images/icons/android/144.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
packages/twenty-front/public/images/icons/android/144B.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
packages/twenty-front/public/images/icons/android/192.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
packages/twenty-front/public/images/icons/android/192B.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
packages/twenty-front/public/images/icons/android/48.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
packages/twenty-front/public/images/icons/android/48B.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
packages/twenty-front/public/images/icons/android/512.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
packages/twenty-front/public/images/icons/android/512B.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
packages/twenty-front/public/images/icons/android/72.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
packages/twenty-front/public/images/icons/android/72B.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
packages/twenty-front/public/images/icons/android/96.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
packages/twenty-front/public/images/icons/android/96B.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Twenty",
|
"short_name": "MessageKnot",
|
||||||
"name": "Twenty",
|
"name": "MessageKnot",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
|
|||||||
@ -740,7 +740,6 @@ export type FeatureFlagDto = {
|
|||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED',
|
|
||||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
|
|||||||
@ -704,7 +704,6 @@ export type FeatureFlagDto = {
|
|||||||
export enum FeatureFlagKey {
|
export enum FeatureFlagKey {
|
||||||
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
|
||||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||||
IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED',
|
|
||||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export const Logo = ({
|
|||||||
onClick,
|
onClick,
|
||||||
}: LogoProps) => {
|
}: LogoProps) => {
|
||||||
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
|
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
|
||||||
const defaultPrimaryLogoUrl = `${window.location.origin}/images/icons/android/android-launchericon-192-192.png`;
|
const defaultPrimaryLogoUrl = `${window.location.origin}/images/icons/android/192B.png`;
|
||||||
|
|
||||||
const primaryLogoUrl = getImageAbsoluteURI({
|
const primaryLogoUrl = getImageAbsoluteURI({
|
||||||
imageUrl: primaryLogo ?? defaultPrimaryLogoUrl,
|
imageUrl: primaryLogo ?? defaultPrimaryLogoUrl,
|
||||||
|
|||||||
@ -20,7 +20,7 @@ const StyledContainer = styled.div`
|
|||||||
|
|
||||||
export const FooterNote = () => (
|
export const FooterNote = () => (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Trans>By using Twenty, you agree to the</Trans>{' '}
|
<Trans>By using MessageKnot, you agree to the</Trans>{' '}
|
||||||
<a
|
<a
|
||||||
href="https://twenty.com/legal/terms"
|
href="https://twenty.com/legal/terms"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import { t } from '@lingui/core/macro';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import {
|
import {
|
||||||
IconApi,
|
IconApi,
|
||||||
IconApps,
|
|
||||||
IconAt,
|
IconAt,
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
IconColorSwatch,
|
IconColorSwatch,
|
||||||
@ -23,12 +22,11 @@ import {
|
|||||||
IconKey,
|
IconKey,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconMail,
|
IconMail,
|
||||||
IconRocket,
|
|
||||||
IconServer,
|
IconServer,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
IconWebhook,
|
IconWebhook
|
||||||
} from 'twenty-ui/display';
|
} from 'twenty-ui/display';
|
||||||
import { PermissionFlagType } from '~/generated/graphql';
|
import { PermissionFlagType } from '~/generated/graphql';
|
||||||
|
|
||||||
@ -134,12 +132,12 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
|||||||
Icon: IconHierarchy2,
|
Icon: IconHierarchy2,
|
||||||
isHidden: !permissionMap[PermissionFlagType.DATA_MODEL],
|
isHidden: !permissionMap[PermissionFlagType.DATA_MODEL],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: t`Integrations`,
|
// label: t`Integrations`,
|
||||||
path: SettingsPath.Integrations,
|
// path: SettingsPath.Integrations,
|
||||||
Icon: IconApps,
|
// Icon: IconApps,
|
||||||
isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
|
// isHidden: !permissionMap[PermissionFlagType.API_KEYS_AND_WEBHOOKS],
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: t`Security`,
|
label: t`Security`,
|
||||||
path: SettingsPath.Security,
|
path: SettingsPath.Security,
|
||||||
@ -193,11 +191,11 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
|
|||||||
!labPublicFeatureFlags.length ||
|
!labPublicFeatureFlags.length ||
|
||||||
!permissionMap[PermissionFlagType.WORKSPACE],
|
!permissionMap[PermissionFlagType.WORKSPACE],
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
label: t`Releases`,
|
// label: t`Releases`,
|
||||||
path: SettingsPath.Releases,
|
// path: SettingsPath.Releases,
|
||||||
Icon: IconRocket,
|
// Icon: IconRocket,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
label: t`Logout`,
|
label: t`Logout`,
|
||||||
onClick: signOut,
|
onClick: signOut,
|
||||||
|
|||||||
@ -81,6 +81,7 @@ export const RestPlayground = ({ onError, schema }: RestPlaygroundProps) => {
|
|||||||
forceDarkModeState: theme.name === 'dark' ? 'dark' : 'light',
|
forceDarkModeState: theme.name === 'dark' ? 'dark' : 'light',
|
||||||
hideClientButton: true,
|
hideClientButton: true,
|
||||||
hideDarkModeToggle: true,
|
hideDarkModeToggle: true,
|
||||||
|
hideModels: schema === 'metadata',
|
||||||
pathRouting: {
|
pathRouting: {
|
||||||
basePath: getSettingsPath(SettingsPath.RestPlayground, {
|
basePath: getSettingsPath(SettingsPath.RestPlayground, {
|
||||||
schema,
|
schema,
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export const Default: Story = {
|
|||||||
that I expect you to, anyways. :)
|
that I expect you to, anyways. :)
|
||||||
</Modal.Content>
|
</Modal.Content>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
By using Twenty, you're opting for the finest CRM experience you'll
|
By using MessageKnot, you're opting for the finest CRM experience you'll
|
||||||
ever encounter.
|
ever encounter.
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
|
import { ViewBarFilterDropdownAdvancedFilterButton } from '@/views/components/ViewBarFilterDropdownAdvancedFilterButton';
|
||||||
import { ViewBarFilterDropdownAnyFieldSearchButton } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchButton';
|
import { ViewBarFilterDropdownAnyFieldSearchButton } from '@/views/components/ViewBarFilterDropdownAnyFieldSearchButton';
|
||||||
import { ViewBarFilterDropdownVectorSearchButton } from '@/views/components/ViewBarFilterDropdownVectorSearchButton';
|
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -14,17 +11,9 @@ const StyledContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ViewBarFilterDropdownBottomMenu = () => {
|
export const ViewBarFilterDropdownBottomMenu = () => {
|
||||||
const isAnyFieldSearchEnabled = useIsFeatureEnabled(
|
|
||||||
FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{isAnyFieldSearchEnabled ? (
|
|
||||||
<ViewBarFilterDropdownAnyFieldSearchButton />
|
<ViewBarFilterDropdownAnyFieldSearchButton />
|
||||||
) : (
|
|
||||||
<ViewBarFilterDropdownVectorSearchButton />
|
|
||||||
)}
|
|
||||||
<ViewBarFilterDropdownAdvancedFilterButton />
|
<ViewBarFilterDropdownAdvancedFilterButton />
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/vie
|
|||||||
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
import { transformViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/transform-view-filter-workspace-value-to-core-value';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'migrate:views-to-core',
|
name: 'migrate:views-to-core',
|
||||||
@ -298,22 +299,12 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedValue: JSON;
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsedValue = JSON.parse(filter.value);
|
|
||||||
} catch {
|
|
||||||
throw new Error(
|
|
||||||
`Could not parse value to JSON for view filter ${filter.id} for workspace ${workspaceId}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const coreViewFilter: Partial<ViewFilter> = {
|
const coreViewFilter: Partial<ViewFilter> = {
|
||||||
id: filter.id,
|
id: filter.id,
|
||||||
fieldMetadataId: filter.fieldMetadataId,
|
fieldMetadataId: filter.fieldMetadataId,
|
||||||
viewId: filter.viewId,
|
viewId: filter.viewId,
|
||||||
operand: filter.operand,
|
operand: filter.operand,
|
||||||
value: parsedValue,
|
value: transformViewFilterWorkspaceValueToCoreValue(filter.value),
|
||||||
viewFilterGroupId: filter.viewFilterGroupId,
|
viewFilterGroupId: filter.viewFilterGroupId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
createdAt: new Date(filter.createdAt),
|
createdAt: new Date(filter.createdAt),
|
||||||
|
|||||||
@ -210,6 +210,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
|
|||||||
.setFindOptions({
|
.setFindOptions({
|
||||||
select: selectedColumns,
|
select: selectedColumns,
|
||||||
})
|
})
|
||||||
|
.withDeleted()
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,15 +30,6 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
|
|||||||
imagePath: '',
|
imagePath: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED,
|
|
||||||
metadata: {
|
|
||||||
label: 'Any field filter',
|
|
||||||
description:
|
|
||||||
'Search multiple fields at the same time with the new "Search any field" feature on tables and kanbans',
|
|
||||||
imagePath: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(process.env.CLOUDFLARE_API_KEY
|
...(process.env.CLOUDFLARE_API_KEY
|
||||||
? [
|
? [
|
||||||
// {
|
// {
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export enum FeatureFlagKey {
|
|||||||
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
|
||||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED = 'IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED',
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
IS_FIELDS_PERMISSIONS_ENABLED = 'IS_FIELDS_PERMISSIONS_ENABLED',
|
||||||
IS_ANY_FIELD_SEARCH_ENABLED = 'IS_ANY_FIELD_SEARCH_ENABLED',
|
|
||||||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||||
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
|
IS_TWO_FACTOR_AUTHENTICATION_ENABLED = 'IS_TWO_FACTOR_AUTHENTICATION_ENABLED',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { OpenAPIV3_1 } from 'openapi-types';
|
import { OpenAPIV3_1 } from 'openapi-types';
|
||||||
import { capitalize } from 'twenty-shared/utils';
|
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||||
@ -42,6 +42,7 @@ import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metada
|
|||||||
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects';
|
||||||
import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util';
|
import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util';
|
||||||
import { getServerUrl } from 'src/utils/get-server-url';
|
import { getServerUrl } from 'src/utils/get-server-url';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OpenApiService {
|
export class OpenApiService {
|
||||||
@ -52,6 +53,30 @@ export class OpenApiService {
|
|||||||
private readonly featureFlagService: FeatureFlagService,
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private async getWorkspaceFromRequest(request: Request) {
|
||||||
|
try {
|
||||||
|
const { workspace } =
|
||||||
|
await this.accessTokenService.validateTokenByRequest(request);
|
||||||
|
|
||||||
|
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||||
|
|
||||||
|
return workspace;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getObjectMetadataItems(workspace: Workspace) {
|
||||||
|
return await this.objectMetadataService.findManyWithinWorkspace(
|
||||||
|
workspace.id,
|
||||||
|
{
|
||||||
|
order: {
|
||||||
|
namePlural: 'ASC',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async generateCoreSchema(request: Request): Promise<OpenAPIV3_1.Document> {
|
async generateCoreSchema(request: Request): Promise<OpenAPIV3_1.Document> {
|
||||||
const baseUrl = getServerUrl(
|
const baseUrl = getServerUrl(
|
||||||
this.twentyConfigService.get('SERVER_URL'),
|
this.twentyConfigService.get('SERVER_URL'),
|
||||||
@ -60,26 +85,14 @@ export class OpenApiService {
|
|||||||
|
|
||||||
const schema = baseSchema('core', baseUrl);
|
const schema = baseSchema('core', baseUrl);
|
||||||
|
|
||||||
let objectMetadataItems;
|
const workspace = await this.getWorkspaceFromRequest(request);
|
||||||
let workspace;
|
|
||||||
|
|
||||||
try {
|
if (!isDefined(workspace)) {
|
||||||
const authResult =
|
|
||||||
await this.accessTokenService.validateTokenByRequest(request);
|
|
||||||
|
|
||||||
workspace = authResult.workspace;
|
|
||||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
|
||||||
|
|
||||||
objectMetadataItems =
|
|
||||||
await this.objectMetadataService.findManyWithinWorkspace(workspace.id, {
|
|
||||||
order: {
|
|
||||||
namePlural: 'ASC',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const objectMetadataItems = await this.getObjectMetadataItems(workspace);
|
||||||
|
|
||||||
if (!objectMetadataItems.length) {
|
if (!objectMetadataItems.length) {
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
@ -105,7 +118,7 @@ export class OpenApiService {
|
|||||||
return paths;
|
return paths;
|
||||||
}, schema.paths as OpenAPIV3_1.PathsObject);
|
}, schema.paths as OpenAPIV3_1.PathsObject);
|
||||||
|
|
||||||
schema.webhooks = objectMetadataItems.reduce(
|
schema.webhooks = filteredObjectMetadataItems.reduce(
|
||||||
(paths, item) => {
|
(paths, item) => {
|
||||||
paths[
|
paths[
|
||||||
this.createWebhookEventName(
|
this.createWebhookEventName(
|
||||||
@ -134,8 +147,6 @@ export class OpenApiService {
|
|||||||
>,
|
>,
|
||||||
);
|
);
|
||||||
|
|
||||||
schema.tags = computeSchemaTags(objectMetadataItems);
|
|
||||||
|
|
||||||
schema.components = {
|
schema.components = {
|
||||||
...schema.components, // components.securitySchemes is defined in base Schema
|
...schema.components, // components.securitySchemes is defined in base Schema
|
||||||
schemas: computeSchemaComponents(filteredObjectMetadataItems),
|
schemas: computeSchemaComponents(filteredObjectMetadataItems),
|
||||||
@ -161,7 +172,11 @@ export class OpenApiService {
|
|||||||
|
|
||||||
const schema = baseSchema('metadata', baseUrl);
|
const schema = baseSchema('metadata', baseUrl);
|
||||||
|
|
||||||
schema.tags = [{ name: 'placeholder' }];
|
const workspace = await this.getWorkspaceFromRequest(request);
|
||||||
|
|
||||||
|
if (!isDefined(workspace)) {
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
const metadata = [
|
const metadata = [
|
||||||
{
|
{
|
||||||
@ -177,7 +192,7 @@ export class OpenApiService {
|
|||||||
namePlural: 'webhooks',
|
namePlural: 'webhooks',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
nameSingular: 'apikey',
|
nameSingular: 'apiKey',
|
||||||
namePlural: 'apiKeys',
|
namePlural: 'apiKeys',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -249,9 +264,18 @@ export class OpenApiService {
|
|||||||
return path;
|
return path;
|
||||||
}, schema.paths as OpenAPIV3_1.PathsObject);
|
}, schema.paths as OpenAPIV3_1.PathsObject);
|
||||||
|
|
||||||
|
const objectMetadataItems = await this.getObjectMetadataItems(workspace);
|
||||||
|
|
||||||
|
const webhookAndApiKeyObjectMetadataItems = objectMetadataItems.filter(
|
||||||
|
({ nameSingular }) => ['webhook', 'apiKey'].includes(nameSingular),
|
||||||
|
);
|
||||||
|
|
||||||
schema.components = {
|
schema.components = {
|
||||||
...schema.components, // components.securitySchemes is defined in base Schema
|
...schema.components, // components.securitySchemes is defined in base Schema
|
||||||
schemas: computeMetadataSchemaComponents(metadata),
|
schemas: {
|
||||||
|
...computeMetadataSchemaComponents(metadata),
|
||||||
|
...computeSchemaComponents(webhookAndApiKeyObjectMetadataItems),
|
||||||
|
},
|
||||||
parameters: computeParameterComponents(true),
|
parameters: computeParameterComponents(true),
|
||||||
responses: {
|
responses: {
|
||||||
'400': get400ErrorResponses(),
|
'400': get400ErrorResponses(),
|
||||||
@ -259,6 +283,8 @@ export class OpenApiService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
schema.tags = computeSchemaTags(webhookAndApiKeyObjectMetadataItems);
|
||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -239,10 +239,13 @@ export const getJsonResponse = () => {
|
|||||||
servers: {
|
servers: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
url: { type: 'string' },
|
url: { type: 'string' },
|
||||||
description: { type: 'string' },
|
description: { type: 'string' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@ -17,4 +17,5 @@ export enum ObjectMetadataExceptionCode {
|
|||||||
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
|
OBJECT_MUTATION_NOT_ALLOWED = 'OBJECT_MUTATION_NOT_ALLOWED',
|
||||||
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
|
OBJECT_ALREADY_EXISTS = 'OBJECT_ALREADY_EXISTS',
|
||||||
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
|
MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD = 'MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD',
|
||||||
|
INVALID_ORM_OUTPUT = 'INVALID_ORM_OUTPUT',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -606,6 +606,21 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
|||||||
queryRunner,
|
queryRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const morphRelationFieldMetadataToUpdate =
|
||||||
|
await this.objectMetadataFieldRelationService.updateMorphRelationsJoinColumnName(
|
||||||
|
{
|
||||||
|
existingObjectMetadata,
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
queryRunner,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.objectMetadataMigrationService.updateMorphRelationMigrations({
|
||||||
|
workspaceId: objectMetadataForUpdate.workspaceId,
|
||||||
|
morphRelationFieldMetadataToUpdate: morphRelationFieldMetadataToUpdate,
|
||||||
|
queryRunner,
|
||||||
|
});
|
||||||
|
|
||||||
await this.objectMetadataMigrationService.recomputeEnumNames(
|
await this.objectMetadataMigrationService.recomputeEnumNames(
|
||||||
objectMetadataForUpdate,
|
objectMetadataForUpdate,
|
||||||
objectMetadataForUpdate.workspaceId,
|
objectMetadataForUpdate.workspaceId,
|
||||||
|
|||||||
@ -9,7 +9,12 @@ import { v4 as uuidV4 } from 'uuid';
|
|||||||
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { computeMorphRelationFieldJoinColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-morph-relation-field-join-column-name.util';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import {
|
||||||
|
ObjectMetadataException,
|
||||||
|
ObjectMetadataExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||||
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
import { buildDescriptionForRelationFieldMetadataOnFromField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-from-field.util';
|
||||||
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
import { buildDescriptionForRelationFieldMetadataOnToField } from 'src/engine/metadata-modules/object-metadata/utils/build-description-for-relation-field-on-to-field.util';
|
||||||
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-on-delete-action.type';
|
||||||
@ -412,4 +417,108 @@ export class ObjectMetadataFieldRelationService {
|
|||||||
description,
|
description,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private validateFieldMetadataTypeIsMorphRelation = (
|
||||||
|
fieldMetadatas: FieldMetadataEntity[],
|
||||||
|
): fieldMetadatas is Array<
|
||||||
|
FieldMetadataEntity & FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>
|
||||||
|
> => {
|
||||||
|
return fieldMetadatas.every(
|
||||||
|
(fieldMetadata) =>
|
||||||
|
fieldMetadata.type === FieldMetadataType.MORPH_RELATION,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public async findTargetMorphRelationFieldMetadatas(
|
||||||
|
objectMetadataId: string,
|
||||||
|
): Promise<FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>[]> {
|
||||||
|
const fieldMetadatas = await this.fieldMetadataRepository.find({
|
||||||
|
where: {
|
||||||
|
relationTargetObjectMetadataId: objectMetadataId,
|
||||||
|
type: FieldMetadataType.MORPH_RELATION,
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
relationTargetObjectMetadata: true,
|
||||||
|
object: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.validateFieldMetadataTypeIsMorphRelation(fieldMetadatas)) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
'Invalid field metadata type. Expected MORPH_RELATION only',
|
||||||
|
ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldMetadatas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateMorphRelationsJoinColumnName({
|
||||||
|
existingObjectMetadata,
|
||||||
|
objectMetadataForUpdate,
|
||||||
|
queryRunner,
|
||||||
|
}: {
|
||||||
|
existingObjectMetadata: Pick<
|
||||||
|
ObjectMetadataItemWithFieldMaps,
|
||||||
|
'nameSingular' | 'isCustom' | 'id' | 'labelPlural' | 'icon' | 'fieldsById'
|
||||||
|
>;
|
||||||
|
objectMetadataForUpdate: Pick<
|
||||||
|
ObjectMetadataItemWithFieldMaps,
|
||||||
|
| 'nameSingular'
|
||||||
|
| 'isCustom'
|
||||||
|
| 'workspaceId'
|
||||||
|
| 'id'
|
||||||
|
| 'labelSingular'
|
||||||
|
| 'labelPlural'
|
||||||
|
| 'icon'
|
||||||
|
| 'fieldsById'
|
||||||
|
>;
|
||||||
|
queryRunner: QueryRunner;
|
||||||
|
}): Promise<
|
||||||
|
{
|
||||||
|
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
|
||||||
|
newJoinColumnName: string;
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
|
const fieldMetadataRepository =
|
||||||
|
queryRunner.manager.getRepository(FieldMetadataEntity);
|
||||||
|
|
||||||
|
const morphRelationFieldMetadataTargets =
|
||||||
|
await this.findTargetMorphRelationFieldMetadatas(
|
||||||
|
existingObjectMetadata.id,
|
||||||
|
);
|
||||||
|
const morphRelationFieldMetadataToUpdate =
|
||||||
|
morphRelationFieldMetadataTargets.filter(
|
||||||
|
(morphRelationFieldMetadata) =>
|
||||||
|
morphRelationFieldMetadata.settings?.relationType ===
|
||||||
|
RelationType.MANY_TO_ONE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const morphRelationFieldMetadataToUpdateWithNewJoinColumnName = [];
|
||||||
|
|
||||||
|
if (morphRelationFieldMetadataToUpdate.length > 0) {
|
||||||
|
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
|
||||||
|
const newJoinColumnName = computeMorphRelationFieldJoinColumnName({
|
||||||
|
name: morphRelationFieldMetadata.name,
|
||||||
|
targetObjectMetadataNameSingular:
|
||||||
|
objectMetadataForUpdate.nameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fieldMetadataRepository.save({
|
||||||
|
...morphRelationFieldMetadata,
|
||||||
|
settings: {
|
||||||
|
...morphRelationFieldMetadata.settings,
|
||||||
|
joinColumnName: newJoinColumnName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
morphRelationFieldMetadataToUpdateWithNewJoinColumnName.push({
|
||||||
|
fieldMetadata: morphRelationFieldMetadata,
|
||||||
|
newJoinColumnName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return morphRelationFieldMetadataToUpdateWithNewJoinColumnName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,10 @@ import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfa
|
|||||||
|
|
||||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
import {
|
||||||
|
ObjectMetadataException,
|
||||||
|
ObjectMetadataExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/object-metadata/object-metadata.exception';
|
||||||
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
import { buildMigrationsForCustomObjectRelations } from 'src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util';
|
||||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||||
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util';
|
||||||
@ -360,4 +364,61 @@ export class ObjectMetadataMigrationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async updateMorphRelationMigrations({
|
||||||
|
workspaceId,
|
||||||
|
morphRelationFieldMetadataToUpdate,
|
||||||
|
queryRunner,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
morphRelationFieldMetadataToUpdate: {
|
||||||
|
fieldMetadata: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION>;
|
||||||
|
newJoinColumnName: string;
|
||||||
|
}[];
|
||||||
|
queryRunner?: QueryRunner;
|
||||||
|
}) {
|
||||||
|
for (const morphRelationFieldMetadata of morphRelationFieldMetadataToUpdate) {
|
||||||
|
if (!morphRelationFieldMetadata.fieldMetadata.settings?.joinColumnName) {
|
||||||
|
throw new ObjectMetadataException(
|
||||||
|
`Settings for morph relation field should be defined ${morphRelationFieldMetadata.fieldMetadata.name}`,
|
||||||
|
ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
|
generateMigrationName(
|
||||||
|
`rename-join-column-name-${morphRelationFieldMetadata.fieldMetadata.name}-to-${morphRelationFieldMetadata.newJoinColumnName}-in-${morphRelationFieldMetadata.fieldMetadata.object.nameSingular}`,
|
||||||
|
),
|
||||||
|
workspaceId,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: computeObjectTargetTable(
|
||||||
|
morphRelationFieldMetadata.fieldMetadata.object,
|
||||||
|
),
|
||||||
|
action: WorkspaceMigrationTableActionType.ALTER,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
action: WorkspaceMigrationColumnActionType.ALTER,
|
||||||
|
currentColumnDefinition: {
|
||||||
|
columnName:
|
||||||
|
morphRelationFieldMetadata.fieldMetadata.settings
|
||||||
|
?.joinColumnName,
|
||||||
|
columnType: 'uuid',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
alteredColumnDefinition: {
|
||||||
|
columnName: `${morphRelationFieldMetadata.newJoinColumnName}`,
|
||||||
|
columnType: 'uuid',
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -478,4 +478,53 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
|||||||
expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id');
|
expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Undefined locale handling', () => {
|
||||||
|
it('should use SOURCE_LOCALE fallback when locale is undefined for standard object', () => {
|
||||||
|
const objectMetadata = {
|
||||||
|
labelSingular: 'Standard Label',
|
||||||
|
labelPlural: 'Standard Labels',
|
||||||
|
description: 'Standard Description',
|
||||||
|
icon: 'default-icon',
|
||||||
|
isCustom: false,
|
||||||
|
standardOverrides: {
|
||||||
|
labelSingular: 'Source Override',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = resolveObjectMetadataStandardOverride(
|
||||||
|
objectMetadata,
|
||||||
|
'labelSingular',
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('Source Override');
|
||||||
|
expect(mockGenerateMessageId).not.toHaveBeenCalled();
|
||||||
|
expect(mockI18n._).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to auto translation when locale is undefined and no SOURCE_LOCALE override exists', () => {
|
||||||
|
mockI18n._.mockReturnValue('Auto Translated Label');
|
||||||
|
mockGenerateMessageId.mockReturnValue('auto.translation.id');
|
||||||
|
|
||||||
|
const objectMetadata = {
|
||||||
|
labelSingular: 'Standard Label',
|
||||||
|
labelPlural: 'Standard Labels',
|
||||||
|
description: 'Standard Description',
|
||||||
|
icon: 'default-icon',
|
||||||
|
isCustom: false,
|
||||||
|
standardOverrides: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = resolveObjectMetadataStandardOverride(
|
||||||
|
objectMetadata,
|
||||||
|
'labelSingular',
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe('Auto Translated Label');
|
||||||
|
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');
|
||||||
|
expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ConflictError,
|
ConflictError,
|
||||||
ForbiddenError,
|
ForbiddenError,
|
||||||
|
InternalServerError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
UserInputError,
|
UserInputError,
|
||||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||||
@ -25,6 +26,8 @@ export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
|
|||||||
throw new ForbiddenError(error);
|
throw new ForbiddenError(error);
|
||||||
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
|
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
|
||||||
throw new ConflictError(error);
|
throw new ConflictError(error);
|
||||||
|
case ObjectMetadataExceptionCode.INVALID_ORM_OUTPUT:
|
||||||
|
throw new InternalServerError(error);
|
||||||
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
|
||||||
throw error;
|
throw error;
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@ -19,6 +19,8 @@ export const resolveObjectMetadataStandardOverride = (
|
|||||||
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
|
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
|
||||||
locale: keyof typeof APP_LOCALES | undefined,
|
locale: keyof typeof APP_LOCALES | undefined,
|
||||||
): string => {
|
): string => {
|
||||||
|
const safeLocale = locale ?? SOURCE_LOCALE;
|
||||||
|
|
||||||
if (objectMetadata.isCustom) {
|
if (objectMetadata.isCustom) {
|
||||||
return objectMetadata[labelKey] ?? '';
|
return objectMetadata[labelKey] ?? '';
|
||||||
}
|
}
|
||||||
@ -32,11 +34,10 @@ export const resolveObjectMetadataStandardOverride = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
isDefined(objectMetadata.standardOverrides?.translations) &&
|
isDefined(objectMetadata.standardOverrides?.translations) &&
|
||||||
isDefined(locale) &&
|
|
||||||
labelKey !== 'icon'
|
labelKey !== 'icon'
|
||||||
) {
|
) {
|
||||||
const translationValue =
|
const translationValue =
|
||||||
objectMetadata.standardOverrides.translations[locale]?.[labelKey];
|
objectMetadata.standardOverrides.translations[safeLocale]?.[labelKey];
|
||||||
|
|
||||||
if (isDefined(translationValue)) {
|
if (isDefined(translationValue)) {
|
||||||
return translationValue;
|
return translationValue;
|
||||||
@ -44,7 +45,7 @@ export const resolveObjectMetadataStandardOverride = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
locale === SOURCE_LOCALE &&
|
safeLocale === SOURCE_LOCALE &&
|
||||||
isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])
|
isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])
|
||||||
) {
|
) {
|
||||||
return objectMetadata.standardOverrides[labelKey] ?? '';
|
return objectMetadata.standardOverrides[labelKey] ?? '';
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
export type RelationFilterValue = {
|
||||||
|
isCurrentWorkspaceMemberSelected?: boolean;
|
||||||
|
selectedRecordIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ViewFilterValue =
|
||||||
|
| string
|
||||||
|
| string[]
|
||||||
|
| RelationFilterValue
|
||||||
|
| Record<string, unknown>
|
||||||
|
| null
|
||||||
|
| undefined;
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { ViewFilterValue } from 'src/engine/metadata-modules/view/types/view-filter-value.type';
|
||||||
import { View } from 'src/engine/metadata-modules/view/view.entity';
|
import { View } from 'src/engine/metadata-modules/view/view.entity';
|
||||||
|
|
||||||
@Entity({ name: 'viewFilter', schema: 'core' })
|
@Entity({ name: 'viewFilter', schema: 'core' })
|
||||||
@ -31,7 +32,7 @@ export class ViewFilter {
|
|||||||
operand: string;
|
operand: string;
|
||||||
|
|
||||||
@Column({ nullable: false, type: 'jsonb' })
|
@Column({ nullable: false, type: 'jsonb' })
|
||||||
value: JSON;
|
value: ViewFilterValue;
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'uuid' })
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
viewFilterGroupId?: string | null;
|
viewFilterGroupId?: string | null;
|
||||||
|
|||||||
@ -128,6 +128,10 @@ export class MiddlewareService {
|
|||||||
|
|
||||||
public async hydrateGraphqlRequest(request: Request) {
|
public async hydrateGraphqlRequest(request: Request) {
|
||||||
if (!this.isTokenPresent(request)) {
|
if (!this.isTokenPresent(request)) {
|
||||||
|
request.locale =
|
||||||
|
(request.headers['x-locale'] as keyof typeof APP_LOCALES) ??
|
||||||
|
SOURCE_LOCALE;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -110,7 +110,6 @@ describe('WorkspaceEntityManager', () => {
|
|||||||
IS_RELATION_CONNECT_ENABLED: false,
|
IS_RELATION_CONNECT_ENABLED: false,
|
||||||
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED: false,
|
IS_WORKSPACE_API_KEY_WEBHOOK_GRAPHQL_ENABLED: false,
|
||||||
IS_FIELDS_PERMISSIONS_ENABLED: false,
|
IS_FIELDS_PERMISSIONS_ENABLED: false,
|
||||||
IS_ANY_FIELD_SEARCH_ENABLED: false,
|
|
||||||
IS_CORE_VIEW_SYNCING_ENABLED: false,
|
IS_CORE_VIEW_SYNCING_ENABLED: false,
|
||||||
IS_TWO_FACTOR_AUTHENTICATION_ENABLED: false,
|
IS_TWO_FACTOR_AUTHENTICATION_ENABLED: false,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,11 +55,6 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: FeatureFlagKey.IS_ANY_FIELD_SEARCH_ENABLED,
|
|
||||||
workspaceId: workspaceId,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IS_TWO_FACTOR_AUTHENTICATION_ENABLED,
|
key: FeatureFlagKey.IS_TWO_FACTOR_AUTHENTICATION_ENABLED,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
|
|||||||
@ -129,7 +129,9 @@ export class CalendarEventParticipantPersonListener {
|
|||||||
const { addedAdditionalEmails, removedAdditionalEmails } =
|
const { addedAdditionalEmails, removedAdditionalEmails } =
|
||||||
computeChangedAdditionalEmails(eventPayload.properties.diff);
|
computeChangedAdditionalEmails(eventPayload.properties.diff);
|
||||||
|
|
||||||
const removedEmailPromises = removedAdditionalEmails.map((email) =>
|
const removedEmailPromises = removedAdditionalEmails
|
||||||
|
?.filter((email: string) => isDefined(email))
|
||||||
|
.map((email) =>
|
||||||
this.messageQueueService.add<CalendarEventParticipantUnmatchParticipantJobData>(
|
this.messageQueueService.add<CalendarEventParticipantUnmatchParticipantJobData>(
|
||||||
CalendarEventParticipantUnmatchParticipantJob.name,
|
CalendarEventParticipantUnmatchParticipantJob.name,
|
||||||
{
|
{
|
||||||
@ -166,6 +168,7 @@ export class CalendarEventParticipantPersonListener {
|
|||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
for (const eventPayload of payload.events) {
|
for (const eventPayload of payload.events) {
|
||||||
|
if (isDefined(eventPayload.properties.before.emails?.primaryEmail)) {
|
||||||
await this.messageQueueService.add<CalendarEventParticipantUnmatchParticipantJobData>(
|
await this.messageQueueService.add<CalendarEventParticipantUnmatchParticipantJobData>(
|
||||||
CalendarEventParticipantUnmatchParticipantJob.name,
|
CalendarEventParticipantUnmatchParticipantJob.name,
|
||||||
{
|
{
|
||||||
@ -174,12 +177,15 @@ export class CalendarEventParticipantPersonListener {
|
|||||||
personId: eventPayload.recordId,
|
personId: eventPayload.recordId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const additionalEmails =
|
const additionalEmails =
|
||||||
eventPayload.properties.before.emails?.additionalEmails;
|
eventPayload.properties.before.emails?.additionalEmails;
|
||||||
|
|
||||||
if (Array.isArray(additionalEmails)) {
|
if (Array.isArray(additionalEmails)) {
|
||||||
const additionalEmailPromises = additionalEmails.map((email) =>
|
const additionalEmailPromises = additionalEmails
|
||||||
|
?.filter((email: string) => isDefined(email))
|
||||||
|
.map((email) =>
|
||||||
this.messageQueueService.add<CalendarEventParticipantUnmatchParticipantJobData>(
|
this.messageQueueService.add<CalendarEventParticipantUnmatchParticipantJobData>(
|
||||||
CalendarEventParticipantUnmatchParticipantJob.name,
|
CalendarEventParticipantUnmatchParticipantJob.name,
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,209 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import {
|
||||||
|
ViewException,
|
||||||
|
ViewExceptionCode,
|
||||||
|
} from 'src/modules/view/views.exception';
|
||||||
|
|
||||||
|
type EntityWithId = { id: string };
|
||||||
|
|
||||||
|
type SyncOperations<T extends EntityWithId> = {
|
||||||
|
create: (workspaceId: string, entity: T) => Promise<void>;
|
||||||
|
update: (
|
||||||
|
workspaceId: string,
|
||||||
|
entity: T,
|
||||||
|
diff?: Partial<ObjectRecordDiff<T>>,
|
||||||
|
) => Promise<void>;
|
||||||
|
delete: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
|
||||||
|
destroy: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
|
||||||
|
restore: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export abstract class BaseViewSyncListener<T extends EntityWithId> {
|
||||||
|
@Inject(FeatureFlagService)
|
||||||
|
protected readonly featureFlagService: FeatureFlagService;
|
||||||
|
|
||||||
|
@Inject(ExceptionHandlerService)
|
||||||
|
protected readonly exceptionHandlerService: ExceptionHandlerService;
|
||||||
|
|
||||||
|
protected readonly logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly syncOperations: SyncOperations<T>,
|
||||||
|
loggerName: string,
|
||||||
|
protected readonly entityTypeName: string,
|
||||||
|
) {
|
||||||
|
this.logger = new Logger(loggerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<ObjectRecordCreateEvent<T>>,
|
||||||
|
): Promise<void> {
|
||||||
|
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of batchEvent.events) {
|
||||||
|
try {
|
||||||
|
await this.syncOperations.create(
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
event.properties.after,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.captureException(
|
||||||
|
error,
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
'create',
|
||||||
|
event.properties.after.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<ObjectRecordUpdateEvent<T>>,
|
||||||
|
): Promise<void> {
|
||||||
|
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of batchEvent.events) {
|
||||||
|
try {
|
||||||
|
await this.syncOperations.update(
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
event.properties.after,
|
||||||
|
event.properties.diff,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.captureException(
|
||||||
|
error,
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
'update',
|
||||||
|
event.properties.after.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<ObjectRecordDeleteEvent<T>>,
|
||||||
|
): Promise<void> {
|
||||||
|
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of batchEvent.events) {
|
||||||
|
try {
|
||||||
|
await this.syncOperations.delete(
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
event.properties.before,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.captureException(
|
||||||
|
error,
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
'delete',
|
||||||
|
event.properties.before.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<ObjectRecordDestroyEvent<T>>,
|
||||||
|
): Promise<void> {
|
||||||
|
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of batchEvent.events) {
|
||||||
|
try {
|
||||||
|
await this.syncOperations.destroy(
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
event.properties.before,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.captureException(
|
||||||
|
error,
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
'destroy',
|
||||||
|
event.properties.before.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<ObjectRecordRestoreEvent<T>>,
|
||||||
|
): Promise<void> {
|
||||||
|
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of batchEvent.events) {
|
||||||
|
try {
|
||||||
|
await this.syncOperations.restore(
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
event.properties.after,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.captureException(
|
||||||
|
error,
|
||||||
|
batchEvent.workspaceId,
|
||||||
|
'restore',
|
||||||
|
event.properties.after.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isFeatureFlagEnabled(workspaceId: string): Promise<boolean> {
|
||||||
|
const featureFlags =
|
||||||
|
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||||
|
|
||||||
|
return featureFlags.IS_CORE_VIEW_SYNCING_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private captureException(
|
||||||
|
error: Error,
|
||||||
|
workspaceId: string,
|
||||||
|
operation: string,
|
||||||
|
entityId: string,
|
||||||
|
) {
|
||||||
|
const viewException = new ViewException(
|
||||||
|
`Failed to sync ${this.entityTypeName} ${entityId} to core: ${error.message}`,
|
||||||
|
ViewExceptionCode.CORE_VIEW_SYNC_ERROR,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.exceptionHandlerService.captureExceptions([viewException], {
|
||||||
|
workspace: {
|
||||||
|
id: workspaceId,
|
||||||
|
},
|
||||||
|
additionalData: {
|
||||||
|
entityId: entityId,
|
||||||
|
entityType: this.entityTypeName,
|
||||||
|
operation: operation,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import { ViewFieldSyncService } from 'src/modules/view/services/view-field-sync.service';
|
||||||
|
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||||
|
|
||||||
|
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewFieldListener extends BaseViewSyncListener<ViewFieldWorkspaceEntity> {
|
||||||
|
constructor(viewFieldSyncService: ViewFieldSyncService) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
create:
|
||||||
|
viewFieldSyncService.createCoreViewField.bind(viewFieldSyncService),
|
||||||
|
update:
|
||||||
|
viewFieldSyncService.updateCoreViewField.bind(viewFieldSyncService),
|
||||||
|
delete:
|
||||||
|
viewFieldSyncService.deleteCoreViewField.bind(viewFieldSyncService),
|
||||||
|
destroy:
|
||||||
|
viewFieldSyncService.destroyCoreViewField.bind(viewFieldSyncService),
|
||||||
|
restore:
|
||||||
|
viewFieldSyncService.restoreCoreViewField.bind(viewFieldSyncService),
|
||||||
|
},
|
||||||
|
ViewFieldListener.name,
|
||||||
|
'view field',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.CREATED)
|
||||||
|
async handleViewFieldCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordCreateEvent<ViewFieldWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleCreated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.UPDATED)
|
||||||
|
async handleViewFieldUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordUpdateEvent<ViewFieldWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleUpdated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.DELETED)
|
||||||
|
async handleViewFieldDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<ViewFieldWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDeleted(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.DESTROYED)
|
||||||
|
async handleViewFieldDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDestroyEvent<ViewFieldWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDestroyed(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.RESTORED)
|
||||||
|
async handleViewFieldRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordRestoreEvent<ViewFieldWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleRestored(batchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import { ViewFilterGroupSyncService } from 'src/modules/view/services/view-filter-group-sync.service';
|
||||||
|
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||||
|
|
||||||
|
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewFilterGroupListener extends BaseViewSyncListener<ViewFilterGroupWorkspaceEntity> {
|
||||||
|
constructor(viewFilterGroupSyncService: ViewFilterGroupSyncService) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
create: viewFilterGroupSyncService.createCoreViewFilterGroup.bind(
|
||||||
|
viewFilterGroupSyncService,
|
||||||
|
),
|
||||||
|
update: viewFilterGroupSyncService.updateCoreViewFilterGroup.bind(
|
||||||
|
viewFilterGroupSyncService,
|
||||||
|
),
|
||||||
|
delete: viewFilterGroupSyncService.deleteCoreViewFilterGroup.bind(
|
||||||
|
viewFilterGroupSyncService,
|
||||||
|
),
|
||||||
|
destroy: viewFilterGroupSyncService.destroyCoreViewFilterGroup.bind(
|
||||||
|
viewFilterGroupSyncService,
|
||||||
|
),
|
||||||
|
restore: viewFilterGroupSyncService.restoreCoreViewFilterGroup.bind(
|
||||||
|
viewFilterGroupSyncService,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ViewFilterGroupListener.name,
|
||||||
|
'view filter group',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.CREATED)
|
||||||
|
async handleViewFilterGroupCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordCreateEvent<ViewFilterGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleCreated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.UPDATED)
|
||||||
|
async handleViewFilterGroupUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordUpdateEvent<ViewFilterGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleUpdated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DELETED)
|
||||||
|
async handleViewFilterGroupDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<ViewFilterGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDeleted(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DESTROYED)
|
||||||
|
async handleViewFilterGroupDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDestroyEvent<ViewFilterGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDestroyed(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.RESTORED)
|
||||||
|
async handleViewFilterGroupRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordRestoreEvent<ViewFilterGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleRestored(batchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import { ViewFilterSyncService } from 'src/modules/view/services/view-filter-sync.service';
|
||||||
|
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||||
|
|
||||||
|
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewFilterListener extends BaseViewSyncListener<ViewFilterWorkspaceEntity> {
|
||||||
|
constructor(viewFilterSyncService: ViewFilterSyncService) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
create: viewFilterSyncService.createCoreViewFilter.bind(
|
||||||
|
viewFilterSyncService,
|
||||||
|
),
|
||||||
|
update: viewFilterSyncService.updateCoreViewFilter.bind(
|
||||||
|
viewFilterSyncService,
|
||||||
|
),
|
||||||
|
delete: viewFilterSyncService.deleteCoreViewFilter.bind(
|
||||||
|
viewFilterSyncService,
|
||||||
|
),
|
||||||
|
destroy: viewFilterSyncService.destroyCoreViewFilter.bind(
|
||||||
|
viewFilterSyncService,
|
||||||
|
),
|
||||||
|
restore: viewFilterSyncService.restoreCoreViewFilter.bind(
|
||||||
|
viewFilterSyncService,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ViewFilterListener.name,
|
||||||
|
'view filter',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.CREATED)
|
||||||
|
async handleViewFilterCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordCreateEvent<ViewFilterWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleCreated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.UPDATED)
|
||||||
|
async handleViewFilterUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordUpdateEvent<ViewFilterWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleUpdated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DELETED)
|
||||||
|
async handleViewFilterDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<ViewFilterWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDeleted(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DESTROYED)
|
||||||
|
async handleViewFilterDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDestroyEvent<ViewFilterWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDestroyed(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.RESTORED)
|
||||||
|
async handleViewFilterRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordRestoreEvent<ViewFilterWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleRestored(batchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import { ViewGroupSyncService } from 'src/modules/view/services/view-group-sync.service';
|
||||||
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
|
|
||||||
|
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewGroupListener extends BaseViewSyncListener<ViewGroupWorkspaceEntity> {
|
||||||
|
constructor(viewGroupSyncService: ViewGroupSyncService) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
create:
|
||||||
|
viewGroupSyncService.createCoreViewGroup.bind(viewGroupSyncService),
|
||||||
|
update:
|
||||||
|
viewGroupSyncService.updateCoreViewGroup.bind(viewGroupSyncService),
|
||||||
|
delete:
|
||||||
|
viewGroupSyncService.deleteCoreViewGroup.bind(viewGroupSyncService),
|
||||||
|
destroy:
|
||||||
|
viewGroupSyncService.destroyCoreViewGroup.bind(viewGroupSyncService),
|
||||||
|
restore:
|
||||||
|
viewGroupSyncService.restoreCoreViewGroup.bind(viewGroupSyncService),
|
||||||
|
},
|
||||||
|
ViewGroupListener.name,
|
||||||
|
'view group',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.CREATED)
|
||||||
|
async handleViewGroupCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordCreateEvent<ViewGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleCreated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.UPDATED)
|
||||||
|
async handleViewGroupUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordUpdateEvent<ViewGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleUpdated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DELETED)
|
||||||
|
async handleViewGroupDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<ViewGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDeleted(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DESTROYED)
|
||||||
|
async handleViewGroupDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDestroyEvent<ViewGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDestroyed(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.RESTORED)
|
||||||
|
async handleViewGroupRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordRestoreEvent<ViewGroupWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleRestored(batchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import { ViewSortSyncService } from 'src/modules/view/services/view-sort-sync.service';
|
||||||
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
|
|
||||||
|
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewSortListener extends BaseViewSyncListener<ViewSortWorkspaceEntity> {
|
||||||
|
constructor(viewSortSyncService: ViewSortSyncService) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
create:
|
||||||
|
viewSortSyncService.createCoreViewSort.bind(viewSortSyncService),
|
||||||
|
update:
|
||||||
|
viewSortSyncService.updateCoreViewSort.bind(viewSortSyncService),
|
||||||
|
delete:
|
||||||
|
viewSortSyncService.deleteCoreViewSort.bind(viewSortSyncService),
|
||||||
|
destroy:
|
||||||
|
viewSortSyncService.destroyCoreViewSort.bind(viewSortSyncService),
|
||||||
|
restore:
|
||||||
|
viewSortSyncService.restoreCoreViewSort.bind(viewSortSyncService),
|
||||||
|
},
|
||||||
|
ViewSortListener.name,
|
||||||
|
'view sort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.CREATED)
|
||||||
|
async handleViewSortCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordCreateEvent<ViewSortWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleCreated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.UPDATED)
|
||||||
|
async handleViewSortUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordUpdateEvent<ViewSortWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleUpdated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DELETED)
|
||||||
|
async handleViewSortDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<ViewSortWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDeleted(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DESTROYED)
|
||||||
|
async handleViewSortDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDestroyEvent<ViewSortWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDestroyed(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.RESTORED)
|
||||||
|
async handleViewSortRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordRestoreEvent<ViewSortWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleRestored(batchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||||
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
|
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||||
|
import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||||
|
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||||
|
import { ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||||
|
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||||
|
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||||
|
import { ViewSyncService } from 'src/modules/view/services/view-sync.service';
|
||||||
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
|
||||||
|
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewListener extends BaseViewSyncListener<ViewWorkspaceEntity> {
|
||||||
|
constructor(viewSyncService: ViewSyncService) {
|
||||||
|
super(
|
||||||
|
{
|
||||||
|
create: viewSyncService.createCoreView.bind(viewSyncService),
|
||||||
|
update: viewSyncService.updateCoreView.bind(viewSyncService),
|
||||||
|
delete: viewSyncService.deleteCoreView.bind(viewSyncService),
|
||||||
|
destroy: viewSyncService.destroyCoreView.bind(viewSyncService),
|
||||||
|
restore: viewSyncService.restoreCoreView.bind(viewSyncService),
|
||||||
|
},
|
||||||
|
ViewListener.name,
|
||||||
|
'view',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('view', DatabaseEventAction.CREATED)
|
||||||
|
async handleViewCreated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordCreateEvent<ViewWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleCreated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('view', DatabaseEventAction.UPDATED)
|
||||||
|
async handleViewUpdated(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordUpdateEvent<ViewWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleUpdated(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('view', DatabaseEventAction.DELETED)
|
||||||
|
async handleViewDeleted(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDeleteEvent<ViewWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDeleted(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('view', DatabaseEventAction.DESTROYED)
|
||||||
|
async handleViewDestroyed(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordDestroyEvent<ViewWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleDestroyed(batchEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnDatabaseBatchEvent('view', DatabaseEventAction.RESTORED)
|
||||||
|
async handleViewRestored(
|
||||||
|
batchEvent: WorkspaceEventBatch<
|
||||||
|
ObjectRecordRestoreEvent<ViewWorkspaceEntity>
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
return this.handleRestored(batchEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ViewField } from 'src/engine/metadata-modules/view/view-field.entity';
|
||||||
|
import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewFieldSyncService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ViewField, 'core')
|
||||||
|
private readonly coreViewFieldRepository: Repository<ViewField>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private parseUpdateDataFromDiff(
|
||||||
|
diff: Partial<ObjectRecordDiff<ViewFieldWorkspaceEntity>>,
|
||||||
|
): Partial<ViewField> {
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff)) {
|
||||||
|
const diffValue = diff[key as keyof ViewFieldWorkspaceEntity];
|
||||||
|
|
||||||
|
if (isDefined(diffValue)) {
|
||||||
|
updateData[key] = diffValue.after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateData as Partial<ViewField>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCoreViewField(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewField: ViewFieldWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
const coreViewField: Partial<ViewField> = {
|
||||||
|
id: workspaceViewField.id,
|
||||||
|
fieldMetadataId: workspaceViewField.fieldMetadataId,
|
||||||
|
viewId: workspaceViewField.viewId,
|
||||||
|
position: workspaceViewField.position,
|
||||||
|
isVisible: workspaceViewField.isVisible,
|
||||||
|
size: workspaceViewField.size,
|
||||||
|
workspaceId,
|
||||||
|
createdAt: new Date(workspaceViewField.createdAt),
|
||||||
|
updatedAt: new Date(workspaceViewField.updatedAt),
|
||||||
|
deletedAt: workspaceViewField.deletedAt
|
||||||
|
? new Date(workspaceViewField.deletedAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreViewFieldRepository.save(coreViewField);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCoreViewField(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||||
|
diff?: Partial<ObjectRecordDiff<ViewFieldWorkspaceEntity>>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!diff || Object.keys(diff).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await this.coreViewFieldRepository.update(
|
||||||
|
{ id: workspaceViewField.id, workspaceId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCoreViewField(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFieldRepository.softDelete({
|
||||||
|
id: workspaceViewField.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroyCoreViewField(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFieldRepository.delete({
|
||||||
|
id: workspaceViewField.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restoreCoreViewField(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFieldRepository.restore({
|
||||||
|
id: workspaceViewField.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view/enums/view-filter-group-logical-operator';
|
||||||
|
import { ViewFilterGroup } from 'src/engine/metadata-modules/view/view-filter-group.entity';
|
||||||
|
import { ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewFilterGroupSyncService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ViewFilterGroup, 'core')
|
||||||
|
private readonly coreViewFilterGroupRepository: Repository<ViewFilterGroup>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private parseUpdateDataFromDiff(
|
||||||
|
diff: Partial<ObjectRecordDiff<ViewFilterGroupWorkspaceEntity>>,
|
||||||
|
): Partial<ViewFilterGroup> {
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff)) {
|
||||||
|
const diffValue = diff[key as keyof ViewFilterGroupWorkspaceEntity];
|
||||||
|
|
||||||
|
if (isDefined(diffValue)) {
|
||||||
|
if (key === 'logicalOperator') {
|
||||||
|
updateData[key] = diffValue.after as ViewFilterGroupLogicalOperator;
|
||||||
|
} else {
|
||||||
|
updateData[key] = diffValue.after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateData as Partial<ViewFilterGroup>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCoreViewFilterGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
const coreViewFilterGroup: Partial<ViewFilterGroup> = {
|
||||||
|
id: workspaceViewFilterGroup.id,
|
||||||
|
viewId: workspaceViewFilterGroup.viewId,
|
||||||
|
logicalOperator:
|
||||||
|
workspaceViewFilterGroup.logicalOperator as ViewFilterGroupLogicalOperator,
|
||||||
|
parentViewFilterGroupId: workspaceViewFilterGroup.parentViewFilterGroupId,
|
||||||
|
positionInViewFilterGroup:
|
||||||
|
workspaceViewFilterGroup.positionInViewFilterGroup,
|
||||||
|
workspaceId,
|
||||||
|
createdAt: new Date(workspaceViewFilterGroup.createdAt),
|
||||||
|
updatedAt: new Date(workspaceViewFilterGroup.updatedAt),
|
||||||
|
deletedAt: workspaceViewFilterGroup.deletedAt
|
||||||
|
? new Date(workspaceViewFilterGroup.deletedAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreViewFilterGroupRepository.save(coreViewFilterGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCoreViewFilterGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
|
||||||
|
diff?: Partial<ObjectRecordDiff<ViewFilterGroupWorkspaceEntity>>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!diff || Object.keys(diff).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await this.coreViewFilterGroupRepository.update(
|
||||||
|
{ id: workspaceViewFilterGroup.id, workspaceId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCoreViewFilterGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFilterGroupRepository.softDelete({
|
||||||
|
id: workspaceViewFilterGroup.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroyCoreViewFilterGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFilterGroupRepository.delete({
|
||||||
|
id: workspaceViewFilterGroup.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restoreCoreViewFilterGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFilterGroupRepository.restore({
|
||||||
|
id: workspaceViewFilterGroup.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ViewFilter } from 'src/engine/metadata-modules/view/view-filter.entity';
|
||||||
|
import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||||
|
import { transformViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/transform-view-filter-workspace-value-to-core-value';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewFilterSyncService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ViewFilter, 'core')
|
||||||
|
private readonly coreViewFilterRepository: Repository<ViewFilter>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private parseUpdateDataFromDiff(
|
||||||
|
diff: Partial<ObjectRecordDiff<ViewFilterWorkspaceEntity>>,
|
||||||
|
): Partial<ViewFilter> {
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff)) {
|
||||||
|
const diffValue = diff[key as keyof ViewFilterWorkspaceEntity];
|
||||||
|
|
||||||
|
if (isDefined(diffValue)) {
|
||||||
|
if (key === 'value' && typeof diffValue.after === 'string') {
|
||||||
|
updateData[key] = transformViewFilterWorkspaceValueToCoreValue(
|
||||||
|
diffValue.after,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updateData[key] = diffValue.after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateData as Partial<ViewFilter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCoreViewFilter(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilter: ViewFilterWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!workspaceViewFilter.viewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreViewFilter: Partial<ViewFilter> = {
|
||||||
|
id: workspaceViewFilter.id,
|
||||||
|
fieldMetadataId: workspaceViewFilter.fieldMetadataId,
|
||||||
|
viewId: workspaceViewFilter.viewId,
|
||||||
|
operand: workspaceViewFilter.operand,
|
||||||
|
value: transformViewFilterWorkspaceValueToCoreValue(
|
||||||
|
workspaceViewFilter.value,
|
||||||
|
),
|
||||||
|
viewFilterGroupId: workspaceViewFilter.viewFilterGroupId,
|
||||||
|
workspaceId,
|
||||||
|
createdAt: new Date(workspaceViewFilter.createdAt),
|
||||||
|
updatedAt: new Date(workspaceViewFilter.updatedAt),
|
||||||
|
deletedAt: workspaceViewFilter.deletedAt
|
||||||
|
? new Date(workspaceViewFilter.deletedAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreViewFilterRepository.save(coreViewFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCoreViewFilter(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilter: ViewFilterWorkspaceEntity,
|
||||||
|
diff?: Partial<ObjectRecordDiff<ViewFilterWorkspaceEntity>>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!workspaceViewFilter.viewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diff || Object.keys(diff).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await this.coreViewFilterRepository.update(
|
||||||
|
{ id: workspaceViewFilter.id, workspaceId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCoreViewFilter(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFilterRepository.softDelete({
|
||||||
|
id: workspaceViewFilter.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroyCoreViewFilter(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFilterRepository.delete({
|
||||||
|
id: workspaceViewFilter.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restoreCoreViewFilter(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewFilterRepository.restore({
|
||||||
|
id: workspaceViewFilter.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ViewGroup } from 'src/engine/metadata-modules/view/view-group.entity';
|
||||||
|
import { ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewGroupSyncService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ViewGroup, 'core')
|
||||||
|
private readonly coreViewGroupRepository: Repository<ViewGroup>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private parseUpdateDataFromDiff(
|
||||||
|
diff: Partial<ObjectRecordDiff<ViewGroupWorkspaceEntity>>,
|
||||||
|
): Partial<ViewGroup> {
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff)) {
|
||||||
|
const diffValue = diff[key as keyof ViewGroupWorkspaceEntity];
|
||||||
|
|
||||||
|
if (isDefined(diffValue)) {
|
||||||
|
updateData[key] = diffValue.after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateData as Partial<ViewGroup>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCoreViewGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewGroup: ViewGroupWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!workspaceViewGroup.viewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coreViewGroup: Partial<ViewGroup> = {
|
||||||
|
id: workspaceViewGroup.id,
|
||||||
|
fieldMetadataId: workspaceViewGroup.fieldMetadataId,
|
||||||
|
viewId: workspaceViewGroup.viewId,
|
||||||
|
fieldValue: workspaceViewGroup.fieldValue,
|
||||||
|
isVisible: workspaceViewGroup.isVisible,
|
||||||
|
position: workspaceViewGroup.position,
|
||||||
|
workspaceId,
|
||||||
|
createdAt: new Date(workspaceViewGroup.createdAt),
|
||||||
|
updatedAt: new Date(workspaceViewGroup.updatedAt),
|
||||||
|
deletedAt: workspaceViewGroup.deletedAt
|
||||||
|
? new Date(workspaceViewGroup.deletedAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreViewGroupRepository.save(coreViewGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCoreViewGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewGroup: ViewGroupWorkspaceEntity,
|
||||||
|
diff?: Partial<ObjectRecordDiff<ViewGroupWorkspaceEntity>>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!workspaceViewGroup.viewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diff || Object.keys(diff).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await this.coreViewGroupRepository.update(
|
||||||
|
{ id: workspaceViewGroup.id, workspaceId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCoreViewGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewGroupRepository.softDelete({
|
||||||
|
id: workspaceViewGroup.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroyCoreViewGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewGroupRepository.delete({
|
||||||
|
id: workspaceViewGroup.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restoreCoreViewGroup(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewGroupRepository.restore({
|
||||||
|
id: workspaceViewGroup.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ViewSortDirection } from 'src/engine/metadata-modules/view/enums/view-sort-direction';
|
||||||
|
import { ViewSort } from 'src/engine/metadata-modules/view/view-sort.entity';
|
||||||
|
import { ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewSortSyncService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ViewSort, 'core')
|
||||||
|
private readonly coreViewSortRepository: Repository<ViewSort>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private parseUpdateDataFromDiff(
|
||||||
|
diff: Partial<ObjectRecordDiff<ViewSortWorkspaceEntity>>,
|
||||||
|
): Partial<ViewSort> {
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff)) {
|
||||||
|
const diffValue = diff[key as keyof ViewSortWorkspaceEntity];
|
||||||
|
|
||||||
|
if (isDefined(diffValue)) {
|
||||||
|
if (key === 'direction') {
|
||||||
|
updateData[key] = (
|
||||||
|
diffValue.after as string
|
||||||
|
).toUpperCase() as ViewSortDirection;
|
||||||
|
} else {
|
||||||
|
updateData[key] = diffValue.after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateData as Partial<ViewSort>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCoreViewSort(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewSort: ViewSortWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!workspaceViewSort.viewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction =
|
||||||
|
workspaceViewSort.direction.toUpperCase() as ViewSortDirection;
|
||||||
|
|
||||||
|
const coreViewSort: Partial<ViewSort> = {
|
||||||
|
id: workspaceViewSort.id,
|
||||||
|
fieldMetadataId: workspaceViewSort.fieldMetadataId,
|
||||||
|
viewId: workspaceViewSort.viewId,
|
||||||
|
direction: direction,
|
||||||
|
workspaceId,
|
||||||
|
createdAt: new Date(workspaceViewSort.createdAt),
|
||||||
|
updatedAt: new Date(workspaceViewSort.updatedAt),
|
||||||
|
deletedAt: workspaceViewSort.deletedAt
|
||||||
|
? new Date(workspaceViewSort.deletedAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreViewSortRepository.save(coreViewSort);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCoreViewSort(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewSort: ViewSortWorkspaceEntity,
|
||||||
|
diff?: Partial<ObjectRecordDiff<ViewSortWorkspaceEntity>>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!workspaceViewSort.viewId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diff || Object.keys(diff).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await this.coreViewSortRepository.update(
|
||||||
|
{ id: workspaceViewSort.id, workspaceId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCoreViewSort(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewSortRepository.softDelete({
|
||||||
|
id: workspaceViewSort.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroyCoreViewSort(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewSortRepository.delete({
|
||||||
|
id: workspaceViewSort.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restoreCoreViewSort(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewSortRepository.restore({
|
||||||
|
id: workspaceViewSort.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||||
|
import { ViewOpenRecordIn } from 'src/engine/metadata-modules/view/enums/view-open-record-in';
|
||||||
|
import { View } from 'src/engine/metadata-modules/view/view.entity';
|
||||||
|
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ViewSyncService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(View, 'core')
|
||||||
|
private readonly coreViewRepository: Repository<View>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private parseUpdateDataFromDiff(
|
||||||
|
diff: Partial<ObjectRecordDiff<ViewWorkspaceEntity>>,
|
||||||
|
): Partial<View> {
|
||||||
|
const updateData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(diff)) {
|
||||||
|
const diffValue = diff[key as keyof ViewWorkspaceEntity];
|
||||||
|
|
||||||
|
if (isDefined(diffValue)) {
|
||||||
|
if (key === 'openRecordIn') {
|
||||||
|
updateData[key] =
|
||||||
|
diffValue.after === 'SIDE_PANEL'
|
||||||
|
? ViewOpenRecordIn.SIDE_PANEL
|
||||||
|
: ViewOpenRecordIn.RECORD_PAGE;
|
||||||
|
} else {
|
||||||
|
updateData[key] = diffValue.after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateData as Partial<View>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCoreView(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceView: ViewWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
const coreView: Partial<View> = {
|
||||||
|
id: workspaceView.id,
|
||||||
|
name: workspaceView.name,
|
||||||
|
objectMetadataId: workspaceView.objectMetadataId,
|
||||||
|
type: workspaceView.type,
|
||||||
|
key: workspaceView.key,
|
||||||
|
icon: workspaceView.icon,
|
||||||
|
position: workspaceView.position,
|
||||||
|
isCompact: workspaceView.isCompact,
|
||||||
|
openRecordIn:
|
||||||
|
workspaceView.openRecordIn === 'SIDE_PANEL'
|
||||||
|
? ViewOpenRecordIn.SIDE_PANEL
|
||||||
|
: ViewOpenRecordIn.RECORD_PAGE,
|
||||||
|
kanbanAggregateOperation: workspaceView.kanbanAggregateOperation,
|
||||||
|
kanbanAggregateOperationFieldMetadataId:
|
||||||
|
workspaceView.kanbanAggregateOperationFieldMetadataId,
|
||||||
|
workspaceId,
|
||||||
|
createdAt: new Date(workspaceView.createdAt),
|
||||||
|
updatedAt: new Date(workspaceView.updatedAt),
|
||||||
|
deletedAt: workspaceView.deletedAt
|
||||||
|
? new Date(workspaceView.deletedAt)
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.coreViewRepository.save(coreView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateCoreView(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceView: ViewWorkspaceEntity,
|
||||||
|
diff?: Partial<ObjectRecordDiff<ViewWorkspaceEntity>>,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!diff || Object.keys(diff).length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length > 0) {
|
||||||
|
await this.coreViewRepository.update(
|
||||||
|
{ id: workspaceView.id, workspaceId },
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteCoreView(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceView: ViewWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewRepository.softDelete({
|
||||||
|
id: workspaceView.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async destroyCoreView(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceView: ViewWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewRepository.delete({
|
||||||
|
id: workspaceView.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async restoreCoreView(
|
||||||
|
workspaceId: string,
|
||||||
|
workspaceView: ViewWorkspaceEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.coreViewRepository.restore({
|
||||||
|
id: workspaceView.id,
|
||||||
|
workspaceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { ViewFilterValue } from 'src/engine/metadata-modules/view/types/view-filter-value.type';
|
||||||
|
|
||||||
|
export const transformViewFilterWorkspaceValueToCoreValue = (
|
||||||
|
value: string,
|
||||||
|
): ViewFilterValue => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,11 +1,52 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
|
import { ViewField } from 'src/engine/metadata-modules/view/view-field.entity';
|
||||||
|
import { ViewFilterGroup } from 'src/engine/metadata-modules/view/view-filter-group.entity';
|
||||||
|
import { ViewFilter } from 'src/engine/metadata-modules/view/view-filter.entity';
|
||||||
|
import { ViewGroup } from 'src/engine/metadata-modules/view/view-group.entity';
|
||||||
|
import { ViewSort } from 'src/engine/metadata-modules/view/view-sort.entity';
|
||||||
|
import { View } from 'src/engine/metadata-modules/view/view.entity';
|
||||||
|
import { ViewFieldListener } from 'src/modules/view/listeners/view-field.listener';
|
||||||
|
import { ViewFilterGroupListener } from 'src/modules/view/listeners/view-filter-group.listener';
|
||||||
|
import { ViewFilterListener } from 'src/modules/view/listeners/view-filter.listener';
|
||||||
|
import { ViewGroupListener } from 'src/modules/view/listeners/view-group.listener';
|
||||||
|
import { ViewSortListener } from 'src/modules/view/listeners/view-sort.listener';
|
||||||
|
import { ViewListener } from 'src/modules/view/listeners/view.listener';
|
||||||
|
import { ViewDeleteOnePreQueryHook } from 'src/modules/view/pre-hooks/view-delete-one.pre-query.hook';
|
||||||
|
import { ViewFieldSyncService } from 'src/modules/view/services/view-field-sync.service';
|
||||||
|
import { ViewFilterGroupSyncService } from 'src/modules/view/services/view-filter-group-sync.service';
|
||||||
|
import { ViewFilterSyncService } from 'src/modules/view/services/view-filter-sync.service';
|
||||||
|
import { ViewGroupSyncService } from 'src/modules/view/services/view-group-sync.service';
|
||||||
|
import { ViewSortSyncService } from 'src/modules/view/services/view-sort-sync.service';
|
||||||
|
import { ViewSyncService } from 'src/modules/view/services/view-sync.service';
|
||||||
import { ViewService } from 'src/modules/view/services/view.service';
|
import { ViewService } from 'src/modules/view/services/view.service';
|
||||||
|
|
||||||
import { ViewDeleteOnePreQueryHook } from './pre-hooks/view-delete-one.pre-query.hook';
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
providers: [ViewService, ViewDeleteOnePreQueryHook],
|
TypeOrmModule.forFeature(
|
||||||
|
[View, ViewField, ViewFilter, ViewFilterGroup, ViewGroup, ViewSort],
|
||||||
|
'core',
|
||||||
|
),
|
||||||
|
FeatureFlagModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ViewService,
|
||||||
|
ViewDeleteOnePreQueryHook,
|
||||||
|
ViewSyncService,
|
||||||
|
ViewFieldSyncService,
|
||||||
|
ViewFilterSyncService,
|
||||||
|
ViewFilterGroupSyncService,
|
||||||
|
ViewGroupSyncService,
|
||||||
|
ViewSortSyncService,
|
||||||
|
ViewListener,
|
||||||
|
ViewFieldListener,
|
||||||
|
ViewFilterListener,
|
||||||
|
ViewFilterGroupListener,
|
||||||
|
ViewGroupListener,
|
||||||
|
ViewSortListener,
|
||||||
|
],
|
||||||
exports: [ViewService],
|
exports: [ViewService],
|
||||||
})
|
})
|
||||||
export class ViewModule {}
|
export class ViewModule {}
|
||||||
|
|||||||
@ -10,10 +10,12 @@ export enum ViewExceptionCode {
|
|||||||
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
|
VIEW_NOT_FOUND = 'VIEW_NOT_FOUND',
|
||||||
CANNOT_DELETE_INDEX_VIEW = 'CANNOT_DELETE_INDEX_VIEW',
|
CANNOT_DELETE_INDEX_VIEW = 'CANNOT_DELETE_INDEX_VIEW',
|
||||||
METHOD_NOT_IMPLEMENTED = 'METHOD_NOT_IMPLEMENTED',
|
METHOD_NOT_IMPLEMENTED = 'METHOD_NOT_IMPLEMENTED',
|
||||||
|
CORE_VIEW_SYNC_ERROR = 'CORE_VIEW_SYNC_ERROR',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ViewExceptionMessage {
|
export enum ViewExceptionMessage {
|
||||||
VIEW_NOT_FOUND = 'View not found',
|
VIEW_NOT_FOUND = 'View not found',
|
||||||
CANNOT_DELETE_INDEX_VIEW = 'Cannot delete index view',
|
CANNOT_DELETE_INDEX_VIEW = 'Cannot delete index view',
|
||||||
METHOD_NOT_IMPLEMENTED = 'Method not implemented',
|
METHOD_NOT_IMPLEMENTED = 'Method not implemented',
|
||||||
|
CORE_VIEW_SYNC_ERROR = 'Failed to sync view data to core',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
import { findManyFieldsMetadataQueryFactory } from 'test/integration/metadata/suites/field-metadata/utils/find-many-fields-metadata-query-factory.util';
|
||||||
|
import { createMorphRelationBetweenObjects } from 'test/integration/metadata/suites/object-metadata/utils/create-morph-relation-between-objects.util';
|
||||||
|
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||||
|
import { forceCreateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/force-create-one-object-metadata.util';
|
||||||
|
import { updateOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/update-one-object-metadata.util';
|
||||||
|
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
|
||||||
|
|
||||||
|
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||||
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
|
describe('Rename an object metadata with morph relation should succeed', () => {
|
||||||
|
let opportunityId = '';
|
||||||
|
let personId = '';
|
||||||
|
let companyId = '';
|
||||||
|
let morphRelationField: FieldMetadataEntity<FieldMetadataType.MORPH_RELATION> & {
|
||||||
|
morphRelations: RelationDTO[];
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
createOneObject: { id: aId },
|
||||||
|
},
|
||||||
|
} = await forceCreateOneObjectMetadata({
|
||||||
|
input: {
|
||||||
|
nameSingular: 'opportunityForRename',
|
||||||
|
namePlural: 'opportunitiesForRename',
|
||||||
|
labelSingular: 'Opportunity For Rename',
|
||||||
|
labelPlural: 'Opportunities For Rename',
|
||||||
|
icon: 'IconOpportunity',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
opportunityId = aId;
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
createOneObject: { id: bId },
|
||||||
|
},
|
||||||
|
} = await forceCreateOneObjectMetadata({
|
||||||
|
input: {
|
||||||
|
nameSingular: 'personForRename',
|
||||||
|
namePlural: 'peopleForRename',
|
||||||
|
labelSingular: 'Person For Rename',
|
||||||
|
labelPlural: 'People For Rename',
|
||||||
|
icon: 'IconPerson',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
personId = bId;
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
createOneObject: { id: cId },
|
||||||
|
},
|
||||||
|
} = await forceCreateOneObjectMetadata({
|
||||||
|
input: {
|
||||||
|
nameSingular: 'companyForRename',
|
||||||
|
namePlural: 'companiesForRename',
|
||||||
|
labelSingular: 'Company For Rename',
|
||||||
|
labelPlural: 'Companies For Rename',
|
||||||
|
icon: 'IconCompany',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
companyId = cId;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await deleteOneObjectMetadata({ input: { idToDelete: opportunityId } });
|
||||||
|
await deleteOneObjectMetadata({ input: { idToDelete: personId } });
|
||||||
|
await deleteOneObjectMetadata({ input: { idToDelete: companyId } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should rename custom object, and update the join column name of the morph relation that contains the object name', async () => {
|
||||||
|
morphRelationField = await createMorphRelationBetweenObjects({
|
||||||
|
name: 'owner',
|
||||||
|
objectMetadataId: opportunityId,
|
||||||
|
firstTargetObjectMetadataId: personId,
|
||||||
|
secondTargetObjectMetadataId: companyId,
|
||||||
|
type: FieldMetadataType.MORPH_RELATION,
|
||||||
|
relationType: RelationType.MANY_TO_ONE,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await updateOneObjectMetadata({
|
||||||
|
gqlFields: `
|
||||||
|
nameSingular
|
||||||
|
labelSingular
|
||||||
|
namePlural
|
||||||
|
labelPlural
|
||||||
|
`,
|
||||||
|
input: {
|
||||||
|
idToUpdate: personId,
|
||||||
|
updatePayload: {
|
||||||
|
nameSingular: 'personForRename2',
|
||||||
|
namePlural: 'peopleForRename2',
|
||||||
|
labelSingular: 'Person For Rename2',
|
||||||
|
labelPlural: 'People For Rename2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(data.updateOneObject.nameSingular).toBe('personForRename2');
|
||||||
|
|
||||||
|
const ownerFieldMetadataOnPersonId = morphRelationField.morphRelations.find(
|
||||||
|
(morphRelation) => morphRelation.targetObjectMetadata.id === personId,
|
||||||
|
)?.sourceFieldMetadata.id;
|
||||||
|
|
||||||
|
if (!ownerFieldMetadataOnPersonId) {
|
||||||
|
throw new Error(
|
||||||
|
'Morph Relation Error: Owner field metadata on person not found',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldAfterRenaming = await findFieldMetadata({
|
||||||
|
fieldMetadataId: ownerFieldMetadataOnPersonId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fieldAfterRenaming.settings.joinColumnName).toBe(
|
||||||
|
'ownerPersonForRename2Id',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const findFieldMetadata = async ({
|
||||||
|
fieldMetadataId,
|
||||||
|
}: {
|
||||||
|
fieldMetadataId: string;
|
||||||
|
}) => {
|
||||||
|
const operation = findManyFieldsMetadataQueryFactory({
|
||||||
|
gqlFields: `
|
||||||
|
id
|
||||||
|
name
|
||||||
|
object { id nameSingular }
|
||||||
|
relation { type targetFieldMetadata { id } targetObjectMetadata { id } }
|
||||||
|
settings
|
||||||
|
`,
|
||||||
|
input: {
|
||||||
|
filter: { id: { eq: fieldMetadataId } },
|
||||||
|
paging: { first: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const fields = await makeMetadataAPIRequest(operation);
|
||||||
|
const field = fields.body.data.fields.edges?.[0]?.node;
|
||||||
|
|
||||||
|
return field;
|
||||||
|
};
|
||||||
@ -74,6 +74,12 @@ export const createMorphRelationBetweenObjects = async ({
|
|||||||
targetObjectMetadata {
|
targetObjectMetadata {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
sourceFieldMetadata {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
sourceObjectMetadata {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
expectToFail: false,
|
expectToFail: false,
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export default {
|
|||||||
label: 'Api Key',
|
label: 'Api Key',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
helpText:
|
helpText:
|
||||||
'Create an API key in [your twenty workspace](https://app.twenty.com/settings/apis)',
|
'Create an API key in [your twenty workspace](https://crm.rootxwire.com/settings/apis)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
computed: false,
|
computed: false,
|
||||||
@ -29,7 +29,7 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
label: 'Api Url',
|
label: 'Api Url',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
placeholder: 'https://api.twenty.com',
|
placeholder: 'https://crm.rootxwire.com',
|
||||||
helpText:
|
helpText:
|
||||||
'Set this only if you self-host Twenty. Use the same value as `REACT_APP_SERVER_BASE_URL` in https://docs.twenty.com/start/self-hosting/',
|
'Set this only if you self-host Twenty. Use the same value as `REACT_APP_SERVER_BASE_URL` in https://docs.twenty.com/start/self-hosting/',
|
||||||
},
|
},
|
||||||
|
|||||||