@bjoerge/mutiny

中文文档
0.7.1发布于 2024/08/02MITTypeScriptESM + CJS近 30 天下载 2万

Tiny toolkit for working with Sanity mutations in JavaScript & TypeScript

原始使用文档
文档目录11 节
  1. At a glance
  2. Features
  3. Usage Example
  4. Mutation creators
  5. Mutations
  6. Patches
  7. Patch Operations
  8. Advanced examples
  9. Apply mutations on local documents (experimental)
  10. Apply a patch mutation to a single document
  11. Differences from Sanity API

mutiny

Tiny toolkit for working with Sanity mutations in JavaScript & TypeScript

Disclaimer: This is work in progress, use at own risk!

At a glance

  • Declarative & composable mutation creators
  • Utilities for applying mutations on in-memory documents (experimental)
  • Local in-memory dataset replica with support for optimistic updates (experimental)

Features

  • Mutations can be declared using creator functions and passed around like any other values, transformed and composed into larger operations spanning multiple documents
  • Mutations are mere descriptions of operations and can be serialized to a compact json format or a Sanity mutation API request payload
  • Nodes can be addressed using paths as JavaScript values instead of string paths
  • Closely aligned with the Sanity.io mutation format
  • Supports automatically adding _key's to objects in arrays, so you don't have to.
  • Experimental support for applying mutations on in-memory documents
  • Great TypeScript support

Usage Example

import {
  at,
  create,
  createIfNotExists,
  patch,
  SanityEncoder,
  set,
  setIfMissing,
} from '@bjoerge/mutiny'

const mutations = [
  create({_type: 'dog', name: 'Fido'}),
  createIfNotExists({_id: 'document-1', _type: 'someType'}),
  createIfNotExists({_id: 'other-document', _type: 'author'}),
  patch('other-document', [
    at('published', set(true)),
    at('address', setIfMissing({_type: 'address'})),
    at('address.city', set('Oslo')),
  ]),
]

// get a projectId and dataset at sanity.io
const projectId = '<projectId>'
const dataset = '<dataset>'

// Submit mutations to the Sanity API
fetch(`https://${projectId}.api.sanity.io/v2023-08-01/data/mutate/${dataset}`, {
  method: 'POST',
  mode: 'cors',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(SanityEncoder.encode(mutations)),
})
  .then(response => response.json())
  .then(result => console.log(result))

Mutation creators

Mutations

  • create(document: SanityDocument): Create a new document
  • createIfNotExists(document: SanityDocument): Create a new document if it does not exist
  • createOrReplace(document: SanityDocument): Create a new document or replace an existing one
  • delete(documentId: SanityDocument): Delete a document (aliases: del, destroy)
  • patch(documentId: string, patches: Patch | Patch[], options?: {ifRevision?: string}): Patch a document. Can optionally provide a revisionId for optimistic locking. If the current document revision doesn't match the given revision the patch mutation will fail when applied.

Patches

A patch is a combination of a node path and an operation. The node path is a simplified JSONMatch path or an array of path segments that points to a specific node in the document. The operation is one of the operations described below.

  • at(path: Path | string, operation: Operation): Create a patch from a path and an operation

Examples:

at('foo.bar', set('baz'))

// equivalent to the above
at(['foo', 'bar'], set('baz'))

at(['array[0]'], insert('baz'))

// Set a value deep into an array of objects addressed by `_key`
at(['people', {_key: 'xyz'}, 'name'], set('Bjørge'))

// equivalent to the above, using a serialized path:
at('people[_key=="xyz"].name', set('Bjørge'))

Patch Operations

Patch operations applicable for all data types

  • set(value: any): Set the value of the node to the given value
  • setIfMissing(value: any): Set the value of the node to the given value if the node has no value
  • unset(): Remove the node from the document

Object operations

  • assign(value: object): Do a shallow merge of the node with the given value. If the node is an object, the value will be merged into the object similar to Object.assign(<currentValue>, value).
  • unassign(attributes: string[]): Remove the given attributes from the existing value.

Array operations

  • prepend(items: any[]): Prepend the given items to the beginning of the array
  • append(items: any[]): Append the given items to the end of the array
  • insert(items: any | any[], position: "before" | "after", referenceItem: number | {_key: string}): Insert the given items before or after the given before or after item. If before or after is not provided, the items will be inserted at the beginning or end of the array.
  • truncate(startIndex: number, endIndex?: number): Remove items from the array starting at startIndex and ending at endIndex. If endIndex is not provided, all items after startIndex will be removed.
  • replace(items: any | any[], referenceItem: number | {_key: string}): Replaces the referenceItem (addressed by index or _key) with the given item or items. If items is an array, referenceItem will be replaced by the items and any existing elements that comes after referenceItem will be shifted to the right.
  • upsert(items: any | any[], position: "before" | "after", referenceItem: number | {_key: string}): Upsert one or more items into the array. If the items match existing items in the array, the existing items will be replaced with the given items. If the items do not match any existing items, it will be inserted into the array. ThereferenceItem specifies a reference item to place missing items relative to. If. If not provided, any missing items will be inserted at the beginning or end of the array, depending on position. The positionoption can be used to specify where to insert the item if it does not match any existing items. If not provided, the item will be inserted at the end of the array.

Number operations

  • inc(value: number): Increment the number by the given value
  • dec(value: number): Decrement the number by the given value

String operations

  • diffMatchPatch(patch: string): Apply an incremental text patch to the current string. Read more about diffMatchPatch.

Advanced examples

Define a set of operations and turn it into a patch mutation that can be applied on a set of documents

