@sanity/client
Version:
Client for retrieving, creating and patching data from Sanity.io
384 lines (351 loc) • 13.1 kB
text/typescript
import type {Observable} from 'rxjs'
import type {ObservableSanityClient, SanityClient} from '../SanityClient'
import type {
Any,
BaseMutationOptions,
IdentifiedSanityDocumentStub,
MultipleMutationResult,
Mutation,
MutationSelection,
PatchOperations,
SanityDocument,
SanityDocumentStub,
SingleMutationResult,
TransactionAllDocumentIdsMutationOptions,
TransactionAllDocumentsMutationOptions,
TransactionFirstDocumentIdMutationOptions,
TransactionFirstDocumentMutationOptions,
} from '../types'
import * as validators from '../validators'
import {ObservablePatch, Patch} from './patch'
/** @public */
export type PatchBuilder = (patch: Patch) => Patch
/** @public */
export type ObservablePatchBuilder = (patch: ObservablePatch) => ObservablePatch
const defaultMutateOptions = {returnDocuments: false}
/** @internal */
export class BaseTransaction {
protected operations: Mutation[]
protected trxId?: string
constructor(operations: Mutation[] = [], transactionId?: string) {
this.operations = operations
this.trxId = transactionId
}
/**
* Creates a new Sanity document. If `_id` is provided and already exists, the mutation will fail. If no `_id` is given, one will automatically be generated by the database.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param doc - Document to create. Requires a `_type` property.
*/
create<R extends Record<string, Any> = Record<string, Any>>(doc: SanityDocumentStub<R>): this {
validators.validateObject('create', doc)
return this._add({create: doc})
}
/**
* Creates a new Sanity document. If a document with the same `_id` already exists, the create operation will be ignored.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param doc - Document to create if it does not already exist. Requires `_id` and `_type` properties.
*/
createIfNotExists<R extends Record<string, Any> = Record<string, Any>>(
doc: IdentifiedSanityDocumentStub<R>,
): this {
const op = 'createIfNotExists'
validators.validateObject(op, doc)
validators.requireDocumentId(op, doc)
return this._add({[op]: doc})
}
/**
* Creates a new Sanity document, or replaces an existing one if the same `_id` is already used.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param doc - Document to create or replace. Requires `_id` and `_type` properties.
*/
createOrReplace<R extends Record<string, Any> = Record<string, Any>>(
doc: IdentifiedSanityDocumentStub<R>,
): this {
const op = 'createOrReplace'
validators.validateObject(op, doc)
validators.requireDocumentId(op, doc)
return this._add({[op]: doc})
}
/**
* Deletes the document with the given document ID
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param documentId - Document ID to delete
*/
delete(documentId: string): this {
validators.validateDocumentId('delete', documentId)
return this._add({delete: {id: documentId}})
}
/**
* Gets the current transaction ID, if any
*/
transactionId(): string | undefined
/**
* Set the ID of this transaction.
*
* @param id - Transaction ID
*/
transactionId(id: string): this
transactionId(id?: string): this | string | undefined {
if (!id) {
return this.trxId
}
this.trxId = id
return this
}
/**
* Return a plain JSON representation of the transaction
*/
serialize(): Mutation[] {
return [...this.operations]
}
/**
* Return a plain JSON representation of the transaction
*/
toJSON(): Mutation[] {
return this.serialize()
}
/**
* Clears the transaction of all operations
*/
reset(): this {
this.operations = []
return this
}
protected _add(mut: Mutation): this {
this.operations.push(mut)
return this
}
}
/** @public */
export class Transaction extends BaseTransaction {
#client?: SanityClient
constructor(operations?: Mutation[], client?: SanityClient, transactionId?: string) {
super(operations, transactionId)
this.#client = client
}
/**
* Clones the transaction
*/
clone(): Transaction {
return new Transaction([...this.operations], this.#client, this.trxId)
}
/**
* Commit the transaction, returning a promise that resolves to the first mutated document
*
* @param options - Options for the mutation operation
*/
commit<R extends Record<string, Any>>(
options: TransactionFirstDocumentMutationOptions,
): Promise<SanityDocument<R>>
/**
* Commit the transaction, returning a promise that resolves to an array of the mutated documents
*
* @param options - Options for the mutation operation
*/
commit<R extends Record<string, Any>>(
options: TransactionAllDocumentsMutationOptions,
): Promise<SanityDocument<R>[]>
/**
* Commit the transaction, returning a promise that resolves to a mutation result object
*
* @param options - Options for the mutation operation
*/
commit(options: TransactionFirstDocumentIdMutationOptions): Promise<SingleMutationResult>
/**
* Commit the transaction, returning a promise that resolves to a mutation result object
*
* @param options - Options for the mutation operation
*/
commit(options: TransactionAllDocumentIdsMutationOptions): Promise<MultipleMutationResult>
/**
* Commit the transaction, returning a promise that resolves to a mutation result object
*
* @param options - Options for the mutation operation
*/
commit(options?: BaseMutationOptions): Promise<MultipleMutationResult>
commit<R extends Record<string, Any> = Record<string, Any>>(
options?:
| TransactionFirstDocumentMutationOptions
| TransactionAllDocumentsMutationOptions
| TransactionFirstDocumentIdMutationOptions
| TransactionAllDocumentIdsMutationOptions
| BaseMutationOptions,
): Promise<
SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
> {
if (!this.#client) {
throw new Error(
'No `client` passed to transaction, either provide one or pass the ' +
'transaction to a clients `mutate()` method',
)
}
return this.#client.mutate<R>(
this.serialize() as Any,
Object.assign({transactionId: this.trxId}, defaultMutateOptions, options || {}),
)
}
/**
* Performs a patch on the given document ID. Can either be a builder function or an object of patch operations.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param documentId - Document ID to perform the patch operation on
* @param patchOps - Operations to perform, or a builder function
*/
patch(documentId: string, patchOps?: PatchBuilder | PatchOperations): this
/**
* Performs a patch on the given selection. Can either be a builder function or an object of patch operations.
*
* @param selection - An object with `query` and optional `params`, defining which document(s) to patch
* @param patchOps - Operations to perform, or a builder function
*/
patch(patch: MutationSelection, patchOps?: PatchBuilder | PatchOperations): this
/**
* Adds the given patch instance to the transaction.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param patch - Patch to execute
*/
patch(patch: Patch): this
patch(
patchOrDocumentId: Patch | MutationSelection | string,
patchOps?: PatchBuilder | PatchOperations,
): this {
const isBuilder = typeof patchOps === 'function'
const isPatch = typeof patchOrDocumentId !== 'string' && patchOrDocumentId instanceof Patch
const isMutationSelection =
typeof patchOrDocumentId === 'object' &&
('query' in patchOrDocumentId || 'id' in patchOrDocumentId)
// transaction.patch(client.patch('documentId').inc({visits: 1}))
if (isPatch) {
return this._add({patch: patchOrDocumentId.serialize()})
}
// patch => patch.inc({visits: 1}).set({foo: 'bar'})
if (isBuilder) {
const patch = patchOps(new Patch(patchOrDocumentId, {}, this.#client))
if (!(patch instanceof Patch)) {
throw new Error('function passed to `patch()` must return the patch')
}
return this._add({patch: patch.serialize()})
}
/*
* transaction.patch(
* {query: "*[_type == 'person' && points >= $threshold]", params: { threshold: 100 }},
* {dec: { points: 100 }, inc: { bonuses: 1 }}
* )
*/
if (isMutationSelection) {
const patch = new Patch(patchOrDocumentId, patchOps || {}, this.#client)
return this._add({patch: patch.serialize()})
}
return this._add({patch: {id: patchOrDocumentId, ...patchOps}})
}
}
/** @public */
export class ObservableTransaction extends BaseTransaction {
#client?: ObservableSanityClient
constructor(operations?: Mutation[], client?: ObservableSanityClient, transactionId?: string) {
super(operations, transactionId)
this.#client = client
}
/**
* Clones the transaction
*/
clone(): ObservableTransaction {
return new ObservableTransaction([...this.operations], this.#client, this.trxId)
}
/**
* Commit the transaction, returning an observable that produces the first mutated document
*
* @param options - Options for the mutation operation
*/
commit<R extends Record<string, Any>>(
options: TransactionFirstDocumentMutationOptions,
): Observable<SanityDocument<R>>
/**
* Commit the transaction, returning an observable that produces an array of the mutated documents
*
* @param options - Options for the mutation operation
*/
commit<R extends Record<string, Any>>(
options: TransactionAllDocumentsMutationOptions,
): Observable<SanityDocument<R>[]>
/**
* Commit the transaction, returning an observable that produces a mutation result object
*
* @param options - Options for the mutation operation
*/
commit(options: TransactionFirstDocumentIdMutationOptions): Observable<SingleMutationResult>
/**
* Commit the transaction, returning an observable that produces a mutation result object
*
* @param options - Options for the mutation operation
*/
commit(options: TransactionAllDocumentIdsMutationOptions): Observable<MultipleMutationResult>
/**
* Commit the transaction, returning an observable that produces a mutation result object
*
* @param options - Options for the mutation operation
*/
commit(options?: BaseMutationOptions): Observable<MultipleMutationResult>
commit<R extends Record<string, Any> = Record<string, Any>>(
options?:
| TransactionFirstDocumentMutationOptions
| TransactionAllDocumentsMutationOptions
| TransactionFirstDocumentIdMutationOptions
| TransactionAllDocumentIdsMutationOptions
| BaseMutationOptions,
): Observable<
SanityDocument<R> | SanityDocument<R>[] | SingleMutationResult | MultipleMutationResult
> {
if (!this.#client) {
throw new Error(
'No `client` passed to transaction, either provide one or pass the ' +
'transaction to a clients `mutate()` method',
)
}
return this.#client.mutate<R>(
this.serialize() as Any,
Object.assign({transactionId: this.trxId}, defaultMutateOptions, options || {}),
)
}
/**
* Performs a patch on the given document ID. Can either be a builder function or an object of patch operations.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param documentId - Document ID to perform the patch operation on
* @param patchOps - Operations to perform, or a builder function
*/
patch(documentId: string, patchOps?: ObservablePatchBuilder | PatchOperations): this
/**
* Adds the given patch instance to the transaction.
* The operation is added to the current transaction, ready to be commited by `commit()`
*
* @param patch - ObservablePatch to execute
*/
patch(patch: ObservablePatch): this
patch(
patchOrDocumentId: ObservablePatch | string,
patchOps?: ObservablePatchBuilder | PatchOperations,
): this {
const isBuilder = typeof patchOps === 'function'
const isPatch =
typeof patchOrDocumentId !== 'string' && patchOrDocumentId instanceof ObservablePatch
// transaction.patch(client.patch('documentId').inc({visits: 1}))
if (isPatch) {
return this._add({patch: patchOrDocumentId.serialize()})
}
// patch => patch.inc({visits: 1}).set({foo: 'bar'})
if (isBuilder) {
const patch = patchOps(new ObservablePatch(patchOrDocumentId, {}, this.#client))
if (!(patch instanceof ObservablePatch)) {
throw new Error('function passed to `patch()` must return the patch')
}
return this._add({patch: patch.serialize()})
}
return this._add({patch: {id: patchOrDocumentId, ...patchOps}})
}
}