import { CONTENT_USER_DATA_ENTRY_TYPES, ContentTypes, ContentUserDataTypes, INDEPENDENT_ENTRY_TYPES } from "./graphql-schema"; /** * CollectionResponse is used to define the response of a collection of entries in the system. * This is a base type to define the metadata provided for pagination. * * @template Type The type definition for the entries in the collection. */ interface CollectionResponse { /** * The items in the collection. */ items: Type[]; /** * How many items were returned per page. */ limit: number; /** * How many items were skipped. */ skip: number; /** * How many items match the request in total. */ total: number; } type GraphQLParams = Record; /** * Options for configuring the GraphQL client. */ export interface GraphQLClientOptions { /** * The base URL of the Content Cloud GraphQL API, excluding the `/graphql` endpoint. */ baseUrl: string; /** * The access token to use for authentication. */ accessToken?: string; /** * The ID of the space to use for the GraphQL requests. */ spaceId?: string; /** * The ID of the environment to use for the GraphQL requests. */ environmentId?: string; /** * Optional fetch function to use for making requests. * If not provided, the native fetch will be used. */ fetch?: typeof fetch; } export type GraphQLSelect = { [K in keyof Type]?: NonNullable extends Array ? NonNullable extends object ? GraphQLSelect> : 1 : NonNullable extends object ? GraphQLSelect> : 1; }; export type GraphQLSelected> = { [K in keyof SelectedType]: K extends keyof Type ? Type[K] extends Array ? U extends object ? SelectedType[K] extends GraphQLSelect ? Array> : never : Type[K] : Type[K] extends object ? SelectedType[K] extends GraphQLSelect ? GraphQLSelected : never : Type[K] : never; }; function getSelectedFields(select: GraphQLSelect>): string { return Object.entries(select) .map(([key, value]) => { if (typeof value === "object" && value !== null) { return `${key} { ${getSelectedFields(value)} }`; } return key; }) .join("\n"); } /** * Base class for GraphQL clients that handles the core GraphQL functionality. * This class uses native fetch and expects a Proxy on top to handle GraphQL operations. * It is designed to be extended for specific content types and user data types. * * @protected */ class ContentCloudGraphQLClient { /** * The fetch function to use for making requests. * * @protected */ protected get fetch() { return this.options.fetch ?? ((...args: Parameters) => fetch(...args)); } /** * Create a new instance of the ContentCloudGraphQLClient. * * @param {GraphQLClientOptions} options The options to configure the client. */ constructor(private readonly options: GraphQLClientOptions) {} /** * Execute a GraphQL query or mutation. * * @param {string} query The GraphQL query string * @param {Record} variables The variables for the query * @returns {Promise} The response data */ async query>( query: string, variables: Record = {}, queryName?: string, ): Promise { let url = `${this.options.baseUrl}/graphql`; if (variables.userDataTypes) { // If userDataTypes are provided, append them to the URL as a query parameter url += `?user_data_types=${variables.userDataTypes.join(",")}`; // Remove from variables to avoid sending it in the body variables = { ...variables }; delete variables.userDataTypes; } const response = await this.fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", ...(this.options.accessToken ? { Authorization: `Bearer ${this.options.accessToken}`, } : {}), }, body: JSON.stringify({ query, variables, }), }); if (!response.ok) { throw new Error(await response.text()); } const result = await response.json(); if (result.errors) { console.error("GraphQL Errors:", result.errors); if (!result.data) { throw new Error(result.errors[0].message); } } if (!result.data) { console.error("GraphQL Response without data:", result); throw new Error("GraphQL response does not contain data."); } if (queryName) { if (!result.data[queryName]) { console.error(`GraphQL Response does not contain data for query "${queryName}":`, result.data); throw new Error(`GraphQL response does not contain data for query "${queryName}".`); } return result.data[queryName]; } return result.data; } /** * Fetch a collection of entries for a specific content type. * * @template ResponseData The type of the response data. * @template Select The type of the fields to select from the collection. * * @param {keyof ContentTypes} contentType The content type to fetch the collection for. * @param {GraphQLSelect} select The fields to select from the collection. * @param {GraphQLParams} [params] Optional parameters for the query, such as locale, skip, limit, where, search, and order. * @return {Promise} A promise that resolves to the collection response data. */ collection>, Select extends GraphQLSelect>( contentType: keyof ContentTypes, select: Select, params?: GraphQLParams, ): Promise> { const queryName = contentType.charAt(0).toLowerCase() + contentType.slice(1) + "Collection"; return this.query>( ` query ${queryName}($locale: String, $skip: Int, $limit: Int, $where: ${contentType}Filter, $search: String, $order: [${contentType}Order!]) { ${queryName}(locale: $locale, skip: $skip, limit: $limit, where: $where, search: $search, order: $order) { ${getSelectedFields(select)} } } `, params, queryName, ); } /** * Fetch a single entry for a specific content type. * * @template ResponseData The type of the response data. * @template Select The type of the fields to select from the entry. * * @param {keyof ContentTypes} contentType The content type to fetch the entry for. * @param {GraphQLSelect} select The fields to select from the entry. * @param {GraphQLParams} [params] Optional parameters for the query, such as locale, id, revisionId, uuid, customId, and slug. * Must include at least one filter parameter to identify the entry. * @return {Promise} A promise that resolves to the entry response data. */ entry, Select extends GraphQLSelect>( contentType: keyof ContentTypes, select: Select, params?: GraphQLParams, ): Promise> { const queryName = contentType.charAt(0).toLowerCase() + contentType.slice(1); return this.query>( ` query ${queryName}($locale: String, $id: String, $revisionId: String, $uuid: String, $customId: String, $slug: String) { ${queryName}(locale: $locale, id: $id, revisionId: $revisionId, uuid: $uuid, customId: $customId, slug: $slug) { ${getSelectedFields(select)} } } `, params, queryName, ); } /** * Set user data for a specific content entry and user data type. * * @template ContentType The type of the content user data to set. * @template Select The type of the fields to select from the user data entry. * * @param {ContentType} contentType The content type to set the user data for. * @param {GraphQLSelect} select The fields to select from the user data entry. * @param {Object} variables The variables for the mutation, including contentId and input for the mutation. * @return {Promise>} A promise that resolves to the updated user data entry. */ setContentUserData< ContentType extends keyof ContentUserDataTypes, Select extends GraphQLSelect, >( contentType: ContentType, select: Select, variables: { contentId: string; input: ContentUserDataTypes[ContentType]["Update"]; }, ): Promise> { return this.query>( ` mutation Set${contentType}($contentId: String!, $input: Set${contentType}Input!) { set${contentType}(contentId: $contentId, input: $input) { ${getSelectedFields(select)} } } `, { ...variables, userDataTypes: [contentType] }, `set${contentType}`, ); } } type EntryParams = { userDataTypes?: (keyof ContentUserDataTypes)[]; locale?: string; id?: string; revisionId?: string; uuid?: string; customId?: string; slug?: string; }; type CollectionParams = { userDataTypes?: (keyof ContentUserDataTypes)[]; locale?: string; skip?: number; limit?: number; where?: ContentTypes[K]["Filter"]; search?: string; order?: ContentTypes[K]["Order"][]; }; type QueryMethods = { [K in Uncapitalize]: >>( select: Select, params?: CollectionParams, ) => Promise, Select>>; }; type MutationMethods = { [K in `set${ContentType}`]: