From cb1e34a3e0f9ef307e10cf50e3fafca78f2f85b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 08:43:17 +0000 Subject: [PATCH 1/4] Restrict content scope values to string, number, null and undefined The content scope interfaces accepted values of any type (Record, [key: string]: unknown), so mistakes involving scopes went unnoticed by the compiler. Narrowing the value type to string | number | null | undefined surfaced several of them: - Embeddable scope properties and resolver scope arguments were typed `typeof Scope`, the scope class, instead of a scope instance. - The Brevo redirect entity factory had the same problem. - DeleteUnsubscribedBrevoContactsConsole wrapped the scope in another object before passing it to the Brevo contacts API, so the API client was resolved for a scope that can never match a Brevo config. Router match params in the admin are always strings, so they no longer derive their type from ContentScope. Scope classes need an explicit index signature to stay assignable to the scope interfaces, as TypeScript does not infer one for classes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnYJ1LdUKw9FxdKdFAdWAr --- .../narrow-content-scope-value-types.md | 49 +++++++++++++++++++ .../dto/brevo-contact-subscribe.scope.ts | 2 + .../email-campaign-content-scope.ts | 2 + demo/api/src/dam/dto/dam-scope.ts | 2 + demo/api/src/news/entities/news.entity.ts | 2 + .../src/page-tree/dto/page-tree-node-scope.ts | 2 + demo/api/src/redirects/dto/redirect-scope.ts | 2 + .../brevoConfiguration/BrevoConfigPage.tsx | 13 ++--- .../src/brevoContacts/BrevoContactsPage.tsx | 13 ++--- .../BrevoTestContactsPage.tsx | 13 ++--- .../src/emailCampaigns/EmailCampaignsPage.tsx | 13 ++--- .../form/TestEmailCampaignForm.tsx | 13 ++--- .../src/targetGroups/TargetGroupsPage.tsx | 13 ++--- .../contentScope/ContentScopeIndicator.tsx | 2 +- .../src/contentScope/ContentScopeSelect.tsx | 14 ++++-- .../cms-admin/src/contentScope/Provider.tsx | 22 ++++----- .../ContentScopeSelect.stories.tsx | 4 +- .../src/dam/config/DamScopeContext.ts | 4 +- .../src/dam/config/DamScopeProvider.tsx | 17 +++---- .../cms-admin/src/dam/config/useDamScope.ts | 3 +- .../admin/cms-admin/src/form/file/upload.ts | 3 +- .../src/pages/pageTree/findAvailableSlug.ts | 4 +- .../useCopyPastePages/createInboxFolder.ts | 5 +- .../pageTree/useCopyPastePages/sendPages.tsx | 8 +-- .../pageTree/useTranslatePagesAction.tsx | 2 +- .../cms-admin/src/redirects/RedirectForm.tsx | 3 +- .../cms-admin/src/redirects/RedirectsGrid.tsx | 3 +- .../src/redirects/redirectsConfig.ts | 15 +++--- .../cms-admin/src/redirects/submitMutation.ts | 3 +- .../translation/AzureAiTranslatorProvider.tsx | 4 +- .../cms-admin/src/warnings/WarningsGrid.tsx | 2 +- .../blacklisted-contacts.entity.factory.ts | 2 +- .../src/brevo-config/brevo-config.resolver.ts | 10 ++-- .../entities/brevo-config-entity.factory.ts | 2 +- .../brevo-contact-import.console.ts | 8 +-- .../brevo-contact/brevo-contact.console.ts | 6 +-- .../brevo-contact/brevo-contact.resolver.ts | 14 +++--- .../validator/redirect-url.validator.ts | 4 +- .../brevo-email-import-log.entity.factory.ts | 2 +- .../email-campaign/email-campaign.resolver.ts | 2 +- .../entities/email-campaign-entity.factory.ts | 2 +- .../entity/target-group-entity.factory.ts | 2 +- .../src/target-group/target-group.resolver.ts | 2 +- packages/api/brevo-api/src/types.ts | 3 +- .../src/dam/files/dto/empty-dam-scope.ts | 2 +- .../dto/find-copies-of-file-in-scope.args.ts | 2 +- .../src/dam/files/entities/file.entity.ts | 2 +- .../src/dam/files/entities/folder.entity.ts | 2 +- .../cms-api/src/dam/files/files.resolver.ts | 4 +- .../cms-api/src/dam/files/folders.resolver.ts | 6 +-- packages/api/cms-api/src/dam/types.ts | 3 +- ...internal-link-block-transformer.service.ts | 3 +- .../dto/empty-page-tree-node-scope.ts | 2 +- packages/api/cms-api/src/page-tree/types.ts | 3 +- .../src/redirects/dto/empty-redirect-scope.ts | 2 +- .../entities/redirect-entity.factory.ts | 2 +- .../src/redirects/redirects.resolver.ts | 6 +-- packages/api/cms-api/src/redirects/types.ts | 3 +- .../src/sitePreview/previewUtils.ts | 3 +- 59 files changed, 201 insertions(+), 160 deletions(-) create mode 100644 .changeset/narrow-content-scope-value-types.md diff --git a/.changeset/narrow-content-scope-value-types.md b/.changeset/narrow-content-scope-value-types.md new file mode 100644 index 00000000000..af926241b8a --- /dev/null +++ b/.changeset/narrow-content-scope-value-types.md @@ -0,0 +1,49 @@ +--- +"@dextinity/cms-api": major +"@dextinity/brevo-api": major +"@dextinity/cms-admin": major +"@dextinity/site-nextjs": major +--- + +Restrict content scope values to `string | number | null | undefined` + +The content scope interfaces accepted values of any type (`Record`, `[key: string]: unknown`), which hid mistakes such as passing a scope class instead of a scope instance, or wrapping a scope in another object. +Scope dimensions are now typed as `string | number | null | undefined`: + +- `ScopeInterface` (page tree), `RedirectScopeInterface` and `DamScopeInterface` in `@dextinity/cms-api` +- `EmailCampaignScopeInterface` in `@dextinity/brevo-api` +- `ContentScope` in `@dextinity/cms-admin` +- `scope` in the preview params returned by `previewParams()`, `legacyPagesRouterPreviewParams()` and `setSitePreviewParams()` in `@dextinity/site-nextjs` + +**Migration** + +TypeScript only gives classes an index signature when it is declared explicitly, so scope classes in your application need one: + +```ts +@Embeddable() +@ObjectType("PageTreeNodeScope") +@InputType("PageTreeNodeScopeInput") +export class PageTreeNodeScope { + [key: string]: string | number | null | undefined; + + @Property({ columnType: "text" }) + @Field() + @IsString() + domain: string; + + @Property({ columnType: "text" }) + @Field() + @IsString() + language: string; +} +``` + +Places that used a scope value where a `string` is expected (for instance a language passed to a translation service) now need an explicit conversion: + +```ts +// Before +transformToSlug(name, scope.language); + +// After +transformToSlug(name, String(scope.language)); +``` diff --git a/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts b/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts index 513d44d1bdc..fc6ca6af131 100644 --- a/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts +++ b/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts @@ -1,6 +1,8 @@ import { IsString, MaxLength } from "class-validator"; export class EmailContactSubscribeScope { + [key: string]: string | number | null | undefined; + @IsString() @MaxLength(64) domain: string; diff --git a/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts b/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts index c1180c6a33e..3d05564707d 100644 --- a/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts +++ b/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts @@ -6,6 +6,8 @@ import { IsString } from "class-validator"; @ObjectType() @InputType("EmailCampaignContentScopeInput") export class EmailCampaignContentScope { + [key: string]: string | number | null | undefined; + @Property({ columnType: "text" }) @Field() @IsString() diff --git a/demo/api/src/dam/dto/dam-scope.ts b/demo/api/src/dam/dto/dam-scope.ts index e1f3a664cc6..01976bb7f21 100644 --- a/demo/api/src/dam/dto/dam-scope.ts +++ b/demo/api/src/dam/dto/dam-scope.ts @@ -6,6 +6,8 @@ import { IsString } from "class-validator"; @ObjectType() @InputType("DamScopeInput") export class DamScope { + [key: string]: string | number | null | undefined; + @Property({ columnType: "text" }) @Field() @IsString() diff --git a/demo/api/src/news/entities/news.entity.ts b/demo/api/src/news/entities/news.entity.ts index 954cf025ea0..72025e48d56 100644 --- a/demo/api/src/news/entities/news.entity.ts +++ b/demo/api/src/news/entities/news.entity.ts @@ -51,6 +51,8 @@ registerEnumType(NewsCategory, { @ObjectType("") @InputType("NewsContentScopeInput") export class NewsContentScope { + [key: string]: string | number | null | undefined; + @Property({ columnType: "text" }) @Field() @IsString() diff --git a/demo/api/src/page-tree/dto/page-tree-node-scope.ts b/demo/api/src/page-tree/dto/page-tree-node-scope.ts index 644c478a6e6..b5100fe14d6 100644 --- a/demo/api/src/page-tree/dto/page-tree-node-scope.ts +++ b/demo/api/src/page-tree/dto/page-tree-node-scope.ts @@ -7,6 +7,8 @@ import { IsString } from "class-validator"; @InputType("PageTreeNodeScopeInput") // name must not be changed in the app // @TODO: disguise @ObjectType("PageTreeContentScope") and @InputType("PageTreeContentScopeInput") decorators under a custom decorator: f.i. @PageTreeNodeScope export class PageTreeNodeScope { + [key: string]: string | number | null | undefined; + @Property({ columnType: "text" }) @Field() @IsString() diff --git a/demo/api/src/redirects/dto/redirect-scope.ts b/demo/api/src/redirects/dto/redirect-scope.ts index 031878aaed7..a233afb1c82 100644 --- a/demo/api/src/redirects/dto/redirect-scope.ts +++ b/demo/api/src/redirects/dto/redirect-scope.ts @@ -7,6 +7,8 @@ import { IsString } from "class-validator"; @InputType("RedirectScopeInput") // name must not be changed in the app // @TODO: disguise @ObjectType("RedirectScope") and @InputType("RedirectScopeInput") decorators under a custom decorator: f.i. @RedirectScope export class RedirectScope { + [key: string]: string | number | null | undefined; + @Index() // this does nothing, migration has to be created manually as the entity is in library @Property({ columnType: "text" }) @Field() diff --git a/packages/admin/brevo-admin/src/brevoConfiguration/BrevoConfigPage.tsx b/packages/admin/brevo-admin/src/brevoConfiguration/BrevoConfigPage.tsx index b92550534b0..e169ed5c42e 100644 --- a/packages/admin/brevo-admin/src/brevoConfiguration/BrevoConfigPage.tsx +++ b/packages/admin/brevo-admin/src/brevoConfiguration/BrevoConfigPage.tsx @@ -1,4 +1,4 @@ -import { useContentScope } from "@dextinity/cms-admin"; +import { type ContentScope, useContentScope } from "@dextinity/cms-admin"; import type { JSX } from "react"; import { useBrevoConfig } from "../common/BrevoConfigProvider"; @@ -8,13 +8,10 @@ export function BrevoConfigPage(): JSX.Element { const { scopeParts } = useBrevoConfig(); const { scope: completeScope } = useContentScope(); - const scope = scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ); + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope); return ; } diff --git a/packages/admin/brevo-admin/src/brevoContacts/BrevoContactsPage.tsx b/packages/admin/brevo-admin/src/brevoContacts/BrevoContactsPage.tsx index 12f1475abc7..4a4d045defb 100644 --- a/packages/admin/brevo-admin/src/brevoContacts/BrevoContactsPage.tsx +++ b/packages/admin/brevo-admin/src/brevoContacts/BrevoContactsPage.tsx @@ -1,5 +1,5 @@ import { type GridColDef, Stack, StackPage, StackSwitch, StackToolbar } from "@dextinity/admin"; -import { ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; +import { type ContentScope, ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; import type { DocumentNode } from "graphql"; import type { JSX, ReactNode } from "react"; import { useIntl } from "react-intl"; @@ -27,13 +27,10 @@ function createBrevoContactsPage({ const { scopeParts } = useBrevoConfig(); const { scope: completeScope } = useContentScope(); - const scope = scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ); + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope); return ( diff --git a/packages/admin/brevo-admin/src/brevoTestContacts/BrevoTestContactsPage.tsx b/packages/admin/brevo-admin/src/brevoTestContacts/BrevoTestContactsPage.tsx index 3a9f181d741..e800234060b 100644 --- a/packages/admin/brevo-admin/src/brevoTestContacts/BrevoTestContactsPage.tsx +++ b/packages/admin/brevo-admin/src/brevoTestContacts/BrevoTestContactsPage.tsx @@ -1,5 +1,5 @@ import { type GridColDef, Stack, StackPage, StackSwitch, StackToolbar } from "@dextinity/admin"; -import { ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; +import { type ContentScope, ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; import type { DocumentNode } from "graphql"; import type { JSX, ReactNode } from "react"; import { useIntl } from "react-intl"; @@ -31,13 +31,10 @@ function createBrevoTestContactsPage({ const scopeParts = passedScopeParts ?? brevoConfig.scopeParts; const { scope: completeScope } = useContentScope(); - const scope = scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ); + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope); return ( diff --git a/packages/admin/brevo-admin/src/emailCampaigns/EmailCampaignsPage.tsx b/packages/admin/brevo-admin/src/emailCampaigns/EmailCampaignsPage.tsx index 64c16158885..c39b882f6c1 100644 --- a/packages/admin/brevo-admin/src/emailCampaigns/EmailCampaignsPage.tsx +++ b/packages/admin/brevo-admin/src/emailCampaigns/EmailCampaignsPage.tsx @@ -1,5 +1,5 @@ import { Stack, StackPage, StackSwitch, StackToolbar } from "@dextinity/admin"; -import { type BlockInterface, ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; +import { type BlockInterface, type ContentScope, ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; import type { JSX } from "react"; import { useIntl } from "react-intl"; @@ -20,13 +20,10 @@ export function createEmailCampaignsPage({ EmailCampaignContentBlock }: CreateEm const { scope: completeScope } = useContentScope(); const intl = useIntl(); - const scope = scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ); + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope); return ( diff --git a/packages/admin/brevo-admin/src/emailCampaigns/form/TestEmailCampaignForm.tsx b/packages/admin/brevo-admin/src/emailCampaigns/form/TestEmailCampaignForm.tsx index b13b61fb175..af749d8e981 100644 --- a/packages/admin/brevo-admin/src/emailCampaigns/form/TestEmailCampaignForm.tsx +++ b/packages/admin/brevo-admin/src/emailCampaigns/form/TestEmailCampaignForm.tsx @@ -1,7 +1,7 @@ import { gql, useApolloClient, useQuery } from "@apollo/client"; import { Field, FinalForm, FinalFormSelect, SaveButton, Tooltip } from "@dextinity/admin"; import { Info, Newsletter } from "@dextinity/admin-icons"; -import { BlockAdminComponentPaper, BlockAdminComponentSectionGroup, useContentScope } from "@dextinity/cms-admin"; +import { BlockAdminComponentPaper, BlockAdminComponentSectionGroup, type ContentScope, useContentScope } from "@dextinity/cms-admin"; import { Card } from "@mui/material"; import { FormattedMessage } from "react-intl"; @@ -50,13 +50,10 @@ export const TestEmailCampaignForm = ({ id, isSendable = false, isCampaignCreate const { scopeParts } = useBrevoConfig(); const { scope: completeScope } = useContentScope(); - const scope = scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ); + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope); // Contact creation is limited to 100 at a time. Therefore, 100 contacts are queried without using pagination. const { data, loading, error } = useQuery(brevoTestContactsSelectQuery, { diff --git a/packages/admin/brevo-admin/src/targetGroups/TargetGroupsPage.tsx b/packages/admin/brevo-admin/src/targetGroups/TargetGroupsPage.tsx index e56f7083a2c..bc0b2131c53 100644 --- a/packages/admin/brevo-admin/src/targetGroups/TargetGroupsPage.tsx +++ b/packages/admin/brevo-admin/src/targetGroups/TargetGroupsPage.tsx @@ -1,5 +1,5 @@ import { Stack, StackPage, StackSwitch, Toolbar } from "@dextinity/admin"; -import { ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; +import { type ContentScope, ContentScopeIndicator, useContentScope } from "@dextinity/cms-admin"; import type { DocumentNode } from "graphql"; import type { JSX, ReactNode } from "react"; import { useIntl } from "react-intl"; @@ -26,13 +26,10 @@ export function createTargetGroupsPage({ additionalFormFields, nodeFragment, inp const { scope: completeScope } = useContentScope(); const intl = useIntl(); - const scope = scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ); + const scope = scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope); return ( diff --git a/packages/admin/cms-admin/src/contentScope/ContentScopeIndicator.tsx b/packages/admin/cms-admin/src/contentScope/ContentScopeIndicator.tsx index 5f0c9a0d9ab..d89392c69d0 100644 --- a/packages/admin/cms-admin/src/contentScope/ContentScopeIndicator.tsx +++ b/packages/admin/cms-admin/src/contentScope/ContentScopeIndicator.tsx @@ -25,7 +25,7 @@ export const ContentScopeIndicator = ({ global = false, scope: passedScope, chil const label = values.find((value) => { return value.scope[scopePart] === scope[scopePart]; })?.label; - return (label && label[scopePart]) ?? (scope[scopePart] ? capitalizeString(scope[scopePart]) : undefined); + return (label && label[scopePart]) ?? (scope[scopePart] ? capitalizeString(String(scope[scopePart])) : undefined); }; let content: ReactNode; diff --git a/packages/admin/cms-admin/src/contentScope/ContentScopeSelect.tsx b/packages/admin/cms-admin/src/contentScope/ContentScopeSelect.tsx index 1f85bd0dac7..e8600d45549 100644 --- a/packages/admin/cms-admin/src/contentScope/ContentScopeSelect.tsx +++ b/packages/admin/cms-admin/src/contentScope/ContentScopeSelect.tsx @@ -60,8 +60,11 @@ export function ContentScopeSelect({ if (searchable) { filteredOptions = options.filter((option) => { return ( - Object.values(option.scope).some((value) => value.toLowerCase().includes(searchValue.toLowerCase())) || - Object.values(option.label || []).some((value) => value?.toLowerCase().includes(searchValue.toLowerCase())) + Object.values(option.scope).some((value) => + String(value ?? "") + .toLowerCase() + .includes(searchValue.toLowerCase()), + ) || Object.values(option.label || []).some((value) => value?.toLowerCase().includes(searchValue.toLowerCase())) ); }); } @@ -71,12 +74,13 @@ export function ContentScopeSelect({ if (groupBy) { if (hasMultipleDimensions) { for (const option of filteredOptions) { - const groupForOption = groups.find((group) => group.value === option.scope[groupBy]); + const groupValue = String(option.scope[groupBy] ?? ""); + const groupForOption = groups.find((group) => group.value === groupValue); if (groupForOption) { groupForOption.options.push(option); } else { - groups.push({ value: option.scope[groupBy], label: option.label ? option.label[groupBy] : undefined, options: [option] }); + groups.push({ value: groupValue, label: option.label ? option.label[groupBy] : undefined, options: [option] }); } } } else { @@ -125,7 +129,7 @@ export function ContentScopeSelect({ if (!renderSelectedOption) { renderSelectedOption = (option) => { return Object.keys(option.scope) - .map((key) => humanReadableLabel({ label: option.label ? option.label[key] : undefined, value: option.scope[key] })) + .map((key) => humanReadableLabel({ label: option.label ? option.label[key] : undefined, value: String(option.scope[key] ?? "") })) .join(" / "); }; } diff --git a/packages/admin/cms-admin/src/contentScope/Provider.tsx b/packages/admin/cms-admin/src/contentScope/Provider.tsx index 16c16b92a54..1161f5324c7 100644 --- a/packages/admin/cms-admin/src/contentScope/Provider.tsx +++ b/packages/admin/cms-admin/src/contentScope/Provider.tsx @@ -8,8 +8,7 @@ import { NoContentScopeFallback } from "./noContentScopeFallback/NoContentScopeF import { defaultCreatePath } from "./utils/defaultCreatePath"; export interface ContentScope { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; + [key: string]: string | number | null | undefined; } type ContentScopeLocation = { @@ -36,10 +35,7 @@ const defaultContentScopeContext: ContentScopeContext = { location: defaultContentScopeLocation, }; -type NonNull = T extends null ? never : T; -type NonNullRecord = { - [P in keyof T]: NonNull; -}; +type ContentScopeRouterParams = Record; type SetContentScopeAction = (state: ContentScope) => ContentScope; @@ -64,7 +60,7 @@ const Context = createContext(defaultContentScopeContext); const NullValueAsString = "-"; // used to represent null-values in the url -function parseScopeFromRouterMatchParams(params: NonNullRecord): ContentScope { +function parseScopeFromRouterMatchParams(params: ContentScopeRouterParams): ContentScope { return Object.entries(params).reduce((a, [key, value]) => { return { ...a, @@ -73,13 +69,13 @@ function parseScopeFromRouterMatchParams(params: NonNullRecord): C }, {} as ContentScope); } -function formatScopeToRouterMatchParams(scope: Partial): NonNullRecord { +function formatScopeToRouterMatchParams(scope: ContentScope): ContentScopeRouterParams { return Object.entries(scope).reduce((a, [key, value]) => { return { ...a, - [key]: !value || value === null ? NullValueAsString : value, + [key]: value === null || value === undefined || value === "" ? NullValueAsString : String(value), }; - }, {} as NonNullRecord); + }, {} as ContentScopeRouterParams); } function defaultCreateUrl(scope: ContentScope) { @@ -93,7 +89,7 @@ function defaultCreateUrl(scope: ContentScope) { export function useContentScope(): UseContentScopeApi { const context = useContext(Context); const history = useHistory(); - const matchContextScope = useRouteMatch>(context?.path || ""); + const matchContextScope = useRouteMatch(context?.path || ""); const matchDefault = useRouteMatch(); const match = matchContextScope || matchDefault; @@ -124,7 +120,7 @@ export function useContentScope(): UseContentScopeApi { export interface ContentScopeProviderProps { defaultValue?: ContentScope; values?: ContentScopeValues; - children: (p: { match: match> }) => ReactNode; + children: (p: { match: match }) => ReactNode; location?: ContentScopeLocation; /** @@ -151,7 +147,7 @@ export function ContentScopeProvider({ } const path = location.createPath(values); - const match = useRouteMatch>(path); + const match = useRouteMatch(path); const [redirectPathAfterChange, setRedirectPathAfterChange] = useState(""); if (values.length === 0) { diff --git a/packages/admin/cms-admin/src/contentScope/__stories__/ContentScopeSelect.stories.tsx b/packages/admin/cms-admin/src/contentScope/__stories__/ContentScopeSelect.stories.tsx index 700a99611e9..654bb579bae 100644 --- a/packages/admin/cms-admin/src/contentScope/__stories__/ContentScopeSelect.stories.tsx +++ b/packages/admin/cms-admin/src/contentScope/__stories__/ContentScopeSelect.stories.tsx @@ -297,9 +297,9 @@ export const GroupingWithOptionalScopeParts = { renderOption={(option, query, isSelected) => { let text: string; if (option.scope.company === undefined) { - text = option.label?.country ?? option.scope.country; + text = option.label?.country ?? String(option.scope.country); } else { - text = option.label?.company ?? option.scope.company; + text = option.label?.company ?? String(option.scope.company); } const matches = findTextMatches(text, query); diff --git a/packages/admin/cms-admin/src/dam/config/DamScopeContext.ts b/packages/admin/cms-admin/src/dam/config/DamScopeContext.ts index d762e6e9f8e..f6e51cc3935 100644 --- a/packages/admin/cms-admin/src/dam/config/DamScopeContext.ts +++ b/packages/admin/cms-admin/src/dam/config/DamScopeContext.ts @@ -1,3 +1,5 @@ import { createContext } from "react"; -export const DamScopeContext = createContext>({}); +import type { ContentScope } from "../../contentScope/Provider"; + +export const DamScopeContext = createContext({}); diff --git a/packages/admin/cms-admin/src/dam/config/DamScopeProvider.tsx b/packages/admin/cms-admin/src/dam/config/DamScopeProvider.tsx index 9b52683be0e..defb67bc7c7 100644 --- a/packages/admin/cms-admin/src/dam/config/DamScopeProvider.tsx +++ b/packages/admin/cms-admin/src/dam/config/DamScopeProvider.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react"; -import { useContentScope } from "../../contentScope/Provider"; +import { type ContentScope, useContentScope } from "../../contentScope/Provider"; import { useDamConfig } from "./damConfig"; import { DamScopeContext } from "./DamScopeContext"; @@ -8,15 +8,12 @@ export function DamScopeProvider({ children }: { children?: ReactNode }) { const { scopeParts = [] } = useDamConfig(); const { scope: completeScope } = useContentScope(); - const damScope = scopeParts.reduce( - (damScope, scope) => { - if (completeScope[scope] !== undefined) { - damScope[scope] = completeScope[scope]; - } - return damScope; - }, - {} as Record, - ); + const damScope = scopeParts.reduce((damScope, scope) => { + if (completeScope[scope] !== undefined) { + damScope[scope] = completeScope[scope]; + } + return damScope; + }, {} as ContentScope); return {children}; } diff --git a/packages/admin/cms-admin/src/dam/config/useDamScope.ts b/packages/admin/cms-admin/src/dam/config/useDamScope.ts index 6f6ead7d0c4..82da7eb8a4a 100644 --- a/packages/admin/cms-admin/src/dam/config/useDamScope.ts +++ b/packages/admin/cms-admin/src/dam/config/useDamScope.ts @@ -1,8 +1,9 @@ import { useContext } from "react"; +import type { ContentScope } from "../../contentScope/Provider"; import { DamScopeContext } from "./DamScopeContext"; -function useDamScope(): Record { +function useDamScope(): ContentScope { return useContext(DamScopeContext); } diff --git a/packages/admin/cms-admin/src/form/file/upload.ts b/packages/admin/cms-admin/src/form/file/upload.ts index 7663693fc2f..6f4e423f86a 100644 --- a/packages/admin/cms-admin/src/form/file/upload.ts +++ b/packages/admin/cms-admin/src/form/file/upload.ts @@ -1,9 +1,10 @@ +import type { ContentScope } from "../../contentScope/Provider"; import type { GQLUpdateDamFileInput } from "../../graphql.generated"; interface UploadFileData { file: File & Pick & { importSource?: { importSourceType: string; importSourceId: string } }; - scope: Record; + scope: ContentScope; folderId?: string; /** * @deprecated Set `file.importSource.importSourceId` instead diff --git a/packages/admin/cms-admin/src/pages/pageTree/findAvailableSlug.ts b/packages/admin/cms-admin/src/pages/pageTree/findAvailableSlug.ts index d00c0f66f9b..f1942e88f76 100644 --- a/packages/admin/cms-admin/src/pages/pageTree/findAvailableSlug.ts +++ b/packages/admin/cms-admin/src/pages/pageTree/findAvailableSlug.ts @@ -1,6 +1,8 @@ import { type ApolloClient, gql } from "@apollo/client"; import { LocalErrorScopeApolloContext } from "@dextinity/admin"; +import type { ContentScope } from "../../contentScope/Provider"; + const slugAvailableQuery = gql` query FindAvailableSlug($parentId: ID, $slug: String!, $scope: PageTreeNodeScopeInput!) { pageTreeNodeSlugAvailable(parentId: $parentId, slug: $slug, scope: $scope) @@ -9,7 +11,7 @@ const slugAvailableQuery = gql` export async function findAvailableSlug( apolloClient: ApolloClient, - { slug, name, parentId, scope }: { slug: string; name: string; parentId: string | null; scope: Record }, + { slug, name, parentId, scope }: { slug: string; name: string; parentId: string | null; scope: ContentScope }, ): Promise<{ slug: string; name: string }> { let candidateSlug = slug; let candidateName = name; diff --git a/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/createInboxFolder.ts b/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/createInboxFolder.ts index 0750a747f5b..02e74606fec 100644 --- a/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/createInboxFolder.ts +++ b/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/createInboxFolder.ts @@ -2,6 +2,7 @@ import { type ApolloClient, gql } from "@apollo/client"; import { LocalErrorScopeApolloContext } from "@dextinity/admin"; import { format } from "date-fns"; +import type { ContentScope } from "../../../contentScope/Provider"; import type { GQLCreateInboxFolderMutation, GQLCreateInboxFolderMutationVariables } from "./createInboxFolder.generated"; export const createInboxFolder = async ({ @@ -10,8 +11,8 @@ export const createInboxFolder = async ({ sourceScopes, }: { client: ApolloClient; - targetScope: Record; - sourceScopes: Record[]; + targetScope: ContentScope; + sourceScopes: ContentScope[]; }) => { const scopeString = sourceScopes.length === 0 ? "unknown" : sourceScopes.map((scope) => Object.values(scope).join("-")).join(", "); const date = new Date(); diff --git a/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/sendPages.tsx b/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/sendPages.tsx index ba06743854c..272ae743881 100644 --- a/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/sendPages.tsx +++ b/packages/admin/cms-admin/src/pages/pageTree/useCopyPastePages/sendPages.tsx @@ -61,7 +61,7 @@ interface SendPagesDependencies { scope: ContentScope; documentTypes: PageTreeConfig["documentTypes"]; apiUrl: string; - damScope: Record; + damScope: ContentScope; currentCategory: string; damBasePath: string; } @@ -96,7 +96,7 @@ export async function sendPages( updateProgress(0, ); { let progressPages = 0; - const sourceScopes: Record[] = []; + const sourceScopes: ContentScope[] = []; for (const sourcePage of pages) { const documentType = documentTypes[sourcePage.documentType]; if (!documentType) { @@ -341,7 +341,7 @@ function unhandledDependenciesFromDocument( existingReplacements, hasDamScope = false, targetDamScope, - }: { existingReplacements: ReplaceDependencyObject[]; hasDamScope?: boolean; targetDamScope: Record }, + }: { existingReplacements: ReplaceDependencyObject[]; hasDamScope?: boolean; targetDamScope: ContentScope }, ) { const unhandledDependencies = documentType.dependencies(document).filter((dependency) => { if (isDamFileDependency(dependency)) { @@ -391,7 +391,7 @@ function fileDependenciesFromDocument(documentType: DocumentInterface, document: function isDamFileDependency( dependency: BlockDependency, -): dependency is BlockDependency & { data: { damFile: GQLDamFile & { scope?: Record } } } { +): dependency is BlockDependency & { data: { damFile: GQLDamFile & { scope?: ContentScope } } } { // eslint-disable-next-line @typescript-eslint/no-explicit-any return dependency.targetGraphqlObjectType === "DamFile" && dependency.data && (dependency.data as any).damFile; } diff --git a/packages/admin/cms-admin/src/pages/pageTree/useTranslatePagesAction.tsx b/packages/admin/cms-admin/src/pages/pageTree/useTranslatePagesAction.tsx index c4562592a95..61bf35fc0ef 100644 --- a/packages/admin/cms-admin/src/pages/pageTree/useTranslatePagesAction.tsx +++ b/packages/admin/cms-admin/src/pages/pageTree/useTranslatePagesAction.tsx @@ -108,7 +108,7 @@ export function useTranslatePagesAction({ pages, documentTypes }: Props): { const [translatedName, ...rest] = translatedTexts; translatedContentTexts = rest; - const translatedSlug = transformToSlug(translatedName, scope.language); + const translatedSlug = transformToSlug(translatedName, String(scope.language)); if (translatedName !== page.name || translatedSlug !== page.slug) { const available = await findAvailableSlug(apolloClient, { diff --git a/packages/admin/cms-admin/src/redirects/RedirectForm.tsx b/packages/admin/cms-admin/src/redirects/RedirectForm.tsx index b8cb7bc5762..0e898c2b4f5 100644 --- a/packages/admin/cms-admin/src/redirects/RedirectForm.tsx +++ b/packages/admin/cms-admin/src/redirects/RedirectForm.tsx @@ -24,6 +24,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { createFinalFormBlock } from "../blocks/form/createFinalFormBlock"; import type { BlockInterface, BlockState } from "../blocks/types"; import { ContentScopeIndicator } from "../contentScope/ContentScopeIndicator"; +import type { ContentScope } from "../contentScope/Provider"; import type { GQLRedirectSourceType } from "../graphql.generated"; import type { GQLRedirectSourceAvailableQuery, GQLRedirectSourceAvailableQueryVariables } from "./RedirectForm.generated"; import { redirectDetailQuery } from "./RedirectForm.gql"; @@ -42,7 +43,7 @@ interface Props { id?: string; mode: "edit" | "add"; linkBlock: BlockInterface; - scope: Record; + scope: ContentScope; } export interface FormValues { diff --git a/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx b/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx index f699464e5e9..cb1557fc4f9 100644 --- a/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx +++ b/packages/admin/cms-admin/src/redirects/RedirectsGrid.tsx @@ -35,6 +35,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { BlockPreviewContent } from "../blocks/common/blockRow/BlockPreviewContent"; import type { BlockInterface } from "../blocks/types"; +import type { ContentScope } from "../contentScope/Provider"; import { DataGrid } from "../dataGrid/DataGrid"; import RedirectActiveness from "./RedirectActiveness"; import { deleteRedirectMutation, deleteRedirectsMutation, paginatedRedirectsQuery } from "./RedirectsGrid.gql"; @@ -48,7 +49,7 @@ import { interface Props { linkBlock: BlockInterface; - scope: Record; + scope: ContentScope; } interface RedirectsGridToolbarProps extends GridToolbarProps { diff --git a/packages/admin/cms-admin/src/redirects/redirectsConfig.ts b/packages/admin/cms-admin/src/redirects/redirectsConfig.ts index 67673d18c0f..582b5497439 100644 --- a/packages/admin/cms-admin/src/redirects/redirectsConfig.ts +++ b/packages/admin/cms-admin/src/redirects/redirectsConfig.ts @@ -1,5 +1,5 @@ import { useDextinityConfig } from "../config/DextinityConfigContext"; -import { useContentScope } from "../contentScope/Provider"; +import { type ContentScope, useContentScope } from "../contentScope/Provider"; export interface RedirectsConfig { scopeParts?: string[]; @@ -15,18 +15,15 @@ function useRedirectsConfig(): RedirectsConfig { return dextinityConfig.redirects; } -export function useRedirectsScope(): { [key: string]: unknown } { +export function useRedirectsScope(): ContentScope { const { scopeParts } = useRedirectsConfig(); const { scope: completeScope } = useContentScope(); const redirectScope = scopeParts?.length - ? scopeParts.reduce( - (acc, scopePart) => { - acc[scopePart] = completeScope[scopePart]; - return acc; - }, - {} as { [key: string]: unknown }, - ) + ? scopeParts.reduce((acc, scopePart) => { + acc[scopePart] = completeScope[scopePart]; + return acc; + }, {} as ContentScope) : completeScope; return redirectScope; diff --git a/packages/admin/cms-admin/src/redirects/submitMutation.ts b/packages/admin/cms-admin/src/redirects/submitMutation.ts index 4c30a92121b..18017656bb2 100644 --- a/packages/admin/cms-admin/src/redirects/submitMutation.ts +++ b/packages/admin/cms-admin/src/redirects/submitMutation.ts @@ -3,6 +3,7 @@ import type { ApolloError } from "@apollo/client/errors"; import type { FetchResult } from "@apollo/client/link/core"; import type { BlockInterface } from "../blocks/types"; +import type { ContentScope } from "../contentScope/Provider"; import type { GQLRedirectInput } from "../graphql.generated"; import { createRedirectMutation, @@ -28,7 +29,7 @@ export const useSubmitMutation = ( mode: "edit" | "add", id: string | undefined, linkBlock: BlockInterface, - scope: Record, + scope: ContentScope, ): [ (values: FormValues) => Promise>, { loading: boolean; error: ApolloError | undefined }, diff --git a/packages/admin/cms-admin/src/translation/AzureAiTranslatorProvider.tsx b/packages/admin/cms-admin/src/translation/AzureAiTranslatorProvider.tsx index e663cac560a..ca4e7fca43e 100644 --- a/packages/admin/cms-admin/src/translation/AzureAiTranslatorProvider.tsx +++ b/packages/admin/cms-admin/src/translation/AzureAiTranslatorProvider.tsx @@ -23,7 +23,7 @@ export const AzureAiTranslatorProvider = ({ children, enabled = false, ...rest } const { data } = await apolloClient.query({ query: translationQuery, variables: { - input: { text, targetLanguage: scope.language }, + input: { text, targetLanguage: String(scope.language) }, }, }); return data.azureAiTranslate; @@ -32,7 +32,7 @@ export const AzureAiTranslatorProvider = ({ children, enabled = false, ...rest } const { data } = await apolloClient.query<{ azureAiTranslateBatch: string[] }>({ query: batchTranslationQuery, variables: { - input: { texts, targetLanguage: scope.language }, + input: { texts, targetLanguage: String(scope.language) }, }, fetchPolicy: "no-cache", }); diff --git a/packages/admin/cms-admin/src/warnings/WarningsGrid.tsx b/packages/admin/cms-admin/src/warnings/WarningsGrid.tsx index 26a718d5615..4db24457faf 100644 --- a/packages/admin/cms-admin/src/warnings/WarningsGrid.tsx +++ b/packages/admin/cms-admin/src/warnings/WarningsGrid.tsx @@ -94,7 +94,7 @@ export function WarningsGrid() { if (item.label && item.label[key]) { label.push(item.label[key]); } else if (value) { - label.push(capitalCase(value)); + label.push(capitalCase(String(value))); } } diff --git a/packages/api/brevo-api/src/blacklisted-contacts/entity/blacklisted-contacts.entity.factory.ts b/packages/api/brevo-api/src/blacklisted-contacts/entity/blacklisted-contacts.entity.factory.ts index 5aa08d57c89..114bb3fc9c6 100644 --- a/packages/api/brevo-api/src/blacklisted-contacts/entity/blacklisted-contacts.entity.factory.ts +++ b/packages/api/brevo-api/src/blacklisted-contacts/entity/blacklisted-contacts.entity.factory.ts @@ -44,7 +44,7 @@ export function createBlacklistedContactsEntity({ Scope }: { Scope: Type Scope) @Field(() => Scope) - scope: typeof Scope; + scope: EmailCampaignScopeInterface; } return BrevoBlacklistedContacts; diff --git a/packages/api/brevo-api/src/brevo-config/brevo-config.resolver.ts b/packages/api/brevo-api/src/brevo-config/brevo-config.resolver.ts index 090953935d3..16ab6399741 100644 --- a/packages/api/brevo-api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/brevo-api/src/brevo-config/brevo-config.resolver.ts @@ -73,7 +73,7 @@ export function createBrevoConfigResolver({ @Query(() => [BrevoApiSender], { nullable: true }) async brevoSenders( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, ): Promise | undefined> { const senders = await this.brevoSenderApiService.getSenders(scope); return senders; @@ -83,7 +83,7 @@ export function createBrevoConfigResolver({ @Query(() => [BrevoApiEmailTemplate], { nullable: true }) async brevoDoubleOptInTemplates( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, ): Promise | undefined> { const { templates } = await this.brevoTransactionalEmailsApiService.getEmailTemplates(scope); const doubleOptInTemplates = templates?.filter((template) => template.tag === "optin" && template.isActive); @@ -94,7 +94,7 @@ export function createBrevoConfigResolver({ @RequiredPermission("brevoNewsletter") async isBrevoConfigDefined( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, ): Promise { const brevoConfig = await this.repository.findOne({ scope }); return !!brevoConfig; @@ -103,7 +103,7 @@ export function createBrevoConfigResolver({ @Query(() => BrevoConfig, { nullable: true }) async brevoConfig( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, ): Promise { const brevoConfig = await this.repository.findOne({ scope }); return brevoConfig; @@ -112,7 +112,7 @@ export function createBrevoConfigResolver({ @Mutation(() => BrevoConfig) async createBrevoConfig( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, @Args("input", { type: () => BrevoConfigInput }) input: BrevoConfigInput, ): Promise { if (!(await this.brevoIsValidSender({ email: input.senderMail, name: input.senderName, scope }))) { diff --git a/packages/api/brevo-api/src/brevo-config/entities/brevo-config-entity.factory.ts b/packages/api/brevo-api/src/brevo-config/entities/brevo-config-entity.factory.ts index 28207a2c93f..a2fd4eac583 100644 --- a/packages/api/brevo-api/src/brevo-config/entities/brevo-config-entity.factory.ts +++ b/packages/api/brevo-api/src/brevo-config/entities/brevo-config-entity.factory.ts @@ -72,7 +72,7 @@ export class BrevoConfigEntityFactory { @Embedded(() => Scope) @Field(() => Scope) - scope: typeof Scope; + scope: EmailCampaignScopeInterface; } return BrevoConfig; diff --git a/packages/api/brevo-api/src/brevo-contact/brevo-contact-import.console.ts b/packages/api/brevo-api/src/brevo-contact/brevo-contact-import.console.ts index b836b589f5e..a3018e2ab9d 100644 --- a/packages/api/brevo-api/src/brevo-contact/brevo-contact-import.console.ts +++ b/packages/api/brevo-api/src/brevo-contact/brevo-contact-import.console.ts @@ -15,7 +15,7 @@ import { EmailCampaignScopeInterface } from "../types"; interface CommandOptions { path: string; - scope: Type; + scope: EmailCampaignScopeInterface; targetGroupIds: string[]; sendDoubleOptIn: boolean; } @@ -59,8 +59,8 @@ export function createBrevoContactImportConsole({ Scope }: { Scope: Type { - const parsedScope = JSON.parse(scope) as typeof Scope; + parseScope(scope: string): EmailCampaignScopeInterface { + const parsedScope = JSON.parse(scope) as EmailCampaignScopeInterface; const validateErrors = validateSync(parsedScope); if (validateErrors.length) { @@ -111,7 +111,7 @@ export function createBrevoContactImportConsole({ Scope }: { Scope: Type): Promise { + async validateRedirectUrl(urlToValidate: string, scope: EmailCampaignScopeInterface): Promise { const configForScope = await this.brevoConfigRepository.findOneOrFail({ scope }); if (!configForScope) { diff --git a/packages/api/brevo-api/src/brevo-contact/brevo-contact.console.ts b/packages/api/brevo-api/src/brevo-contact/brevo-contact.console.ts index eb271da4e5f..9f029e6caf4 100644 --- a/packages/api/brevo-api/src/brevo-contact/brevo-contact.console.ts +++ b/packages/api/brevo-api/src/brevo-contact/brevo-contact.console.ts @@ -30,14 +30,12 @@ export class DeleteUnsubscribedBrevoContactsConsole extends CommandRunner { let offset = 0; do { - const contacts = await this.brevoApiContactsService.findContacts(limit, offset, { - scope: targetGroup.scope, - }); + const contacts = await this.brevoApiContactsService.findContacts(limit, offset, targetGroup.scope); const blacklistedContacts = contacts.filter((contact) => contact.emailBlacklisted === true); if (blacklistedContacts.length > 0) { - await this.brevoApiContactsService.deleteContacts(blacklistedContacts, { scope: targetGroup.scope }); + await this.brevoApiContactsService.deleteContacts(blacklistedContacts, targetGroup.scope); } hasMoreContacts = !(contacts.length < limit); diff --git a/packages/api/brevo-api/src/brevo-contact/brevo-contact.resolver.ts b/packages/api/brevo-api/src/brevo-contact/brevo-contact.resolver.ts index 646cd584747..0978aff97f0 100644 --- a/packages/api/brevo-api/src/brevo-contact/brevo-contact.resolver.ts +++ b/packages/api/brevo-api/src/brevo-contact/brevo-contact.resolver.ts @@ -61,7 +61,7 @@ export function createBrevoContactResolver({ @AffectedEntity(BrevoContact) async brevoContact( @Args("id", { type: () => Int }) id: number, - @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: EmailCampaignScopeInterface, ): Promise { const brevoContact = await this.brevoContactsApiService.findContact(id, scope); @@ -158,7 +158,7 @@ export function createBrevoContactResolver({ @AffectedEntity(BrevoContact) async updateBrevoContact( @Args("id", { type: () => Int }) id: number, - @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: EmailCampaignScopeInterface, @Args("input", { type: () => BrevoContactUpdateInput }) input: BrevoContactUpdateInputInterface, ): Promise { // update attributes of contact before (un)assigning to target groups because they cannot be correctly validated for completeness @@ -206,7 +206,7 @@ export function createBrevoContactResolver({ @Mutation(() => SubscribeResponse) @RequiredPermission(["brevoNewsletter"], { skipScopeCheck: true }) async createBrevoContact( - @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: EmailCampaignScopeInterface, @Args("input", { type: () => BrevoContactInput }) input: BrevoContactInputInterface, @GetCurrentUser() user: CurrentUser, @@ -232,7 +232,7 @@ export function createBrevoContactResolver({ @Mutation(() => SubscribeResponse) @RequiredPermission(["brevoNewsletter"], { skipScopeCheck: true }) async createBrevoTestContact( - @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: EmailCampaignScopeInterface, @Args("input", { type: () => BrevoTestContactInput }) input: BrevoContactInputInterface, ): Promise { @@ -278,7 +278,7 @@ export function createBrevoContactResolver({ @AffectedEntity(BrevoContact) async deleteBrevoContact( @Args("id", { type: () => Int }) id: number, - @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: EmailCampaignScopeInterface, ): Promise { const contact = await this.brevoContactsApiService.findContact(id, scope); if (!contact) { @@ -312,7 +312,7 @@ export function createBrevoContactResolver({ @AffectedEntity(BrevoContact) async deleteBrevoTestContact( @Args("id", { type: () => Int }) id: number, - @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: typeof Scope, + @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) scope: EmailCampaignScopeInterface, ): Promise { const contact = await this.brevoContactsApiService.findContact(id, scope); if (!contact) { @@ -347,7 +347,7 @@ export function createBrevoContactResolver({ async subscribeBrevoContact( @Args("input", { type: () => BrevoContactSubscribeInput }) data: SubscribeInputInterface, @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, ): Promise { return this.brevoContactsService.subscribeBrevoContact(data, scope); } diff --git a/packages/api/brevo-api/src/brevo-contact/validator/redirect-url.validator.ts b/packages/api/brevo-api/src/brevo-contact/validator/redirect-url.validator.ts index 0c82cf22442..b29c8620a89 100644 --- a/packages/api/brevo-api/src/brevo-contact/validator/redirect-url.validator.ts +++ b/packages/api/brevo-api/src/brevo-contact/validator/redirect-url.validator.ts @@ -1,6 +1,6 @@ import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityRepository } from "@mikro-orm/postgresql"; -import { Inject, Injectable } from "@nestjs/common"; +import { Inject, Injectable, Type } from "@nestjs/common"; import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator"; import { BrevoConfigInterface } from "src/brevo-config/entities/brevo-config-entity.factory"; import { EmailCampaignScopeInterface } from "src/types"; @@ -8,7 +8,7 @@ import { EmailCampaignScopeInterface } from "src/types"; import { BrevoModuleConfig } from "../../config/brevo-module.config"; import { BREVO_MODULE_CONFIG } from "../../config/brevo-module.constants"; -export const IsValidRedirectURL = (scope: EmailCampaignScopeInterface, validationOptions?: ValidationOptions) => { +export const IsValidRedirectURL = (scope: Type, validationOptions?: ValidationOptions) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (object: Record, propertyName: string): void => { registerDecorator({ diff --git a/packages/api/brevo-api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts b/packages/api/brevo-api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts index 11670bdd29a..95bcf4c7446 100644 --- a/packages/api/brevo-api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts +++ b/packages/api/brevo-api/src/brevo-email-import-log/entity/brevo-email-import-log.entity.factory.ts @@ -56,7 +56,7 @@ export function createBrevoEmailImportLogEntity({ Scope }: { Scope: Type Scope) @Field(() => Scope) - scope: typeof Scope; + scope: EmailCampaignScopeInterface; @Property({ columnType: "uuid" }) @IsUndefinable() diff --git a/packages/api/brevo-api/src/email-campaign/email-campaign.resolver.ts b/packages/api/brevo-api/src/email-campaign/email-campaign.resolver.ts index bab188ffde1..ed239fde430 100644 --- a/packages/api/brevo-api/src/email-campaign/email-campaign.resolver.ts +++ b/packages/api/brevo-api/src/email-campaign/email-campaign.resolver.ts @@ -91,7 +91,7 @@ export function createEmailCampaignsResolver({ @Mutation(() => BrevoEmailCampaign) async createBrevoEmailCampaign( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, @Args("input", { type: () => EmailCampaignInput }, new DynamicDtoValidationPipe(EmailCampaignInput)) input: EmailCampaignInputInterface, ): Promise { const campaign = this.repository.create({ diff --git a/packages/api/brevo-api/src/email-campaign/entities/email-campaign-entity.factory.ts b/packages/api/brevo-api/src/email-campaign/entities/email-campaign-entity.factory.ts index 11343ca60b5..f27efda58cd 100644 --- a/packages/api/brevo-api/src/email-campaign/entities/email-campaign-entity.factory.ts +++ b/packages/api/brevo-api/src/email-campaign/entities/email-campaign-entity.factory.ts @@ -87,7 +87,7 @@ export function createEmailCampaignEntity({ @Embedded(() => Scope) @Field(() => Scope) - scope: typeof Scope; + scope: EmailCampaignScopeInterface; } return BrevoEmailCampaign; diff --git a/packages/api/brevo-api/src/target-group/entity/target-group-entity.factory.ts b/packages/api/brevo-api/src/target-group/entity/target-group-entity.factory.ts index d4caecd955c..c603e7b6cc5 100644 --- a/packages/api/brevo-api/src/target-group/entity/target-group-entity.factory.ts +++ b/packages/api/brevo-api/src/target-group/entity/target-group-entity.factory.ts @@ -71,7 +71,7 @@ export function createTargetGroupEntity({ @Embedded(() => Scope) @Field(() => Scope) - scope: typeof Scope; + scope: EmailCampaignScopeInterface; @Property({ columnType: "int", nullable: true }) @Field(() => Int, { nullable: true }) diff --git a/packages/api/brevo-api/src/target-group/target-group.resolver.ts b/packages/api/brevo-api/src/target-group/target-group.resolver.ts index 1d81147567a..5fe1d718993 100644 --- a/packages/api/brevo-api/src/target-group/target-group.resolver.ts +++ b/packages/api/brevo-api/src/target-group/target-group.resolver.ts @@ -85,7 +85,7 @@ export function createTargetGroupsResolver({ @Mutation(() => BrevoTargetGroup) async createBrevoTargetGroup( @Args("scope", { type: () => Scope }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: EmailCampaignScopeInterface, @Args("input", { type: () => TargetGroupInput }, new DynamicDtoValidationPipe(TargetGroupInput)) input: TargetGroupInputInterface, ): Promise { const brevoId = await this.brevoApiContactsService.createBrevoContactList(input.title, scope); diff --git a/packages/api/brevo-api/src/types.ts b/packages/api/brevo-api/src/types.ts index 2b078912476..ef922d6f18b 100644 --- a/packages/api/brevo-api/src/types.ts +++ b/packages/api/brevo-api/src/types.ts @@ -4,5 +4,4 @@ export type BrevoContactAttributesInterface = Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type BrevoContactFilterAttributesInterface = Record | undefined>; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type EmailCampaignScopeInterface = Record; +export type EmailCampaignScopeInterface = Record; diff --git a/packages/api/cms-api/src/dam/files/dto/empty-dam-scope.ts b/packages/api/cms-api/src/dam/files/dto/empty-dam-scope.ts index be3bf6813d8..601acc766cb 100644 --- a/packages/api/cms-api/src/dam/files/dto/empty-dam-scope.ts +++ b/packages/api/cms-api/src/dam/files/dto/empty-dam-scope.ts @@ -6,7 +6,7 @@ import { DamScopeInterface } from "../../types"; @ObjectType("DamScope") @InputType("DamScopeInput") export class EmptyDamScope implements DamScopeInterface { - [key: string]: unknown; + [key: string]: string | number | null | undefined; // empty scope @Field({ nullable: true }) @IsUndefinable() diff --git a/packages/api/cms-api/src/dam/files/dto/find-copies-of-file-in-scope.args.ts b/packages/api/cms-api/src/dam/files/dto/find-copies-of-file-in-scope.args.ts index 5ecb7cad286..30ceff19e1e 100644 --- a/packages/api/cms-api/src/dam/files/dto/find-copies-of-file-in-scope.args.ts +++ b/packages/api/cms-api/src/dam/files/dto/find-copies-of-file-in-scope.args.ts @@ -21,7 +21,7 @@ export function createFindCopiesOfFileInScopeArgs({ Scope, hasNonEmptyScope }: { @Field(() => Scope, { defaultValue: hasNonEmptyScope ? undefined : {} }) @ValidateNested() - scope: typeof Scope; + scope: DamScopeInterface; @Field(() => ImageCropAreaInput, { nullable: true }) @ValidateNested() diff --git a/packages/api/cms-api/src/dam/files/entities/file.entity.ts b/packages/api/cms-api/src/dam/files/entities/file.entity.ts index a4d73d9923d..ae3584ddec4 100644 --- a/packages/api/cms-api/src/dam/files/entities/file.entity.ts +++ b/packages/api/cms-api/src/dam/files/entities/file.entity.ts @@ -180,7 +180,7 @@ export function createFileEntity({ Scope, Folder }: { Scope?: Type Scope) @Field(() => Scope) - scope: typeof Scope; + scope: DamScopeInterface; } return DamFile; } else { diff --git a/packages/api/cms-api/src/dam/files/entities/folder.entity.ts b/packages/api/cms-api/src/dam/files/entities/folder.entity.ts index 418a7f182a6..896824a8330 100644 --- a/packages/api/cms-api/src/dam/files/entities/folder.entity.ts +++ b/packages/api/cms-api/src/dam/files/entities/folder.entity.ts @@ -119,7 +119,7 @@ export function createFolderEntity({ Scope }: { Scope?: Type class DamFolder extends FolderBase { @Embedded(() => Scope) @Field(() => Scope) - scope: typeof Scope; + scope: DamScopeInterface; @Field(() => DamFolder, { nullable: true }) parent: DamFolder | null; diff --git a/packages/api/cms-api/src/dam/files/files.resolver.ts b/packages/api/cms-api/src/dam/files/files.resolver.ts index dc0a54eefcb..aa22c63b766 100644 --- a/packages/api/cms-api/src/dam/files/files.resolver.ts +++ b/packages/api/cms-api/src/dam/files/files.resolver.ts @@ -188,7 +188,7 @@ export function createFilesResolver({ @Query(() => Boolean) async damIsFilenameOccupied( @Args("filename") filename: string, - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: DamScopeInterface, @Args("folderId", { nullable: true }) folderId?: string, ): Promise { const extension = extname(filename); @@ -203,7 +203,7 @@ export function createFilesResolver({ @Query(() => [FilenameResponse]) async damAreFilenamesOccupied( @Args("filenames", { type: () => [FilenameInput] }) filenames: Array, - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: DamScopeInterface, ): Promise> { const response: Array = []; diff --git a/packages/api/cms-api/src/dam/files/folders.resolver.ts b/packages/api/cms-api/src/dam/files/folders.resolver.ts index 1f8be7fcff0..e7fd4f55c68 100644 --- a/packages/api/cms-api/src/dam/files/folders.resolver.ts +++ b/packages/api/cms-api/src/dam/files/folders.resolver.ts @@ -43,7 +43,7 @@ export function createFoldersResolver({ @Query(() => [Folder]) async damFoldersFlat( - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: DamScopeInterface, ): Promise { return this.foldersService.findAllFlat(nonEmptyScopeOrNothing(scope)); } @@ -75,7 +75,7 @@ export function createFoldersResolver({ @SkipBuild() async createDamFolder( @Args("input", { type: () => CreateFolderInput }) input: CreateFolderInput, - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: DamScopeInterface, ): Promise { return this.foldersService.create(input, nonEmptyScopeOrNothing(scope)); } @@ -95,7 +95,7 @@ export function createFoldersResolver({ async moveDamFolders( @Args("folderIds", { type: () => [ID] }) folderIds: string[], @Args("targetFolderId", { type: () => ID, nullable: true }) targetFolderId: string, - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: DamScopeInterface, ): Promise { return this.foldersService.moveBatch({ folderIds, targetFolderId }, nonEmptyScopeOrNothing(scope)); } diff --git a/packages/api/cms-api/src/dam/types.ts b/packages/api/cms-api/src/dam/types.ts index 602f2e977af..7ac739a3c6f 100644 --- a/packages/api/cms-api/src/dam/types.ts +++ b/packages/api/cms-api/src/dam/types.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type DamScopeInterface = Record; +type DamScopeInterface = Record; export type { DamScopeInterface }; diff --git a/packages/api/cms-api/src/page-tree/blocks/internal-link-block-transformer.service.ts b/packages/api/cms-api/src/page-tree/blocks/internal-link-block-transformer.service.ts index bca5f917c3c..70b31c1ff95 100644 --- a/packages/api/cms-api/src/page-tree/blocks/internal-link-block-transformer.service.ts +++ b/packages/api/cms-api/src/page-tree/blocks/internal-link-block-transformer.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import { BlockTransformerServiceInterface } from "../../blocks/block"; import { PageTreeReadApiService } from "../page-tree-read-api.service"; +import type { ScopeInterface } from "../types"; import type { InternalLinkBlockData } from "./internal-link.block"; type TransformResponse = { @@ -10,7 +11,7 @@ type TransformResponse = { name: string; path: string; documentType: string; - scope: Record | null; + scope: ScopeInterface | null; } | null; targetPageAnchor?: string; }; diff --git a/packages/api/cms-api/src/page-tree/dto/empty-page-tree-node-scope.ts b/packages/api/cms-api/src/page-tree/dto/empty-page-tree-node-scope.ts index 2d9b52d48d8..abd666f4728 100644 --- a/packages/api/cms-api/src/page-tree/dto/empty-page-tree-node-scope.ts +++ b/packages/api/cms-api/src/page-tree/dto/empty-page-tree-node-scope.ts @@ -5,7 +5,7 @@ import { ScopeInterface } from "../types"; @ObjectType("PageTreeNodeScope") @InputType("PageTreeNodeScopeInput") export class EmptyPageTreeNodeScope implements ScopeInterface { - [key: string]: unknown; + [key: string]: string | number | null | undefined; // empty scope @Field({ nullable: true }) thisScopeHasNoFields____?: string; // just anything so this class has at least one field and can be interpreted as a gql-object/input type diff --git a/packages/api/cms-api/src/page-tree/types.ts b/packages/api/cms-api/src/page-tree/types.ts index 03f8a6a5fe3..c56fcb1e730 100644 --- a/packages/api/cms-api/src/page-tree/types.ts +++ b/packages/api/cms-api/src/page-tree/types.ts @@ -3,8 +3,7 @@ import { registerEnumType } from "@nestjs/graphql"; import type { PageTreeNodeBaseCreateInput, PageTreeNodeBaseUpdateInput } from "./dto/page-tree-node.input"; import type { PageTreeNodeBase } from "./entities/page-tree-node-base.entity"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type ScopeInterface = Record; //@TODO: move to general scope (other modules (redirect, dam) need this too) +export type ScopeInterface = Record; //@TODO: move to general scope (other modules (redirect, dam) need this too) export type PageTreeNodeCategory = string; export type PageTreeNodeInterface = PageTreeNodeBase & { scope?: ScopeInterface }; export type PageTreeNodeCreateInputInterface = PageTreeNodeBaseCreateInput; diff --git a/packages/api/cms-api/src/redirects/dto/empty-redirect-scope.ts b/packages/api/cms-api/src/redirects/dto/empty-redirect-scope.ts index 6b4ab54e4d3..3774febdbeb 100644 --- a/packages/api/cms-api/src/redirects/dto/empty-redirect-scope.ts +++ b/packages/api/cms-api/src/redirects/dto/empty-redirect-scope.ts @@ -5,7 +5,7 @@ import { RedirectScopeInterface } from "../types"; @ObjectType("RedirectScope") @InputType("RedirectScopeInput") export class EmptyRedirectScope implements RedirectScopeInterface { - [key: string]: unknown; + [key: string]: string | number | null | undefined; // empty scope @Field({ nullable: true }) thisScopeHasNoFields____?: string; // just anything so this class has at least one field and can be interpreted as a gql-object/input type diff --git a/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts b/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts index 1476d4d8429..fffae410a3f 100644 --- a/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts +++ b/packages/api/cms-api/src/redirects/entities/redirect-entity.factory.ts @@ -97,7 +97,7 @@ export class RedirectEntityFactory { class Redirect extends RedirectBase { @Embedded(() => RedirectScope) @Field(() => RedirectScope) - scope: typeof RedirectScope; + scope: RedirectScopeInterface; } return Redirect; } else { diff --git a/packages/api/cms-api/src/redirects/redirects.resolver.ts b/packages/api/cms-api/src/redirects/redirects.resolver.ts index 03743e98da6..2b4e1433782 100644 --- a/packages/api/cms-api/src/redirects/redirects.resolver.ts +++ b/packages/api/cms-api/src/redirects/redirects.resolver.ts @@ -184,7 +184,7 @@ export function createRedirectsResolver({ @Query(() => Redirect, { nullable: true }) async redirectBySource( - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: RedirectScopeInterface, @Args("source", { type: () => String }) source: string, @Args("sourceType", { type: () => RedirectSourceType }) sourceType: RedirectSourceType, ): Promise { @@ -198,7 +198,7 @@ export function createRedirectsResolver({ @Query(() => Boolean) async redirectSourceAvailable( - @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: typeof Scope, + @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }) scope: RedirectScopeInterface, @Args("source", { type: () => String }) source: string, ): Promise { return this.redirectService.isRedirectSourceAvailable(source, nonEmptyScopeOrNothing(scope)); @@ -207,7 +207,7 @@ export function createRedirectsResolver({ @Mutation(() => Redirect) async createRedirect( @Args("scope", { type: () => Scope, defaultValue: hasNonEmptyScope ? undefined : {} }, new DynamicDtoValidationPipe(Scope)) - scope: typeof Scope, + scope: RedirectScopeInterface, @Args("input", { type: () => RedirectInput }, new DynamicDtoValidationPipe(RedirectInput)) input: RedirectInputInterface, ): Promise { if (!(await this.redirectService.isRedirectSourceAvailable(input.source, nonEmptyScopeOrNothing(scope)))) { diff --git a/packages/api/cms-api/src/redirects/types.ts b/packages/api/cms-api/src/redirects/types.ts index 8c56f714f1a..7aa4e03dde0 100644 --- a/packages/api/cms-api/src/redirects/types.ts +++ b/packages/api/cms-api/src/redirects/types.ts @@ -1,2 +1 @@ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type RedirectScopeInterface = Record; //@TODO: move to general scope (other modules (page-tree, dam) need this too) +export type RedirectScopeInterface = Record; //@TODO: move to general scope (other modules (page-tree, dam) need this too) diff --git a/packages/site/site-nextjs/src/sitePreview/previewUtils.ts b/packages/site/site-nextjs/src/sitePreview/previewUtils.ts index e0e0f2b22cc..2379ae888d9 100644 --- a/packages/site/site-nextjs/src/sitePreview/previewUtils.ts +++ b/packages/site/site-nextjs/src/sitePreview/previewUtils.ts @@ -5,8 +5,7 @@ import { cookies, draftMode, headers as getHeaders } from "next/headers"; // Return type of previewParams function type PreviewParams = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - scope: Record; + scope: Record; previewData?: PreviewData; }; From 46b92f46725d5fd09ef1fa9d019dd66fb8de5fe4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 09:23:33 +0000 Subject: [PATCH 2/4] Demo: Narrow the scope classes' index signature to string All scope dimensions in Demo are strings, and an index signature only has to be assignable to the scope interface's, not identical to it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnYJ1LdUKw9FxdKdFAdWAr --- .changeset/narrow-content-scope-value-types.md | 5 +++-- .../brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts | 2 +- .../src/brevo/email-campaign/email-campaign-content-scope.ts | 2 +- demo/api/src/dam/dto/dam-scope.ts | 2 +- demo/api/src/news/entities/news.entity.ts | 2 +- demo/api/src/page-tree/dto/page-tree-node-scope.ts | 2 +- demo/api/src/redirects/dto/redirect-scope.ts | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.changeset/narrow-content-scope-value-types.md b/.changeset/narrow-content-scope-value-types.md index af926241b8a..170f385bef4 100644 --- a/.changeset/narrow-content-scope-value-types.md +++ b/.changeset/narrow-content-scope-value-types.md @@ -17,14 +17,15 @@ Scope dimensions are now typed as `string | number | null | undefined`: **Migration** -TypeScript only gives classes an index signature when it is declared explicitly, so scope classes in your application need one: +TypeScript only gives classes an index signature when it is declared explicitly, so scope classes in your application need one. +It can be narrower than the interface, so a scope with only string dimensions declares `[key: string]: string`: ```ts @Embeddable() @ObjectType("PageTreeNodeScope") @InputType("PageTreeNodeScopeInput") export class PageTreeNodeScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @Property({ columnType: "text" }) @Field() diff --git a/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts b/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts index fc6ca6af131..8c713bfdf31 100644 --- a/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts +++ b/demo/api/src/brevo/brevo-contact/dto/brevo-contact-subscribe.scope.ts @@ -1,7 +1,7 @@ import { IsString, MaxLength } from "class-validator"; export class EmailContactSubscribeScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @IsString() @MaxLength(64) diff --git a/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts b/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts index 3d05564707d..ebf5ad6fc9e 100644 --- a/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts +++ b/demo/api/src/brevo/email-campaign/email-campaign-content-scope.ts @@ -6,7 +6,7 @@ import { IsString } from "class-validator"; @ObjectType() @InputType("EmailCampaignContentScopeInput") export class EmailCampaignContentScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @Property({ columnType: "text" }) @Field() diff --git a/demo/api/src/dam/dto/dam-scope.ts b/demo/api/src/dam/dto/dam-scope.ts index 01976bb7f21..93217bcdb16 100644 --- a/demo/api/src/dam/dto/dam-scope.ts +++ b/demo/api/src/dam/dto/dam-scope.ts @@ -6,7 +6,7 @@ import { IsString } from "class-validator"; @ObjectType() @InputType("DamScopeInput") export class DamScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @Property({ columnType: "text" }) @Field() diff --git a/demo/api/src/news/entities/news.entity.ts b/demo/api/src/news/entities/news.entity.ts index 72025e48d56..5322c997a99 100644 --- a/demo/api/src/news/entities/news.entity.ts +++ b/demo/api/src/news/entities/news.entity.ts @@ -51,7 +51,7 @@ registerEnumType(NewsCategory, { @ObjectType("") @InputType("NewsContentScopeInput") export class NewsContentScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @Property({ columnType: "text" }) @Field() diff --git a/demo/api/src/page-tree/dto/page-tree-node-scope.ts b/demo/api/src/page-tree/dto/page-tree-node-scope.ts index b5100fe14d6..24a704fd556 100644 --- a/demo/api/src/page-tree/dto/page-tree-node-scope.ts +++ b/demo/api/src/page-tree/dto/page-tree-node-scope.ts @@ -7,7 +7,7 @@ import { IsString } from "class-validator"; @InputType("PageTreeNodeScopeInput") // name must not be changed in the app // @TODO: disguise @ObjectType("PageTreeContentScope") and @InputType("PageTreeContentScopeInput") decorators under a custom decorator: f.i. @PageTreeNodeScope export class PageTreeNodeScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @Property({ columnType: "text" }) @Field() diff --git a/demo/api/src/redirects/dto/redirect-scope.ts b/demo/api/src/redirects/dto/redirect-scope.ts index a233afb1c82..9acfb771ce2 100644 --- a/demo/api/src/redirects/dto/redirect-scope.ts +++ b/demo/api/src/redirects/dto/redirect-scope.ts @@ -7,7 +7,7 @@ import { IsString } from "class-validator"; @InputType("RedirectScopeInput") // name must not be changed in the app // @TODO: disguise @ObjectType("RedirectScope") and @InputType("RedirectScopeInput") decorators under a custom decorator: f.i. @RedirectScope export class RedirectScope { - [key: string]: string | number | null | undefined; + [key: string]: string; @Index() // this does nothing, migration has to be created manually as the entity is in library @Property({ columnType: "text" }) From bd692f2b73cc4e0b6cf9c3c8e75232c6ec89367a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 09:26:24 +0000 Subject: [PATCH 3/4] Drop the string conversion hint from the content scope changeset A project's scope class declares its own index signature, so a scope with only string dimensions keeps string values and needs no conversion. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnYJ1LdUKw9FxdKdFAdWAr --- .changeset/narrow-content-scope-value-types.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.changeset/narrow-content-scope-value-types.md b/.changeset/narrow-content-scope-value-types.md index 170f385bef4..383b23ae3ec 100644 --- a/.changeset/narrow-content-scope-value-types.md +++ b/.changeset/narrow-content-scope-value-types.md @@ -38,13 +38,3 @@ export class PageTreeNodeScope { language: string; } ``` - -Places that used a scope value where a `string` is expected (for instance a language passed to a translation service) now need an explicit conversion: - -```ts -// Before -transformToSlug(name, scope.language); - -// After -transformToSlug(name, String(scope.language)); -``` From c7f4d19420fadd396e4433609473c6a1a7bb9b91 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 09:50:43 +0000 Subject: [PATCH 4/4] brevo-api: Widen the schema generator's empty scope index signature generate-schema.ts declares its own empty EmailCampaignScope, which still had an unknown index signature and no longer implements EmailCampaignScopeInterface. The file lives outside src/, so neither lint:tsc nor eslint covers it; only the schema generation step in CI compiles it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NnYJ1LdUKw9FxdKdFAdWAr --- packages/api/brevo-api/generate-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/brevo-api/generate-schema.ts b/packages/api/brevo-api/generate-schema.ts index 83807505bb4..5f06cd70616 100644 --- a/packages/api/brevo-api/generate-schema.ts +++ b/packages/api/brevo-api/generate-schema.ts @@ -26,7 +26,7 @@ import { BrevoPermission } from "./src"; @ObjectType("EmailCampaignContentScope") @InputType("EmailCampaignContentScopeInput") class EmailCampaignScope implements EmailCampaignScopeInterface { - [key: string]: unknown; + [key: string]: string | number | null | undefined; // empty scope @Field({ nullable: true }) thisScopeHasNoFields____?: string; // just anything so this class has at least one field and can be interpreted as a gql-object/input type