const patches = [
  at("metadata", setIfMissing({})), // make sure metadata object exists
  at("metadata.published", set(true)),
  at("metadata.publishedAt", set(new Date().toISOString())),
]
const mutations = ["document-1", "document-2", "document-3"].map(id =>
  patch(id, patches),
)

// commit mutations to datastore
commitMutations(mutations)

Apply mutations on local documents (experimental)

Mutations can be applied to an in-memory collection of documents

import {applyInCollection} from '@bjoerge/mutiny/_unstable_apply'
import {createIfNotExists, del} from '@bjoerge/mutiny'

const initial = [{_id: 'deleteme', _type: 'foo'}]

const updated = applyInCollection(initial, [
  createIfNotExists({_id: 'mydocument', _type: 'foo'}),
  createIfNotExists({_id: 'anotherDocument', _type: 'foo'}),
  del('deleteme'),
])

console.log(updated)
/*=>
[
  { _id: 'mydocument', _type: 'foo' },
  { _id: 'anotherDocument', _type: 'foo' }
]
*/

Note: when applying mutations on a collection, referential integrity is preserved. This means that if a mutation is effectively a noop (e.g. nothing actually changed), the same object reference will be returned.

import {applyInCollection} from '@bjoerge/mutiny/_unstable_apply'
import {at, createIfNotExists, patch, set} from '@bjoerge/mutiny'

const initial = [
  {
    _id: 'someDoc',
    _type: 'foo',
    value: 'ok',
    nested: {value: 'something'},
    otherNested: {message: 'something else'},
  },
]

const updated = applyInCollection(initial, [
  createIfNotExists({_id: 'someDoc', _type: 'foo'}),
  patch('someDoc', [at('value', set('ok'))]),
  patch('someDoc', [at('nested.value', set('something'))]),
])

// the mutation didn't cause anything to change
console.log(initial === updated)
//=> true

This is also the case for nodes unaffected by the mutations:

import {applyInCollection} from '@bjoerge/mutiny/_unstable_apply'
import {at, createIfNotExists, patch, set} from '@bjoerge/mutiny'

const initial = [
  {
    _id: 'someDoc',
    _type: 'foo',
    value: 'ok',
    nested: {value: 'something'},
    otherNested: {message: 'something else'},
  },
]

const updated = applyInCollection(initial, [
  createIfNotExists({_id: 'someDoc', _type: 'foo'}),
  patch('someDoc', [at('value', set('ok'))]),
  patch('someDoc', [at('nested.value', set('something'))]),
  patch('someDoc', [at('otherNested.message', set('hello'))]),
])

// the `nested` object unaffected by the mutation
console.log(initial[0].nested === updated[0].nested)
//=> true

Apply a patch mutation to a single document

Alternatively, a patch mutation can be applied to a single document as long as its id matches the document id of the mutation:

import {applyPatchMutation} from '@bjoerge/mutiny/_unstable_apply'
import {at, insert, patch, setIfMissing} from '@bjoerge/mutiny'

const document = {_id: 'test', _type: 'foo'}

const updated = applyPatchMutation(
  document,
  patch('test', [
    at('title', setIfMissing('Foo')),
    at('cities', setIfMissing([])),
    at('cities', insert(['Oslo', 'San Francisco'], 'after', 0)),
  ]),
)

console.log(updated)
/*=>
{
  _id: 'test',
  _type: 'foo',
  title: 'Foo',
  cities: [ 'Oslo', 'San Francisco' ]
}
*/

Differences from Sanity API

To better align with a strict type system, mutiny differs slightly from the Sanity API when applying patches. Although all the mutation types you can express with mutiny can also be expressed as Sanity API mutations, the inverse is not necessarily true; The Sanity API (e.g. a listener) may produce patches that can't be represented in mutiny without an extra conversion step that takes the current document into account. In addition, applying a patch in mutiny behaves differently from applying the same patch using the Sanity API on a few accounts:

  • set andsetIfMissing does not create intermediate empty objects - Using the Sanity API, set and setIfMissing will create intermediate empty objects if any object along the given path doesn't already exist. In mutiny, these patches will only apply to already existing objects.
  • Limited json match support. Sanity mutations supports a powerful path selection syntax for targeting multiple document nodes at once with json-match. To keep things simple, a mutiny patch can only target a single document node.

版本与发布记录

@bjoerge/mutiny 共发布 22 个版本,首次发布于 2023/08/21;过去 12 个月发布了 0 个版本。

版本发布日期距今
0.7.1latest2024/08/022 年前
0.7.02024/08/022 年前
0.6.02024/07/242 年前
0.6.0-beta.4cl-store2024/03/052 年前
0.6.0-beta.32024/03/052 年前
0.6.0-beta.22024/03/042 年前

关于 @bjoerge/mutiny 的常见问题

@bjoerge/mutiny 是什么?

Tiny toolkit for working with Sanity mutations in JavaScript & TypeScript

如何安装 @bjoerge/mutiny?

在项目目录运行 npm install @bjoerge/mutiny,或使用 pnpm add / yarn add / bun add。

@bjoerge/mutiny 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bjoerge/mutiny 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@bjoerge/mutiny 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bjoerge/mutiny 可以商用吗?

@bjoerge/mutiny 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@bjoerge/mutiny 的下载量有多少?

根据 npm 官方下载统计,@bjoerge/mutiny 在 2026/10/08 – 2026/11/06 这 30 天内被下载 19,886 次,日均约 663 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bjoerge/mutiny 还在维护吗?

最新版本 0.7.1 发布于 2024/08/02(2 年前);至今共发布 22 个版本,其中过去 12 个月发布了 0 个;首次发布于 2023/08/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。