diff --git a/src/SanityClient.ts b/src/SanityClient.ts index 7b2c28680..fa1c91e5c 100644 --- a/src/SanityClient.ts +++ b/src/SanityClient.ts @@ -1,14 +1,29 @@ import {lastValueFrom, Observable} from 'rxjs' -import {AssetsClient, ObservableAssetsClient} from './assets/AssetsClient' +import { + AssetsClient, + type AssetsClientType, + ObservableAssetsClient, + type ObservableAssetsClientType, +} from './assets/AssetsClient' import {defaultConfig, initConfig} from './config' import * as dataMethods from './data/dataMethods' import {_listen} from './data/listen' import {LiveClient} from './data/live' import {ObservablePatch, Patch} from './data/patch' import {ObservableTransaction, Transaction} from './data/transaction' -import {DatasetsClient, ObservableDatasetsClient} from './datasets/DatasetsClient' -import {ObservableProjectsClient, ProjectsClient} from './projects/ProjectsClient' +import { + DatasetsClient, + type DatasetsClientType, + ObservableDatasetsClient, + type ObservableDatasetsClientType, +} from './datasets/DatasetsClient' +import { + ObservableProjectsClient, + type ObservableProjectsClientType, + ProjectsClient, + type ProjectsClientType, +} from './projects/ProjectsClient' import type { Action, AllDocumentIdsMutationOptions, @@ -43,7 +58,12 @@ import type { UnfilteredResponseQueryOptions, UnfilteredResponseWithoutQuery, } from './types' -import {ObservableUsersClient, UsersClient} from './users/UsersClient' +import { + ObservableUsersClient, + type ObservableUsersClientType, + UsersClient, + type UsersClientType, +} from './users/UsersClient' export type { _listen, @@ -59,22 +79,16 @@ export type { } /** @public */ -export class ObservableSanityClient { +export class ObservableSanityClient implements ObservableSanityClientType { assets: ObservableAssetsClient datasets: ObservableDatasetsClient live: LiveClient projects: ObservableProjectsClient users: ObservableUsersClient - /** - * Private properties - */ #clientConfig: InitializedClientConfig #httpRequest: HttpRequest - /** - * Instance properties - */ listen = _listen constructor(httpRequest: HttpRequest, config: ClientConfig = defaultConfig) { @@ -176,7 +190,7 @@ export class ObservableSanityClient { Q extends QueryWithoutParams | QueryParams = QueryParams, const G extends string = string, >( - query: string, + query: G, params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, options: UnfilteredResponseQueryOptions, ): Observable>> @@ -727,7 +741,7 @@ export class ObservableSanityClient { } /** @public */ -export class SanityClient { +export class SanityClient implements SanityClientType { assets: AssetsClient datasets: DatasetsClient live: LiveClient @@ -739,15 +753,9 @@ export class SanityClient { */ observable: ObservableSanityClient - /** - * Private properties - */ #clientConfig: InitializedClientConfig #httpRequest: HttpRequest - /** - * Instance properties - */ listen = _listen constructor(httpRequest: HttpRequest, config: ClientConfig = defaultConfig) { @@ -1355,8 +1363,8 @@ export class SanityClient { * @param operations - Optional object of patch operations to initialize the patch instance with * @returns Patch instance - call `.commit()` to perform the operations defined */ - patch(documentId: PatchSelection, operations?: PatchOperations): Patch { - return new Patch(documentId, operations, this) + patch(selection: PatchSelection, operations?: PatchOperations): Patch { + return new Patch(selection, operations, this) } /** @@ -1429,3 +1437,1076 @@ export class SanityClient { return dataMethods._getDataUrl(this, operation, path) } } + +/** + * Shared base type for the `SanityClient` and `ObservableSanityClient` classes. + * TODO: refactor the Promise and Observable differences to use generics so we no longer suffer from all this duplication in TS docs + */ +interface SanityClientBase { + live: LiveClient + listen: typeof _listen + + /** + * Get a Sanity API URL for the URI provided + * + * @param uri - URI/path to build URL for + * @param canUseCdn - Whether or not to allow using the API CDN for this route + */ + getUrl(uri: string, canUseCdn?: boolean): string + + /** + * Get a Sanity API URL for the data operation and path provided + * + * @param operation - Data operation (eg `query`, `mutate`, `listen` or similar) + * @param path - Path to append after the operation + */ + getDataUrl(operation: string, path?: string): string +} + +/** + * The interface implemented by the `ObservableSanityClient` class. + * When writing code that wants to take an instance of `ObservableSanityClient` as input it's better to use this type, + * as the `ObservableSanityClient` class has private properties and thus TypeScrict will consider the type incompatible + * in cases where you might have multiple `@sanity/client` instances in your node_modules. + * @public + */ +export interface ObservableSanityClientType extends SanityClientBase { + assets: ObservableAssetsClientType + datasets: ObservableDatasetsClientType + projects: ObservableProjectsClientType + users: ObservableUsersClientType + + /** + * Clone the client - returns a new instance + */ + clone(): ObservableSanityClientType + + /** + * Returns the current client configuration + */ + config(): InitializedClientConfig + /** + * Reconfigure the client. Note that this _mutates_ the current client. + */ + config(newConfig?: Partial): this + + /** + * Clone the client with a new (partial) configuration. + * + * @param newConfig - New client configuration properties, shallowly merged with existing configuration + */ + withConfig(newConfig?: Partial): this + + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + */ + fetch< + R = Any, + Q extends QueryWithoutParams = QueryWithoutParams, + const G extends string = string, + >( + query: G, + params?: Q | QueryWithoutParams, + ): Observable> + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + * @param params - Optional query parameters + * @param options - Optional request options + */ + fetch< + R = Any, + Q extends QueryWithoutParams | QueryParams = QueryParams, + const G extends string = string, + >( + query: G, + params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, + options?: FilteredResponseQueryOptions, + ): Observable> + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + * @param params - Optional query parameters + * @param options - Request options + */ + fetch< + R = Any, + Q extends QueryWithoutParams | QueryParams = QueryParams, + const G extends string = string, + >( + query: G, + params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, + options: UnfilteredResponseQueryOptions, + ): Observable>> + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + * @param params - Optional query parameters + * @param options - Request options + */ + fetch< + R = Any, + Q extends QueryWithoutParams | QueryParams = QueryParams, + const G extends string = string, + >( + query: G, + params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, + options: UnfilteredResponseWithoutQuery, + ): Observable>> + + /** + * Fetch a single document with the given ID. + * + * @param id - Document ID to fetch + * @param options - Request options + */ + getDocument = Record>( + id: string, + options?: {tag?: string}, + ): Observable | undefined> + + /** + * Fetch multiple documents in one request. + * Should be used sparingly - performing a query is usually a better option. + * The order/position of documents is preserved based on the original array of IDs. + * If any of the documents are missing, they will be replaced by a `null` entry in the returned array + * + * @param ids - Document IDs to fetch + * @param options - Request options + */ + getDocuments = Record>( + ids: string[], + options?: {tag?: string}, + ): Observable<(SanityDocument | null)[]> + + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns an observable that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: FirstDocumentMutationOptions, + ): Observable> + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns an observable that resolves to an array containing the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: AllDocumentsMutationOptions, + ): Observable[]> + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns an observable that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: FirstDocumentIdMutationOptions, + ): Observable + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns an observable that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: AllDocumentIdsMutationOptions, + ): Observable + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns an observable that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options?: BaseMutationOptions, + ): Observable> + + /** + * Create a document if no document with the same ID already exists. + * Returns an observable that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentMutationOptions, + ): Observable> + /** + * Create a document if no document with the same ID already exists. + * Returns an observable that resolves to an array containing the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentsMutationOptions, + ): Observable[]> + /** + * Create a document if no document with the same ID already exists. + * Returns an observable that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentIdMutationOptions, + ): Observable + /** + * Create a document if no document with the same ID already exists. + * Returns an observable that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentIdsMutationOptions, + ): Observable + /** + * Create a document if no document with the same ID already exists. + * Returns an observable that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options?: BaseMutationOptions, + ): Observable> + + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns an observable that resolves to the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentMutationOptions, + ): Observable> + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns an observable that resolves to an array containing the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentsMutationOptions, + ): Observable[]> + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns an observable that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentIdMutationOptions, + ): Observable + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns an observable that resolves to a mutation result object containing the created document ID. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentIdsMutationOptions, + ): Observable + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns an observable that resolves to the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options?: BaseMutationOptions, + ): Observable> + + /** + * Deletes a document with the given document ID. + * Returns an observable that resolves to the deleted document. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete = Record>( + id: string, + options: FirstDocumentMutationOptions, + ): Observable> + /** + * Deletes a document with the given document ID. + * Returns an observable that resolves to an array containing the deleted document. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete = Record>( + id: string, + options: AllDocumentsMutationOptions, + ): Observable[]> + /** + * Deletes a document with the given document ID. + * Returns an observable that resolves to a mutation result object containing the deleted document ID. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete(id: string, options: FirstDocumentIdMutationOptions): Observable + /** + * Deletes a document with the given document ID. + * Returns an observable that resolves to a mutation result object containing the deleted document ID. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete(id: string, options: AllDocumentIdsMutationOptions): Observable + /** + * Deletes a document with the given document ID. + * Returns an observable that resolves to the deleted document. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete = Record>( + id: string, + options?: BaseMutationOptions, + ): Observable> + /** + * Deletes one or more documents matching the given query or document ID. + * Returns an observable that resolves to first deleted document. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete = Record>( + selection: MutationSelection, + options: FirstDocumentMutationOptions, + ): Observable> + /** + * Deletes one or more documents matching the given query or document ID. + * Returns an observable that resolves to an array containing the deleted documents. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete = Record>( + selection: MutationSelection, + options: AllDocumentsMutationOptions, + ): Observable[]> + /** + * Deletes one or more documents matching the given query or document ID. + * Returns an observable that resolves to a mutation result object containing the ID of the first deleted document. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete( + selection: MutationSelection, + options: FirstDocumentIdMutationOptions, + ): Observable + /** + * Deletes one or more documents matching the given query or document ID. + * Returns an observable that resolves to a mutation result object containing the document IDs that were deleted. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete( + selection: MutationSelection, + options: AllDocumentIdsMutationOptions, + ): Observable + /** + * Deletes one or more documents matching the given query or document ID. + * Returns an observable that resolves to first deleted document. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete = Record>( + selection: MutationSelection, + options?: BaseMutationOptions, + ): Observable> + + /** + * Perform mutation operations against the configured dataset + * Returns an observable that resolves to the first mutated document. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | ObservablePatch | ObservableTransaction, + options: FirstDocumentMutationOptions, + ): Observable> + /** + * Perform mutation operations against the configured dataset. + * Returns an observable that resolves to an array of the mutated documents. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | ObservablePatch | ObservableTransaction, + options: AllDocumentsMutationOptions, + ): Observable[]> + /** + * Perform mutation operations against the configured dataset + * Returns an observable that resolves to a mutation result object containing the document ID of the first mutated document. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | ObservablePatch | ObservableTransaction, + options: FirstDocumentIdMutationOptions, + ): Observable + /** + * Perform mutation operations against the configured dataset + * Returns an observable that resolves to a mutation result object containing the mutated document IDs. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | ObservablePatch | ObservableTransaction, + options: AllDocumentIdsMutationOptions, + ): Observable + /** + * Perform mutation operations against the configured dataset + * Returns an observable that resolves to the first mutated document. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | ObservablePatch | ObservableTransaction, + options?: BaseMutationOptions, + ): Observable> + + /** + * Create a new buildable patch of operations to perform + * + * @param documentId - Document ID to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(documentId: string, operations?: PatchOperations): ObservablePatch + + /** + * Create a new buildable patch of operations to perform + * + * @param documentIds - Array of document IDs to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(documentIds: string[], operations?: PatchOperations): ObservablePatch + + /** + * Create a new buildable patch of operations to perform + * + * @param selection - An object with `query` and optional `params`, defining which document(s) to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(selection: MutationSelection, operations?: PatchOperations): ObservablePatch + + /** + * Create a new buildable patch of operations to perform + * + * @param selection - Document ID, an array of document IDs, or an object with `query` and optional `params`, defining which document(s) to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(selection: PatchSelection, operations?: PatchOperations): ObservablePatch + + /** + * Create a new transaction of mutations + * + * @param operations - Optional array of mutation operations to initialize the transaction instance with + */ + transaction = Record>( + operations?: Mutation[], + ): ObservableTransaction + + /** + * Perform action operations against the configured dataset + * + * @param operations - Action operation(s) to execute + * @param options - Action options + */ + action( + operations: Action | Action[], + options?: BaseActionOptions, + ): Observable + + /** + * Perform an HTTP request against the Sanity API + * + * @param options - Request options + */ + request(options: RawRequestOptions): Observable +} + +/** + * The interface implemented by the `SanityClient` class. + * When writing code that wants to take an instance of `SanityClient` as input it's better to use this type, + * as the `SanityClient` class has private properties and thus TypeScrict will consider the type incompatible + * in cases where you might have multiple `@sanity/client` instances in your node_modules. + * @public + */ +export interface SanityClientType extends SanityClientBase { + assets: AssetsClientType + datasets: DatasetsClientType + projects: ProjectsClientType + users: UsersClientType + + /** + * Observable version of the Sanity client, with the same configuration as the promise-based one + */ + observable: ObservableSanityClientType + + /** + * Clone the client - returns a new instance + */ + clone(): SanityClientType + + /** + * Returns the current client configuration + */ + config(): InitializedClientConfig + /** + * Reconfigure the client. Note that this _mutates_ the current client. + */ + config(newConfig?: Partial): this + + /** + * Clone the client with a new (partial) configuration. + * + * @param newConfig - New client configuration properties, shallowly merged with existing configuration + */ + withConfig(newConfig?: Partial): this + + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + */ + fetch< + R = Any, + Q extends QueryWithoutParams = QueryWithoutParams, + const G extends string = string, + >( + query: G, + params?: Q | QueryWithoutParams, + ): Promise> + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + * @param params - Optional query parameters + * @param options - Optional request options + */ + fetch< + R = Any, + Q extends QueryWithoutParams | QueryParams = QueryParams, + const G extends string = string, + >( + query: G, + params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, + options?: FilteredResponseQueryOptions, + ): Promise> + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + * @param params - Optional query parameters + * @param options - Request options + */ + fetch< + R = Any, + Q extends QueryWithoutParams | QueryParams = QueryParams, + const G extends string = string, + >( + query: G, + params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, + options: UnfilteredResponseQueryOptions, + ): Promise>> + /** + * Perform a GROQ-query against the configured dataset. + * + * @param query - GROQ-query to perform + * @param params - Optional query parameters + * @param options - Request options + */ + fetch< + R = Any, + Q extends QueryWithoutParams | QueryParams = QueryParams, + const G extends string = string, + >( + query: G, + params: Q extends QueryWithoutParams ? QueryWithoutParams : Q, + options: UnfilteredResponseWithoutQuery, + ): Promise>> + + /** + * Fetch a single document with the given ID. + * + * @param id - Document ID to fetch + * @param options - Request options + */ + getDocument = Record>( + id: string, + options?: {signal?: AbortSignal; tag?: string}, + ): Promise | undefined> + + /** + * Fetch multiple documents in one request. + * Should be used sparingly - performing a query is usually a better option. + * The order/position of documents is preserved based on the original array of IDs. + * If any of the documents are missing, they will be replaced by a `null` entry in the returned array + * + * @param ids - Document IDs to fetch + * @param options - Request options + */ + getDocuments = Record>( + ids: string[], + options?: {signal?: AbortSignal; tag?: string}, + ): Promise<(SanityDocument | null)[]> + + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns a promise that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: FirstDocumentMutationOptions, + ): Promise> + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns a promise that resolves to an array containing the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: AllDocumentsMutationOptions, + ): Promise[]> + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns a promise that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: FirstDocumentIdMutationOptions, + ): Promise + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns a promise that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options: AllDocumentIdsMutationOptions, + ): Promise + /** + * Create a document. Requires a `_type` property. If no `_id` is provided, it will be generated by the database. + * Returns a promise that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + create = Record>( + document: SanityDocumentStub, + options?: BaseMutationOptions, + ): Promise> + + /** + * Create a document if no document with the same ID already exists. + * Returns a promise that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentMutationOptions, + ): Promise> + /** + * Create a document if no document with the same ID already exists. + * Returns a promise that resolves to an array containing the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentsMutationOptions, + ): Promise[]> + /** + * Create a document if no document with the same ID already exists. + * Returns a promise that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentIdMutationOptions, + ): Promise + /** + * Create a document if no document with the same ID already exists. + * Returns a promise that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentIdsMutationOptions, + ): Promise + /** + * Create a document if no document with the same ID already exists. + * Returns a promise that resolves to the created document. + * + * @param document - Document to create + * @param options - Mutation options + */ + createIfNotExists = Record>( + document: IdentifiedSanityDocumentStub, + options?: BaseMutationOptions, + ): Promise> + + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns a promise that resolves to the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentMutationOptions, + ): Promise> + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns a promise that resolves to an array containing the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentsMutationOptions, + ): Promise[]> + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns a promise that resolves to a mutation result object containing the ID of the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: FirstDocumentIdMutationOptions, + ): Promise + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns a promise that resolves to a mutation result object containing the created document ID. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options: AllDocumentIdsMutationOptions, + ): Promise + /** + * Create a document if it does not exist, or replace a document with the same document ID + * Returns a promise that resolves to the created document. + * + * @param document - Document to either create or replace + * @param options - Mutation options + */ + createOrReplace = Record>( + document: IdentifiedSanityDocumentStub, + options?: BaseMutationOptions, + ): Promise> + + /** + * Deletes a document with the given document ID. + * Returns a promise that resolves to the deleted document. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete = Record>( + id: string, + options: FirstDocumentMutationOptions, + ): Promise> + /** + * Deletes a document with the given document ID. + * Returns a promise that resolves to an array containing the deleted document. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete = Record>( + id: string, + options: AllDocumentsMutationOptions, + ): Promise[]> + /** + * Deletes a document with the given document ID. + * Returns a promise that resolves to a mutation result object containing the deleted document ID. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete(id: string, options: FirstDocumentIdMutationOptions): Promise + /** + * Deletes a document with the given document ID. + * Returns a promise that resolves to a mutation result object containing the deleted document ID. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete(id: string, options: AllDocumentIdsMutationOptions): Promise + /** + * Deletes a document with the given document ID. + * Returns a promise that resolves to the deleted document. + * + * @param id - Document ID to delete + * @param options - Options for the mutation + */ + delete = Record>( + id: string, + options?: BaseMutationOptions, + ): Promise> + /** + * Deletes one or more documents matching the given query or document ID. + * Returns a promise that resolves to first deleted document. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete = Record>( + selection: MutationSelection, + options: FirstDocumentMutationOptions, + ): Promise> + /** + * Deletes one or more documents matching the given query or document ID. + * Returns a promise that resolves to an array containing the deleted documents. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete = Record>( + selection: MutationSelection, + options: AllDocumentsMutationOptions, + ): Promise[]> + /** + * Deletes one or more documents matching the given query or document ID. + * Returns a promise that resolves to a mutation result object containing the ID of the first deleted document. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete( + selection: MutationSelection, + options: FirstDocumentIdMutationOptions, + ): Promise + /** + * Deletes one or more documents matching the given query or document ID. + * Returns a promise that resolves to a mutation result object containing the document IDs that were deleted. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete( + selection: MutationSelection, + options: AllDocumentIdsMutationOptions, + ): Promise + /** + * Deletes one or more documents matching the given query or document ID. + * Returns a promise that resolves to first deleted document. + * + * @param selection - An object with either an `id` or `query` key defining what to delete + * @param options - Options for the mutation + */ + delete = Record>( + selection: MutationSelection, + options?: BaseMutationOptions, + ): Promise> + + /** + * Perform mutation operations against the configured dataset + * Returns a promise that resolves to the first mutated document. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | Patch | Transaction, + options: FirstDocumentMutationOptions, + ): Promise> + /** + * Perform mutation operations against the configured dataset. + * Returns a promise that resolves to an array of the mutated documents. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | Patch | Transaction, + options: AllDocumentsMutationOptions, + ): Promise[]> + /** + * Perform mutation operations against the configured dataset + * Returns a promise that resolves to a mutation result object containing the document ID of the first mutated document. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | Patch | Transaction, + options: FirstDocumentIdMutationOptions, + ): Promise + /** + * Perform mutation operations against the configured dataset + * Returns a promise that resolves to a mutation result object containing the mutated document IDs. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate>( + operations: Mutation[] | Patch | Transaction, + options: AllDocumentIdsMutationOptions, + ): Promise + /** + * Perform mutation operations against the configured dataset + * Returns a promise that resolves to the first mutated document. + * + * @param operations - Mutation operations to execute + * @param options - Mutation options + */ + mutate = Record>( + operations: Mutation[] | Patch | Transaction, + options?: BaseMutationOptions, + ): Promise> + + /** + * Create a new buildable patch of operations to perform + * + * @param documentId - Document ID to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(documentId: string, operations?: PatchOperations): Patch + + /** + * Create a new buildable patch of operations to perform + * + * @param documentIds - Array of document IDs to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(documentIds: string[], operations?: PatchOperations): Patch + + /** + * Create a new buildable patch of operations to perform + * + * @param selection - An object with `query` and optional `params`, defining which document(s) to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(selection: MutationSelection, operations?: PatchOperations): Patch + + /** + * Create a new buildable patch of operations to perform + * + * @param selection - Document ID, an array of document IDs, or an object with `query` and optional `params`, defining which document(s) to patch + * @param operations - Optional object of patch operations to initialize the patch instance with + * @returns Patch instance - call `.commit()` to perform the operations defined + */ + patch(selection: PatchSelection, operations?: PatchOperations): Patch + + /** + * Create a new transaction of mutations + * + * @param operations - Optional array of mutation operations to initialize the transaction instance with + */ + transaction = Record>( + operations?: Mutation[], + ): Transaction + + /** + * Perform action operations against the configured dataset + * Returns a promise that resolves to the transaction result + * + * @param operations - Action operation(s) to execute + * @param options - Action options + */ + action( + operations: Action | Action[], + options?: BaseActionOptions, + ): Promise + + /** + * Perform a request against the Sanity API + * NOTE: Only use this for Sanity API endpoints, not for your own APIs! + * + * @param options - Request options + * @returns Promise resolving to the response body + */ + request(options: RawRequestOptions): Promise + + /** + * Perform an HTTP request a `/data` sub-endpoint + * NOTE: Considered internal, thus marked as deprecated. Use `request` instead. + * + * @deprecated - Use `request()` or your own HTTP library instead + * @param endpoint - Endpoint to hit (mutate, query etc) + * @param body - Request body + * @param options - Request options + * @internal + */ + dataRequest(endpoint: string, body: unknown, options?: BaseMutationOptions): Promise +} diff --git a/src/assets/AssetsClient.ts b/src/assets/AssetsClient.ts index 32a56883e..57e73c771 100644 --- a/src/assets/AssetsClient.ts +++ b/src/assets/AssetsClient.ts @@ -17,7 +17,91 @@ import type { import * as validators from '../validators' /** @internal */ -export class ObservableAssetsClient { +export interface ObservableAssetsClientType { + /** + * Uploads a file asset to the configured dataset + * + * @param assetType - Asset type (file) + * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream. + * @param options - Options to use for the upload + */ + upload( + assetType: 'file', + body: UploadBody, + options?: UploadClientConfig, + ): Observable> + + /** + * Uploads an image asset to the configured dataset + * + * @param assetType - Asset type (image) + * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream. + * @param options - Options to use for the upload + */ + upload( + assetType: 'image', + body: UploadBody, + options?: UploadClientConfig, + ): Observable> + + /** + * Uploads a file or an image asset to the configured dataset + * + * @param assetType - Asset type (file/image) + * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream. + * @param options - Options to use for the upload + */ + upload( + assetType: 'file' | 'image', + body: UploadBody, + options?: UploadClientConfig, + ): Observable> +} + +/** @internal */ +export interface AssetsClientType { + /** + * Uploads a file asset to the configured dataset + * + * @param assetType - Asset type (file) + * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream. + * @param options - Options to use for the upload + */ + upload( + assetType: 'file', + body: UploadBody, + options?: UploadClientConfig, + ): Promise + + /** + * Uploads an image asset to the configured dataset + * + * @param assetType - Asset type (image) + * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream. + * @param options - Options to use for the upload + */ + upload( + assetType: 'image', + body: UploadBody, + options?: UploadClientConfig, + ): Promise + + /** + * Uploads a file or an image asset to the configured dataset + * + * @param assetType - Asset type (file/image) + * @param body - Asset content - can be a browser File instance, a Blob, a Node.js Buffer instance or a Node.js ReadableStream. + * @param options - Options to use for the upload + */ + upload( + assetType: 'file' | 'image', + body: UploadBody, + options?: UploadClientConfig, + ): Promise +} + +/** @internal */ +export class ObservableAssetsClient implements ObservableAssetsClientType { #client: ObservableSanityClient #httpRequest: HttpRequest constructor(client: ObservableSanityClient, httpRequest: HttpRequest) { @@ -72,7 +156,7 @@ export class ObservableAssetsClient { } /** @internal */ -export class AssetsClient { +export class AssetsClient implements AssetsClientType { #client: SanityClient #httpRequest: HttpRequest constructor(client: SanityClient, httpRequest: HttpRequest) { diff --git a/src/datasets/DatasetsClient.ts b/src/datasets/DatasetsClient.ts index f9ddc9b96..4313d3fea 100644 --- a/src/datasets/DatasetsClient.ts +++ b/src/datasets/DatasetsClient.ts @@ -6,7 +6,69 @@ import type {DatasetAclMode, DatasetResponse, DatasetsResponse, HttpRequest} fro import * as validate from '../validators' /** @internal */ -export class ObservableDatasetsClient { +export interface ObservableDatasetsClientType { + /** + * Create a new dataset with the given name + * + * @param name - Name of the dataset to create + * @param options - Options for the dataset + */ + create(name: string, options?: {aclMode?: DatasetAclMode}): Observable + + /** + * Edit a dataset with the given name + * + * @param name - Name of the dataset to edit + * @param options - New options for the dataset + */ + edit(name: string, options?: {aclMode?: DatasetAclMode}): Observable + + /** + * Delete a dataset with the given name + * + * @param name - Name of the dataset to delete + */ + delete(name: string): Observable<{deleted: true}> + + /** + * Fetch a list of datasets for the configured project + */ + list(): Observable +} + +/** @internal */ +export interface DatasetsClientType { + /** + * Create a new dataset with the given name + * + * @param name - Name of the dataset to create + * @param options - Options for the dataset + */ + create(name: string, options?: {aclMode?: DatasetAclMode}): Promise + + /** + * Edit a dataset with the given name + * + * @param name - Name of the dataset to edit + * @param options - New options for the dataset + */ + edit(name: string, options?: {aclMode?: DatasetAclMode}): Promise + + /** + * Delete a dataset with the given name + * + * @param name - Name of the dataset to delete + */ + delete(name: string): Promise<{deleted: true}> + + /** + * Fetch a list of datasets for the configured project + */ + list(): Promise +} + +/** @internal */ +export class ObservableDatasetsClient implements ObservableDatasetsClientType { #client: ObservableSanityClient #httpRequest: HttpRequest constructor(client: ObservableSanityClient, httpRequest: HttpRequest) { @@ -55,7 +117,7 @@ export class ObservableDatasetsClient { } /** @internal */ -export class DatasetsClient { +export class DatasetsClient implements DatasetsClientType { #client: SanityClient #httpRequest: HttpRequest constructor(client: SanityClient, httpRequest: HttpRequest) { diff --git a/src/projects/ProjectsClient.ts b/src/projects/ProjectsClient.ts index 094e215ed..ee40106d6 100644 --- a/src/projects/ProjectsClient.ts +++ b/src/projects/ProjectsClient.ts @@ -6,7 +6,49 @@ import type {HttpRequest, SanityProject} from '../types' import * as validate from '../validators' /** @internal */ -export class ObservableProjectsClient { +export interface ObservableProjectsClientType { + /** + * Fetch a list of projects the authenticated user has access to. + * + * @param options - Options for the list request + * @param options.includeMembers - Whether to include members in the response (default: true) + */ + list(options?: {includeMembers?: true}): Observable + list(options?: {includeMembers?: false}): Observable[]> + list(options?: { + includeMembers?: boolean + }): Observable[]> + + /** + * Fetch a project by project ID + * + * @param projectId - ID of the project to fetch + */ + getById(projectId: string): Observable +} + +/** @internal */ +export interface ProjectsClientType { + /** + * Fetch a list of projects the authenticated user has access to. + * + * @param options - Options for the list request + * @param options.includeMembers - Whether to include members in the response (default: true) + */ + list(options?: {includeMembers?: true}): Promise + list(options?: {includeMembers?: false}): Promise[]> + list(options?: {includeMembers?: boolean}): Promise + + /** + * Fetch a project by project ID + * + * @param projectId - ID of the project to fetch + */ + getById(projectId: string): Promise +} + +/** @internal */ +export class ObservableProjectsClient implements ObservableProjectsClientType { #client: ObservableSanityClient #httpRequest: HttpRequest constructor(client: ObservableSanityClient, httpRequest: HttpRequest) { @@ -42,7 +84,7 @@ export class ObservableProjectsClient { } /** @internal */ -export class ProjectsClient { +export class ProjectsClient implements ProjectsClientType { #client: SanityClient #httpRequest: HttpRequest constructor(client: SanityClient, httpRequest: HttpRequest) { diff --git a/src/users/UsersClient.ts b/src/users/UsersClient.ts index 7bd1eb5fe..357ccb968 100644 --- a/src/users/UsersClient.ts +++ b/src/users/UsersClient.ts @@ -4,8 +4,30 @@ import {_request} from '../data/dataMethods' import type {ObservableSanityClient, SanityClient} from '../SanityClient' import type {CurrentSanityUser, HttpRequest, SanityUser} from '../types' +/** @internal */ +export interface ObservableUsersClientType { + /** + * Fetch a user by user ID + * + * @param id - User ID of the user to fetch. If `me` is provided, a minimal response including the users role is returned. + */ + getById( + id: T, + ): Observable +} + +/** @internal */ +export interface UsersClientType { + /** + * Fetch a user by user ID + * + * @param id - User ID of the user to fetch. If `me` is provided, a minimal response including the users role is returned. + */ + getById(id: T): Promise +} + /** @public */ -export class ObservableUsersClient { +export class ObservableUsersClient implements ObservableUsersClientType { #client: ObservableSanityClient #httpRequest: HttpRequest constructor(client: ObservableSanityClient, httpRequest: HttpRequest) { @@ -30,7 +52,7 @@ export class ObservableUsersClient { } /** @public */ -export class UsersClient { +export class UsersClient implements UsersClientType { #client: SanityClient #httpRequest: HttpRequest constructor(client: SanityClient, httpRequest: HttpRequest) { diff --git a/test/client.test-d.ts b/test/client.test-d.ts index 51ed0c189..51554031b 100644 --- a/test/client.test-d.ts +++ b/test/client.test-d.ts @@ -1,12 +1,28 @@ import { + type Action, + type BaseActionOptions, + type ClientConfig, type ContentSourceMap, createClient, + type InitializedClientConfig, + type MultipleMutationResult, + type Mutation, + type MutationSelection, + type ObservablePatch, + type ObservableTransaction, + type Patch, type QueryOptions, type QueryParams, type QueryWithoutParams, type RawQueryResponse, + type SanityClientType, + type SanityDocument, + type SingleMutationResult, + type Transaction, } from '@sanity/client' +import {lastValueFrom} from 'rxjs' import {describe, expectTypeOf, test} from 'vitest' +import {createClient as createLatestClient} from '@sanity/client-latest' describe('client.fetch', () => { const client = createClient({}) @@ -25,26 +41,55 @@ describe('client.fetch', () => { }) test('simple query', async () => { expectTypeOf(await client.fetch('*')).toMatchTypeOf() + expectTypeOf(await lastValueFrom(client.observable.fetch('*'))).toMatchTypeOf() expectTypeOf(await client.fetch('*', undefined)).toMatchTypeOf() + expectTypeOf(await lastValueFrom(client.observable.fetch('*', undefined))).toMatchTypeOf() expectTypeOf(await client.fetch('*', {})).toMatchTypeOf() + expectTypeOf(await lastValueFrom(client.observable.fetch('*', {}))).toMatchTypeOf() expectTypeOf(await client.fetch('*[_type == $type]', {type: 'post'})).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.fetch('*[_type == $type]', {type: 'post'})), + ).toMatchTypeOf() expectTypeOf(await client.fetch('*', undefined, {filterResponse: false})).toMatchTypeOf< RawQueryResponse >() + expectTypeOf( + await lastValueFrom(client.observable.fetch('*', undefined, {filterResponse: false})), + ).toMatchTypeOf>() expectTypeOf( await client.fetch('*', {} satisfies QueryParams, { filterResponse: false, }), ).toMatchTypeOf>() + expectTypeOf( + await lastValueFrom( + client.observable.fetch('*', {} satisfies QueryParams, { + filterResponse: false, + }), + ), + ).toMatchTypeOf>() expectTypeOf( await client.fetch('*[_type == $type]', {type: 'post'}, {filterResponse: false}), ).toMatchTypeOf>() + expectTypeOf( + await lastValueFrom( + client.observable.fetch('*[_type == $type]', {type: 'post'}, {filterResponse: false}), + ), + ).toMatchTypeOf>() }) test('generics', async () => { expectTypeOf(await client.fetch('count(*)')).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.fetch('count(*)')), + ).toMatchTypeOf() expectTypeOf( await client.fetch('count(*[_type == $type])', {type: 'post'}), ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom( + client.observable.fetch('count(*[_type == $type])', {type: 'post'}), + ), + ).toMatchTypeOf() expectTypeOf( await client.fetch('count(*[_type == $type])', { // @ts-expect-error -- should fail @@ -184,6 +229,9 @@ describe('client.fetch', () => { expectTypeOf( await client.fetch('count(*)', {}, {filterResponse: true}), ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.fetch('count(*)', {}, {filterResponse: true})), + ).toMatchTypeOf() expectTypeOf( await client.fetch('count(*)', {}, {filterResponse: false}), ).toMatchTypeOf<{ @@ -192,6 +240,14 @@ describe('client.fetch', () => { query: string resultSourceMap?: ContentSourceMap }>() + expectTypeOf( + await lastValueFrom(client.observable.fetch('count(*)', {}, {filterResponse: false})), + ).toMatchTypeOf<{ + result: number + ms: number + query: string + resultSourceMap?: ContentSourceMap + }>() expectTypeOf( await client.fetch( 'count(*[_type == $type])', @@ -214,6 +270,9 @@ describe('client.fetch', () => { }) test('stega: false', async () => { expectTypeOf(await client.fetch('*', {}, {stega: false})).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.fetch('*', {}, {stega: false})), + ).toMatchTypeOf() }) test('params can use properties that conflict with Next.js-defined properties', async () => { // `client.fetch` has type checking to prevent the common mistake of passing `cache` and `next` options as params (2nd parameter) in Next.js projects, where they should be passed as options (the 3rd parameter) @@ -350,3 +409,566 @@ describe('client.fetch', () => { ).toMatchTypeOf() }) }) + +// Tests for client configuration methods +describe('client.config and client.withConfig', () => { + test('client.config', () => { + const client = createClient({}) + + // Test config getter + expectTypeOf(client.config()).toMatchTypeOf() + expectTypeOf(client.observable.config()).toMatchTypeOf() + + // Test config setter + const newConfig: Partial = {apiVersion: '2023-05-03'} + expectTypeOf(client.config(newConfig)).toEqualTypeOf(client) + expectTypeOf(client.observable.config(newConfig)).toEqualTypeOf(client.observable) + + // Test withConfig + expectTypeOf(client.withConfig(newConfig)).toMatchTypeOf(client) + expectTypeOf(client.observable.withConfig(newConfig)).toMatchTypeOf(client.observable) + }) +}) + +// Tests for URL helper methods +describe('client URL methods', () => { + test('client.getUrl', () => { + const client = createClient({}) + + // Test getUrl with default canUseCdn + expectTypeOf(client.getUrl('/path/to/resource')).toMatchTypeOf() + expectTypeOf(client.observable.getUrl('/path/to/resource')).toMatchTypeOf() + + // Test getUrl with explicit canUseCdn + expectTypeOf(client.getUrl('/path/to/resource', true)).toMatchTypeOf() + expectTypeOf(client.observable.getUrl('/path/to/resource', true)).toMatchTypeOf() + + expectTypeOf(client.getUrl('/path/to/resource', false)).toMatchTypeOf() + expectTypeOf(client.observable.getUrl('/path/to/resource', false)).toMatchTypeOf() + }) + + test('client.getDataUrl', () => { + const client = createClient({}) + + // Test getDataUrl with only operation + expectTypeOf(client.getDataUrl('query')).toMatchTypeOf() + expectTypeOf(client.observable.getDataUrl('query')).toMatchTypeOf() + + // Test getDataUrl with operation and path + expectTypeOf(client.getDataUrl('query', 'production')).toMatchTypeOf() + expectTypeOf(client.observable.getDataUrl('query', 'production')).toMatchTypeOf() + }) +}) + +// Tests for document methods +describe('client document methods', () => { + const client = createClient({}) + + test('client.getDocument', async () => { + // Basic usage + expectTypeOf(await client.getDocument('doc123')).toMatchTypeOf< + SanityDocument | undefined + >() + expectTypeOf(await lastValueFrom(client.observable.getDocument('doc123'))).toMatchTypeOf< + SanityDocument | undefined + >() + + // With generic type + expectTypeOf(await client.getDocument<{title: string}>('doc123')).toMatchTypeOf< + SanityDocument<{title: string}> | undefined + >() + expectTypeOf( + await lastValueFrom(client.observable.getDocument<{title: string}>('doc123')), + ).toMatchTypeOf | undefined>() + + // With options + expectTypeOf(await client.getDocument('doc123', {tag: 'tag1'})).toMatchTypeOf< + SanityDocument | undefined + >() + expectTypeOf( + await lastValueFrom(client.observable.getDocument('doc123', {tag: 'tag1'})), + ).toMatchTypeOf | undefined>() + }) + + test('client.getDocuments', async () => { + // Basic usage + expectTypeOf(await client.getDocuments(['doc123', 'doc456'])).toMatchTypeOf< + (SanityDocument | null)[] + >() + expectTypeOf( + await lastValueFrom(client.observable.getDocuments(['doc123', 'doc456'])), + ).toMatchTypeOf<(SanityDocument | null)[]>() + + // With generic type + expectTypeOf(await client.getDocuments<{title: string}>(['doc123', 'doc456'])).toMatchTypeOf< + (SanityDocument<{title: string}> | null)[] + >() + expectTypeOf( + await lastValueFrom(client.observable.getDocuments<{title: string}>(['doc123', 'doc456'])), + ).toMatchTypeOf<(SanityDocument<{title: string}> | null)[]>() + + // With options + expectTypeOf(await client.getDocuments(['doc123', 'doc456'], {tag: 'tag1'})).toMatchTypeOf< + (SanityDocument | null)[] + >() + expectTypeOf( + await lastValueFrom(client.observable.getDocuments(['doc123', 'doc456'], {tag: 'tag1'})), + ).toMatchTypeOf<(SanityDocument | null)[]>() + }) + + test('client.create', async () => { + const doc = {_type: 'post', title: 'Hello World'} + + // Basic usage + expectTypeOf(await client.create(doc)).toMatchTypeOf>() + expectTypeOf(await lastValueFrom(client.observable.create(doc))).toMatchTypeOf< + SanityDocument + >() + + // With generic type + expectTypeOf(await client.create<{title: string}>(doc)).toMatchTypeOf< + SanityDocument<{title: string}> + >() + expectTypeOf(await lastValueFrom(client.observable.create<{title: string}>(doc))).toMatchTypeOf< + SanityDocument<{title: string}> + >() + + // Return first document with options + expectTypeOf(await client.create(doc, {returnFirst: true})).toMatchTypeOf>() + expectTypeOf( + await lastValueFrom(client.observable.create(doc, {returnFirst: true})), + ).toMatchTypeOf>() + + // Return documents array with options + const docsResult = await client.create(doc, {returnFirst: false, returnDocuments: true}) + expectTypeOf(docsResult).toBeArray() + + const obsDocsResult = await lastValueFrom( + client.observable.create(doc, {returnFirst: false, returnDocuments: true}), + ) + expectTypeOf(obsDocsResult).toBeArray() + + // Return mutation result + expectTypeOf( + await client.create(doc, {returnDocuments: false}), + ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.create(doc, {returnDocuments: false})), + ).toMatchTypeOf() + }) + + test('client.createIfNotExists', async () => { + const docWithId = {_id: 'unique123', _type: 'post', title: 'Hello World'} + + // Basic usage + expectTypeOf(await client.createIfNotExists(docWithId)).toMatchTypeOf>() + expectTypeOf(await lastValueFrom(client.observable.createIfNotExists(docWithId))).toMatchTypeOf< + SanityDocument + >() + + // With generic type + expectTypeOf(await client.createIfNotExists<{title: string}>(docWithId)).toMatchTypeOf< + SanityDocument<{title: string}> + >() + expectTypeOf( + await lastValueFrom(client.observable.createIfNotExists<{title: string}>(docWithId)), + ).toMatchTypeOf>() + + // Return first document with options + expectTypeOf(await client.createIfNotExists(docWithId, {returnFirst: true})).toMatchTypeOf< + SanityDocument + >() + expectTypeOf( + await lastValueFrom(client.observable.createIfNotExists(docWithId, {returnFirst: true})), + ).toMatchTypeOf>() + + // Return documents array with options + const docsResult = await client.createIfNotExists(docWithId, { + returnFirst: false, + returnDocuments: true, + }) + expectTypeOf(docsResult).toBeArray() + + const obsDocsResult = await lastValueFrom( + client.observable.createIfNotExists(docWithId, {returnFirst: false, returnDocuments: true}), + ) + expectTypeOf(obsDocsResult).toBeArray() + + // Return mutation result + expectTypeOf( + await client.createIfNotExists(docWithId, {returnDocuments: false}), + ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.createIfNotExists(docWithId, {returnDocuments: false})), + ).toMatchTypeOf() + }) + + test('client.createOrReplace', async () => { + const docWithId = {_id: 'unique123', _type: 'post', title: 'Hello World'} + + // Basic usage + expectTypeOf(await client.createOrReplace(docWithId)).toMatchTypeOf>() + expectTypeOf(await lastValueFrom(client.observable.createOrReplace(docWithId))).toMatchTypeOf< + SanityDocument + >() + + // With generic type + expectTypeOf(await client.createOrReplace<{title: string}>(docWithId)).toMatchTypeOf< + SanityDocument<{title: string}> + >() + expectTypeOf( + await lastValueFrom(client.observable.createOrReplace<{title: string}>(docWithId)), + ).toMatchTypeOf>() + + // Return first document with options + expectTypeOf(await client.createOrReplace(docWithId, {returnFirst: true})).toMatchTypeOf< + SanityDocument + >() + expectTypeOf( + await lastValueFrom(client.observable.createOrReplace(docWithId, {returnFirst: true})), + ).toMatchTypeOf>() + + // Return documents array with options + const docsResult = await client.createOrReplace(docWithId, { + returnFirst: false, + returnDocuments: true, + }) + expectTypeOf(docsResult).toBeArray() + + const obsDocsResult = await lastValueFrom( + client.observable.createOrReplace(docWithId, {returnFirst: false, returnDocuments: true}), + ) + expectTypeOf(obsDocsResult).toBeArray() + + // Return mutation result + expectTypeOf( + await client.createOrReplace(docWithId, {returnDocuments: false}), + ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.createOrReplace(docWithId, {returnDocuments: false})), + ).toMatchTypeOf() + }) + + test('client.delete', async () => { + // With document ID + expectTypeOf(await client.delete('doc123')).toMatchTypeOf>() + expectTypeOf(await lastValueFrom(client.observable.delete('doc123'))).toMatchTypeOf< + SanityDocument + >() + + // With generic type + expectTypeOf(await client.delete<{title: string}>('doc123')).toMatchTypeOf< + SanityDocument<{title: string}> + >() + expectTypeOf( + await lastValueFrom(client.observable.delete<{title: string}>('doc123')), + ).toMatchTypeOf>() + + // With selection object + const selection: MutationSelection = {query: '*[_type == "post"]'} + expectTypeOf(await client.delete(selection)).toMatchTypeOf>() + expectTypeOf(await lastValueFrom(client.observable.delete(selection))).toMatchTypeOf< + SanityDocument + >() + + // Return first document with options + expectTypeOf(await client.delete('doc123', {returnFirst: true})).toMatchTypeOf< + SanityDocument + >() + expectTypeOf( + await lastValueFrom(client.observable.delete('doc123', {returnFirst: true})), + ).toMatchTypeOf>() + + // Return documents array with options + const docsResult = await client.delete('doc123', {returnFirst: false, returnDocuments: true}) + expectTypeOf(docsResult).toBeArray() + + const obsDocsResult = await lastValueFrom( + client.observable.delete('doc123', {returnFirst: false, returnDocuments: true}), + ) + expectTypeOf(obsDocsResult).toBeArray() + + // Return mutation result + expectTypeOf( + await client.delete('doc123', {returnDocuments: false}), + ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.delete('doc123', {returnDocuments: false})), + ).toMatchTypeOf() + }) + + test('client.mutate', async () => { + const mutations: Mutation[] = [{create: {_type: 'post', title: 'Hello World'}}] + + // Basic usage + expectTypeOf(await client.mutate(mutations)).toMatchTypeOf>() + expectTypeOf(await lastValueFrom(client.observable.mutate(mutations))).toMatchTypeOf< + SanityDocument + >() + + // Return first document with options + expectTypeOf(await client.mutate(mutations, {returnFirst: true})).toMatchTypeOf< + SanityDocument + >() + expectTypeOf( + await lastValueFrom(client.observable.mutate(mutations, {returnFirst: true})), + ).toMatchTypeOf>() + + // Return documents array with options + const docsResult = await client.mutate(mutations, {returnFirst: false, returnDocuments: true}) + expectTypeOf(docsResult).toBeArray() + + const obsDocsResult = await lastValueFrom( + client.observable.mutate(mutations, {returnFirst: false, returnDocuments: true}), + ) + expectTypeOf(obsDocsResult).toBeArray() + + // Return mutation result + expectTypeOf( + await client.mutate(mutations, {returnDocuments: false}), + ).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.mutate(mutations, {returnDocuments: false})), + ).toMatchTypeOf() + + // With transaction + const transaction = client.transaction().create({_type: 'post', title: 'Hello World'}) + expectTypeOf(await client.mutate(transaction)).toMatchTypeOf>() + + // With observable transaction + const obsTransaction = client.observable + .transaction() + .create({_type: 'post', title: 'Hello World'}) + expectTypeOf(await lastValueFrom(client.observable.mutate(obsTransaction))).toMatchTypeOf< + SanityDocument + >() + }) + + test('client.patch', async () => { + // Create patch with ID + const patch = client.patch('doc123') + expectTypeOf(patch).toMatchTypeOf() + + // Create observable patch with ID + const obsPatch = client.observable.patch('doc123') + expectTypeOf(obsPatch).toMatchTypeOf() + + // Create patch with array of IDs + const patchWithIds = client.patch(['doc123', 'doc456']) + expectTypeOf(patchWithIds).toMatchTypeOf() + + // Create patch with query selection + const patchWithQuery = client.patch({query: '*[_type == "post"]'}) + expectTypeOf(patchWithQuery).toMatchTypeOf() + + // Create patch with operations + const patchWithOps = client.patch('doc123', {set: {title: 'Updated Title'}}) + expectTypeOf(patchWithOps).toMatchTypeOf() + + // Patch operations + const patchWithChain = client + .patch('doc123') + .set({title: 'Updated Title'}) + .inc({count: 1}) + .dec({visits: 1}) + .unset(['oldField']) + + expectTypeOf(patchWithChain).toMatchTypeOf() + + // Commit operations + const commitResult = await patchWithChain.commit() + expectTypeOf(commitResult).toMatchTypeOf>() + + const obsCommitResult = await lastValueFrom(obsPatch.set({title: 'Updated Title'}).commit()) + expectTypeOf(obsCommitResult).toMatchTypeOf>() + + // Commit with options + const commitOptions = await patchWithChain.commit({returnDocuments: false}) + expectTypeOf(commitOptions).toMatchTypeOf() + + const obsCommitOptions = await lastValueFrom(obsPatch.commit({returnDocuments: false})) + expectTypeOf(obsCommitOptions).toMatchTypeOf() + }) + + test('client.transaction', async () => { + // Create empty transaction + const transaction = client.transaction() + expectTypeOf(transaction).toMatchTypeOf() + + // Create observable transaction + const obsTransaction = client.observable.transaction() + expectTypeOf(obsTransaction).toMatchTypeOf() + + // Create transaction with operations + const transactionWithOps = client.transaction([{create: {_type: 'post', title: 'Hello World'}}]) + expectTypeOf(transactionWithOps).toMatchTypeOf() + + // Transaction operations + const transactionChain = client + .transaction() + .create({_type: 'post', title: 'Hello World'}) + .createIfNotExists({_id: 'unique123', _type: 'post', title: 'Hello Again'}) + .delete('doc123') + + expectTypeOf(transactionChain).toMatchTypeOf() + + // Commit operations + const commitResult = await transactionChain.commit() + expectTypeOf(commitResult).toMatchTypeOf() + + const obsCommitResult = await lastValueFrom( + obsTransaction.create({_type: 'post', title: 'Hello'}).commit(), + ) + expectTypeOf(obsCommitResult).toMatchTypeOf() + + // Commit with options + const commitOptions = await transactionChain.commit({returnFirst: true}) + expectTypeOf(commitOptions).toMatchTypeOf() + + const obsCommitOptions = await lastValueFrom( + obsTransaction.commit({returnFirst: true, returnDocuments: false}), + ) + expectTypeOf(obsCommitOptions).toMatchTypeOf() + }) + + test('client.action', async () => { + // Single action + const action = { + actionType: 'sanity.action.document.publish', + draftId: 'draft.bike-123', + publishedId: 'bike-123', + } satisfies Action + + const actionResult = await client.action(action) + expectTypeOf(actionResult).toBeObject() + + const obsActionResult = await lastValueFrom(client.observable.action(action)) + expectTypeOf(obsActionResult).toBeObject() + + // Action array + const actions = [ + { + actionType: 'sanity.action.document.publish', + draftId: 'draft.bike-123', + publishedId: 'bike-123', + }, + { + actionType: 'sanity.action.document.unpublish', + publishedId: 'bike-456', + draftId: 'draft.bike-456', + }, + ] satisfies Action[] + + const actionsResult = await client.action(actions) + expectTypeOf(actionsResult).toBeObject() + + const obsActionsResult = await lastValueFrom(client.observable.action(actions)) + expectTypeOf(obsActionsResult).toBeObject() + + // With options + const actionOptions = { + dryRun: true, + transactionId: 'my-transaction-id', + } satisfies BaseActionOptions + + const actionWithOptions = await client.action(action, actionOptions) + expectTypeOf(actionWithOptions).toBeObject() + + const obsActionWithOptions = await lastValueFrom( + client.observable.action(action, actionOptions), + ) + expectTypeOf(obsActionWithOptions).toBeObject() + }) + + test('client.request', async () => { + // Basic request + const requestResult = await client.request({ + method: 'GET', + uri: '/path/to/endpoint', + }) + expectTypeOf(requestResult).toBeAny() + + const obsRequestResult = await lastValueFrom( + client.observable.request({ + method: 'GET', + uri: '/path/to/endpoint', + }), + ) + expectTypeOf(obsRequestResult).toBeAny() + + // With generic type + interface CustomResponse { + result: string + timestamp: number + } + + const typedRequest = await client.request({ + method: 'GET', + uri: '/path/to/endpoint', + }) + expectTypeOf(typedRequest).toMatchTypeOf() + + const obsTypedRequest = await lastValueFrom( + client.observable.request({ + method: 'GET', + uri: '/path/to/endpoint', + }), + ) + expectTypeOf(obsTypedRequest).toMatchTypeOf() + }) +}) + +describe('client interfaces for library users', () => { + test('SanityClientType', () => { + async function defineLive(client: SanityClientType) { + expectTypeOf(await client.fetch('*')).toMatchTypeOf() + expectTypeOf(await lastValueFrom(client.observable.fetch('*'))).toMatchTypeOf() + expectTypeOf(await client.fetch('*', undefined)).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.fetch('*', undefined)), + ).toMatchTypeOf() + expectTypeOf(await client.fetch('*', {})).toMatchTypeOf() + expectTypeOf(await lastValueFrom(client.observable.fetch('*', {}))).toMatchTypeOf() + expectTypeOf(await client.fetch('*[_type == $type]', {type: 'post'})).toMatchTypeOf() + expectTypeOf( + await lastValueFrom(client.observable.fetch('*[_type == $type]', {type: 'post'})), + ).toMatchTypeOf() + expectTypeOf(await client.fetch('*', undefined, {filterResponse: false})).toMatchTypeOf< + RawQueryResponse + >() + expectTypeOf( + await lastValueFrom(client.observable.fetch('*', undefined, {filterResponse: false})), + ).toMatchTypeOf>() + expectTypeOf( + await client.fetch('*', {} satisfies QueryParams, { + filterResponse: false, + }), + ).toMatchTypeOf>() + expectTypeOf( + await lastValueFrom( + client.observable.fetch( + '*', + {} satisfies QueryParams, + { + filterResponse: false, + }, + ), + ), + ).toMatchTypeOf>() + expectTypeOf( + await client.fetch('*[_type == $type]', {type: 'post'}, {filterResponse: false}), + ).toMatchTypeOf>() + expectTypeOf( + await lastValueFrom( + client.observable.fetch('*[_type == $type]', {type: 'post'}, {filterResponse: false}), + ), + ).toMatchTypeOf>() + } + const client = createLatestClient({ + projectId: 'my-project-id', + dataset: 'my-dataset', + apiVersion: '2021-03-25', + }) + defineLive(client) + }) +})