From 5c8e6f26500f4962be79c45e090df74132e00cb1 Mon Sep 17 00:00:00 2001 From: Radhe Date: Thu, 23 May 2024 03:19:39 +0200 Subject: [PATCH] fix(actions): add unit tests, move actionType out of types --- src/data/dataMethods.ts | 28 ++++++- src/types.ts | 42 +++++------ test/client.test.ts | 159 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 24 deletions(-) diff --git a/src/data/dataMethods.ts b/src/data/dataMethods.ts index 1d0d04f1..12beba62 100644 --- a/src/data/dataMethods.ts +++ b/src/data/dataMethods.ts @@ -255,12 +255,33 @@ export function _action( const transactionId = (options && options.transactionId) || undefined const skipCrossDatasetReferenceValidation = (options && options.skipCrossDatasetReferenceValidation) || undefined + const dryRun = (options && options.dryRun) || undefined + + const actionWithTypes = acts.map((act: Action) => { + if ('create' in act) { + return {actionType: 'sanity.action.document.create', ...act.create} + } else if ('replaceDraft' in act) { + return {actionType: 'sanity.action.document.replaceDraft', ...act.replaceDraft} + } else if ('edit' in act) { + return {actionType: 'sanity.action.document.edit', ...act.edit} + } else if ('delete' in act) { + return {actionType: 'sanity.action.document.delete', ...act.delete} + } else if ('discard' in act) { + return {actionType: 'sanity.action.document.discard', ...act.discard} + } else if ('publish' in act) { + return {actionType: 'sanity.action.document.publish', ...act.publish} + } else if ('unpublish' in act) { + return {actionType: 'sanity.action.document.unpublish', ...act.unpublish} + } + + return act + }) return _dataRequest( client, httpRequest, 'actions', - {actions: acts, transactionId, skipCrossDatasetReferenceValidation}, + {actions: actionWithTypes, transactionId, skipCrossDatasetReferenceValidation, dryRun}, options, ) } @@ -276,12 +297,13 @@ export function _dataRequest( options: Any = {}, ): Any { const isMutation = endpoint === 'mutate' + const isAction = endpoint === 'actions' const isQuery = endpoint === 'query' // Check if the query string is within a configured threshold, // in which case we can use GET. Otherwise, use POST. - const strQuery = isMutation ? '' : encodeQueryString(body) - const useGet = !isMutation && strQuery.length < getQuerySizeLimit + const strQuery = isMutation || isAction ? '' : encodeQueryString(body) + const useGet = !isMutation && !isAction && strQuery.length < getQuerySizeLimit const stringQuery = useGet ? strQuery : '' const returnFirst = options.returnFirst const {timeout, token, tag, headers, returnQuery, lastLiveEventId} = options diff --git a/src/types.ts b/src/types.ts index 779a2ac2..657e2964 100644 --- a/src/types.ts +++ b/src/types.ts @@ -513,22 +513,22 @@ export type Mutation = Record> = /** @public */ export type Action = - | CreateAction - | ReplaceDraftAction - | EditAction - | DeleteAction - | DiscardAction - | PublishAction - | UnpublishAction + | {create: CreateAction} + | {replaceDraft: ReplaceDraftAction} + | {edit: EditAction} + | {delete: DeleteAction} + | {discard: DiscardAction} + | {publish: PublishAction} + | {unpublish: UnpublishAction} /** * Creates a new draft document. The published version of the document must not already exist. * If the draft version of the document already exists the action will fail by default, but * this can be adjusted to instead leave the existing document in place. + * + * @public */ export type CreateAction = { - actionType: 'sanity.action.document.create' - /** * ID of the published document to create a draft for. */ @@ -548,10 +548,10 @@ export type CreateAction = { /** * Replaces an existing draft document. * At least one of the draft or published versions of the document must exist. + * + * @public */ export type ReplaceDraftAction = { - actionType: 'sanity.action.document.replaceDraft' - /** * Draft document ID to replace, if it exists. */ @@ -572,10 +572,10 @@ export type ReplaceDraftAction = { * Modifies an existing draft document. * It applies the given patch to the document referenced by draftId. * If there is no such document then one is created using the current state of the published version and then that is updated accordingly. + * + * @public */ export type EditAction = { - actionType: 'sanity.action.document.edit' - /** * Draft document ID to edit */ @@ -596,10 +596,10 @@ export type EditAction = { * Deletes the published version of a document and optionally some (likely all known) draft versions. * If any draft version exists that is not specified for deletion this is an error. * If the purge flag is set then the document history is also deleted. + * + * @public */ export type DeleteAction = { - actionType: 'sanity.action.document.delete' - /** * Published document ID to delete */ @@ -619,10 +619,10 @@ export type DeleteAction = { /** * Delete the draft version of a document. * It is an error if it does not exist. If the purge flag is set, the document history is also deleted. + * + * @public */ export type DiscardAction = { - actionType: 'sanity.action.document.discard' - /** * Draft document ID to delete */ @@ -640,10 +640,10 @@ export type DiscardAction = { * In either case the draft document is deleted. * The optional revision id parameters can be used for optimistic locking to ensure * that the draft and/or published versions of the document have not been changed by another client. + * + * @public */ export type PublishAction = { - actionType: 'sanity.action.document.publish' - /** * Draft document ID to publish */ @@ -669,10 +669,10 @@ export type PublishAction = { * Retract a published document. * If there is no draft version then this is created from the published version. * In either case the published version is deleted. + * + * @public */ export type UnpublishAction = { - actionType: 'sanity.action.document.unpublish' - /** * Draft document ID to replace the published document with */ diff --git a/test/client.test.ts b/test/client.test.ts index 01844331..4625484d 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -2,6 +2,8 @@ import fs from 'node:fs' import path from 'node:path' import { + Action, + BaseActionOptions, type ClientConfig, ClientError, ContentSourceMap, @@ -1463,6 +1465,163 @@ describe('client', async () => { }, ) + test.skipIf(isEdge)('action() performs single operation', async () => { + const action: Action = { + create: { + publishedId: 'post1', + attributes: {_id: 'post1', _type: 'post'}, + ifExists: 'fail', + }, + } + + nock(projectHost()) + .post('/v1/data/actions/foo', { + actions: [{actionType: 'sanity.action.document.create', ...action.create}], + }) + .reply(200, { + transactionId: 'foo', + }) + + await expect(getClient().action(action)).resolves.not.toThrow() + }) + + test.skipIf(isEdge)('action() performs multiple operations', async () => { + const action1: Action = { + create: { + publishedId: 'post1', + attributes: {_id: 'post1', _type: 'post'}, + ifExists: 'fail', + }, + } + + const action2: Action = { + replaceDraft: { + draftId: 'drafts.post2', + publishedId: 'post2', + attributes: {_id: 'post2', _type: 'post'}, + }, + } + + const action3: Action = { + edit: { + draftId: 'drafts.post3', + publishedId: 'post3', + patch: { + set: {count: 1}, + }, + }, + } + + const action4: Action = { + delete: { + publishedId: 'post4', + includeDrafts: ['drafts.post4'], + purge: true, + }, + } + + const action5: Action = { + discard: { + draftId: 'drafts.post5', + purge: true, + }, + } + + const action6: Action = { + publish: { + draftId: 'drafts.post6', + ifDraftRevisionId: 'rev7', + publishedId: 'post6', + ifPublishedRevisionId: 'rev6', + }, + } + + const action7: Action = { + unpublish: { + draftId: 'drafts.post7', + publishedId: 'post7', + }, + } + + nock(projectHost()) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: TS is wrong here, it is not able to infer the correct type for + // edit action patch interface. + .post('/v1/data/actions/foo', { + actions: [ + {actionType: 'sanity.action.document.create', ...action1.create}, + {actionType: 'sanity.action.document.replaceDraft', ...action2.replaceDraft}, + {actionType: 'sanity.action.document.edit', ...action3.edit}, + {actionType: 'sanity.action.document.delete', ...action4.delete}, + {actionType: 'sanity.action.document.discard', ...action5.discard}, + {actionType: 'sanity.action.document.publish', ...action6.publish}, + {actionType: 'sanity.action.document.unpublish', ...action7.unpublish}, + ], + }) + .reply(200, { + transactionId: 'foo', + }) + + await expect( + getClient().action([action1, action2, action3, action4, action5, action6, action7]), + ).resolves.not.toThrow() + }) + + test.skipIf(isEdge)('action() accepts optional parameters', async () => { + const action: Action = { + create: { + publishedId: 'post1', + attributes: {_id: 'post1', _type: 'post'}, + ifExists: 'fail', + }, + } + + const options: BaseActionOptions = { + transactionId: 'txn1', + skipCrossDatasetReferenceValidation: true, + dryRun: true, + } + + nock(projectHost()) + .post('/v1/data/actions/foo', { + actions: [{actionType: 'sanity.action.document.create', ...action.create}], + transactionId: 'txn1', + skipCrossDatasetReferenceValidation: true, + dryRun: true, + }) + .reply(200, { + transactionId: 'txn1', + }) + + await expect(getClient().action(action, options)).resolves.not.toThrow() + }) + + test.skipIf(isEdge)('action() handles undefined optional parameters gracefully', async () => { + const action: Action = { + create: { + publishedId: 'post1', + attributes: {_id: 'post1', _type: 'post'}, + ifExists: 'fail', + }, + } + + const options: BaseActionOptions = { + transactionId: undefined, + skipCrossDatasetReferenceValidation: undefined, + dryRun: undefined, + } + + nock(projectHost()) + .post('/v1/data/actions/foo', { + actions: [{actionType: 'sanity.action.document.create', ...action.create}], + }) + .reply(200, { + transactionId: 'foo', + }) + + await expect(getClient().action(action, options)).resolves.not.toThrow() + }) + test.skipIf(isEdge)('uses GET for queries below limit', async () => { // Please dont ever do this. Just... don't. const clause: string[] = []