Skip to content

Commit

Permalink
feat: add support for array remove operation (#19)
Browse files Browse the repository at this point in the history
* feat: add support for array remove operation

* fix: improve typings for remove operation
  • Loading branch information
bjoerge authored Sep 2, 2024
1 parent 48a6b55 commit 17e7a3c
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 2 deletions.
3 changes: 3 additions & 0 deletions examples/ts/apply-patch-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
insertBefore,
patch,
prepend,
remove,
setIfMissing,
unassign,
unset,
Expand All @@ -17,6 +18,7 @@ const document = {
_id: 'test',
_type: 'foo',
unsetme: 'yes',
someArray: [{_key: 'foo'}, {_key: 'bar'}],
unassignme: 'please',
assigned: {existing: 'prop'},
} as const
Expand All @@ -30,6 +32,7 @@ const patches = patch('test', [
at('cities', insertAfter(['Chicago'], 1)),
at('cities', insertBefore(['Raleigh'], 3)),
at('unsetme', unset()),
at('someArray', remove({_key: 'foo'})),
at([], unassign(['unassignme'])),
at('hmmm', assign({other: 'value'})),
])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ function FormatOp(props: {op: Operation}) {
</Text>
)
}
if (op.type === 'remove') {
return (
<Text size={1} weight="semibold">
{op.type} ({formatReferenceItem(op.referenceItem)}))
</Text>
)
}
// @ts-expect-error all cases are covered
throw new Error(`Invalid operation type: ${op.type}`)
}
36 changes: 35 additions & 1 deletion src/apply/patch/__test__/array.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {expect, test} from 'vitest'

import {insert, replace} from '../../../mutations/operations/creators'
import {insert, remove, replace} from '../../../mutations/operations/creators'
import {applyOp} from '../applyOp'

test('replace on item', () => {
Expand Down Expand Up @@ -151,3 +151,37 @@ test('insert relative to nonexisting keyed path elements', () => {
applyOp(insert(['INSERT!'], 'after', {_key: 'foo'}), []),
).toThrow()
})

test('remove item at key', () => {
expect(
applyOp(remove({_key: 'foo'}), [
{_key: 'one'},
{_key: 'foo'},
{_key: 'two'},
]),
).toEqual([{_key: 'one'}, {_key: 'two'}])

expect(
applyOp(remove({_key: 'foo'}), [{_key: 'foo'}, {_key: 'two'}]),
).toEqual([{_key: 'two'}])
expect(
applyOp(remove({_key: 'foo'}), [{_key: 'one'}, {_key: 'foo'}]),
).toEqual([{_key: 'one'}])

expect(applyOp(remove({_key: 'foo'}), [{_key: 'foo'}])).toEqual([])
})

test('remove item at index', () => {
expect(
applyOp(remove(1), [{_key: 'one'}, {_key: 'foo'}, {_key: 'two'}]),
).toEqual([{_key: 'one'}, {_key: 'two'}])

expect(applyOp(remove(0), [{_key: 'foo'}, {_key: 'two'}])).toEqual([
{_key: 'two'},
])
expect(applyOp(remove(1), [{_key: 'one'}, {_key: 'foo'}])).toEqual([
{_key: 'one'},
])

expect(applyOp(remove(0), [{_key: 'foo'}])).toEqual([])
})
15 changes: 15 additions & 0 deletions src/apply/patch/operations/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
type InsertOp,
type KeyedPathElement,
type RelativePosition,
type RemoveOp,
type ReplaceOp,
type TruncateOp,
type UpsertOp,
Expand Down Expand Up @@ -87,6 +88,20 @@ export function replace<
}
return splice(currentValue, index, op.items.length, op.items)
}
export function remove<
O extends RemoveOp<number | KeyedPathElement>,
CurrentValue extends unknown[],
>(op: O, currentValue: CurrentValue) {
if (!Array.isArray(currentValue)) {
throw new TypeError('Cannot apply "remove()" on non-array value')
}

const index = findTargetIndex(currentValue, op.referenceItem)
if (index === null) {
throw new Error(`Found no matching array element to replace`)
}
return splice(currentValue, index, 1, [])
}

export function truncate<O extends TruncateOp, CurrentValue extends unknown[]>(
op: O,
Expand Down
48 changes: 47 additions & 1 deletion src/apply/patch/typings/applyOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type InsertOp,
type KeyedPathElement,
type Operation,
type RemoveOp,
type ReplaceOp,
type SetIfMissingOp,
type SetOp,
Expand Down Expand Up @@ -88,6 +89,36 @@ export type InsertAtIndex<
NormalizeIndex<Index, ArrayLength<Current>>
>

export type DropFirst<Array extends unknown[]> = Array extends [
infer Head,
...infer Rest,
]
? Rest
: []

export type _RemoveAtIndex<Current extends unknown[], Index extends number> =
Between<Index, 0, ArrayLength<Current>> extends true
? Call<Tuples.SplitAt<Index>, Current> extends [infer Head, infer Tail]
? Head extends AnyArray
? Tail extends AnyArray
? [
...(Head extends never[] ? [] : Head),
...(Tail extends never[]
? []
: Tail extends unknown[]
? DropFirst<Tail>
: Tail),
]
: never
: never
: never
: Current

export type RemoveAtIndex<
Current extends unknown[],
Index extends number,
> = _RemoveAtIndex<Current, NormalizeIndex<Index, ArrayLength<Current>>>

export type ArrayInsert<
Current extends unknown[],
Items extends unknown[],
Expand All @@ -101,6 +132,16 @@ export type ArrayInsert<
: (E | ArrayElement<Items>)[]
: Current

export type ArrayRemove<
Current extends unknown[],
Ref extends number | KeyedPathElement,
> = number extends Ref
? Current
: Ref extends number
? RemoveAtIndex<Current, Ref>
: // todo: look up index of item with _key
Current

export type Assign<Current, Attrs> = {
[K in keyof Attrs | keyof Current]: K extends keyof Attrs
? Attrs[K]
Expand Down Expand Up @@ -155,4 +196,9 @@ export type ApplyOp<O extends Operation, Current> = Current extends never
}
: O extends DiffMatchPatchOp
? string
: never
: O extends RemoveOp<infer Ref>
? Current extends AnyArray<unknown>
? ArrayRemove<NormalizeReadOnlyArray<Current>, Ref>
: Current
: // fallback
Current
3 changes: 3 additions & 0 deletions src/encoders/compact/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ function encodePatchMutation(
if (op.type === 'truncate') {
return ['patch', 'truncate', id, path, [op.startIndex, op.endIndex]]
}
if (op.type === 'remove') {
return ['patch', 'remove', id, path, [encodeItemRef(op.referenceItem)]]
}
// @ts-expect-error all cases are covered
throw new Error(`Invalid operation type: ${op.type}`)
}
Expand Down
9 changes: 9 additions & 0 deletions src/encoders/compact/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ export type ReplaceMutation = [
[ItemRef, AnyArray],
RevisionLock?,
]
export type RemoveMutation = [
'patch',
'remove',
Id,
CompactPath,
[ItemRef],
RevisionLock?,
]
export type SetMutation = ['patch', 'set', Id, CompactPath, any, RevisionLock?]
export type SetIfMissingMutation = [
'patch',
Expand Down Expand Up @@ -118,6 +126,7 @@ export type CompactPatchMutation =
| AssignMutation
| UnassignMutation
| ReplaceMutation
| RemoveMutation

export type CompactMutation<Doc> =
| DeleteMutation
Expand Down
5 changes: 5 additions & 0 deletions src/encoders/sanity/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ function patchToSanity(patch: NodePatch) {
},
}
}
if (op.type === 'remove') {
return {
unset: [stringifyPath(path.concat(op.referenceItem))],
}
}
//@ts-expect-error all cases should be covered
throw new Error(`Unknown operation type ${op.type}`)
}
3 changes: 3 additions & 0 deletions src/formatters/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ function formatPatchMutation(patch: NodePatch<any>): string {
if (op.type === 'truncate') {
return [path, `truncate(${op.startIndex}, ${op.endIndex}`].join(': ')
}
if (op.type === 'remove') {
return [path, `remove(${encodeItemRef(op.referenceItem)})`].join(': ')
}
// @ts-expect-error all cases are covered
throw new Error(`Invalid operation type: ${op.type}`)
}
13 changes: 13 additions & 0 deletions src/mutations/operations/creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type InsertOp,
type KeyedPathElement,
type RelativePosition,
type RemoveOp,
type ReplaceOp,
type SetIfMissingOp,
type SetOp,
Expand Down Expand Up @@ -135,6 +136,18 @@ export function replace<
}
}

/*
Remove an item from an array by either key or index
*/
export function remove<ReferenceItem extends Index | KeyedPathElement>(
referenceItem: ReferenceItem,
): RemoveOp<ReferenceItem> {
return {
type: 'remove',
referenceItem,
}
}

/*
use this when the reference Items may or may not exist
*/
Expand Down
7 changes: 7 additions & 0 deletions src/mutations/operations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export type TruncateOp = {
startIndex: number
endIndex?: number
}

export type RemoveOp<ReferenceItem extends Index | KeyedPathElement> = {
type: 'remove'
referenceItem: ReferenceItem
}

export type ReplaceOp<
Items extends AnyArray,
ReferenceItem extends Index | KeyedPathElement,
Expand Down Expand Up @@ -90,5 +96,6 @@ export type ArrayOp =
| UpsertOp<AnyArray, RelativePosition, Index | KeyedPathElement>
| ReplaceOp<AnyArray, Index | KeyedPathElement>
| TruncateOp
| RemoveOp<Index | KeyedPathElement>

export type PrimitiveOp = AnyOp | StringOp | NumberOp

0 comments on commit 17e7a3c

Please sign in to comment.