@loro-extended/change 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -31
- package/dist/index.d.ts +115 -53
- package/dist/index.js +743 -393
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/change.test.ts +97 -403
- package/src/derive-placeholder.test.ts +245 -0
- package/src/derive-placeholder.ts +132 -0
- package/src/discriminated-union.test.ts +74 -1
- package/src/draft-nodes/base.ts +9 -4
- package/src/draft-nodes/counter.md +31 -0
- package/src/draft-nodes/counter.test.ts +53 -0
- package/src/draft-nodes/doc.ts +27 -6
- package/src/draft-nodes/list-base.ts +24 -3
- package/src/draft-nodes/list.test.ts +43 -0
- package/src/draft-nodes/list.ts +3 -0
- package/src/draft-nodes/map.ts +57 -16
- package/src/draft-nodes/movable-list.test.ts +27 -0
- package/src/draft-nodes/movable-list.ts +5 -0
- package/src/draft-nodes/proxy-handlers.ts +87 -0
- package/src/{record.test.ts → draft-nodes/record.test.ts} +98 -18
- package/src/draft-nodes/record.ts +42 -80
- package/src/draft-nodes/utils.ts +46 -5
- package/src/equality.test.ts +19 -0
- package/src/index.ts +14 -7
- package/src/json-patch.test.ts +33 -167
- package/src/overlay.ts +46 -39
- package/src/readonly.test.ts +92 -0
- package/src/shape.ts +131 -73
- package/src/{change.ts → typed-doc.ts} +50 -28
- package/src/types.test.ts +188 -0
- package/src/types.ts +37 -5
- package/src/utils/type-guards.ts +1 -0
- package/src/validation.ts +45 -12
|
@@ -84,7 +84,7 @@ export abstract class ListDraftNodeBase<
|
|
|
84
84
|
): DraftNodeParams<ContainerShape> {
|
|
85
85
|
return {
|
|
86
86
|
shape,
|
|
87
|
-
|
|
87
|
+
placeholder: undefined, // List items don't have placeholder
|
|
88
88
|
getContainer: () => {
|
|
89
89
|
const containerItem = this.container.get(index)
|
|
90
90
|
if (!containerItem || !isContainer(containerItem)) {
|
|
@@ -92,6 +92,7 @@ export abstract class ListDraftNodeBase<
|
|
|
92
92
|
}
|
|
93
93
|
return containerItem
|
|
94
94
|
},
|
|
95
|
+
readonly: this.readonly,
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -142,7 +143,7 @@ export abstract class ListDraftNodeBase<
|
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
// Get item for return values - returns DraftItem that can be mutated
|
|
145
|
-
protected getDraftItem(index: number):
|
|
146
|
+
protected getDraftItem(index: number): any {
|
|
146
147
|
// Check if we already have a cached item for this index
|
|
147
148
|
let cachedItem = this.itemCache.get(index)
|
|
148
149
|
if (cachedItem) {
|
|
@@ -167,14 +168,29 @@ export abstract class ListDraftNodeBase<
|
|
|
167
168
|
// For primitives, just use the value directly
|
|
168
169
|
cachedItem = containerItem
|
|
169
170
|
}
|
|
170
|
-
|
|
171
|
+
// Only cache primitive values if NOT readonly
|
|
172
|
+
if (!this.readonly) {
|
|
173
|
+
this.itemCache.set(index, cachedItem)
|
|
174
|
+
}
|
|
171
175
|
return cachedItem as DraftItem
|
|
172
176
|
} else {
|
|
173
177
|
// For container shapes, create a proper draft node using the new pattern
|
|
174
178
|
cachedItem = createContainerDraftNode(
|
|
175
179
|
this.getDraftNodeParams(index, this.shape.shape as ContainerShape),
|
|
176
180
|
)
|
|
181
|
+
// Cache container nodes
|
|
177
182
|
this.itemCache.set(index, cachedItem)
|
|
183
|
+
|
|
184
|
+
if (this.readonly) {
|
|
185
|
+
const shape = this.shape.shape as ContainerShape
|
|
186
|
+
if (shape._type === "counter") {
|
|
187
|
+
return (cachedItem as any).value
|
|
188
|
+
}
|
|
189
|
+
if (shape._type === "text") {
|
|
190
|
+
return (cachedItem as any).toString()
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
178
194
|
return cachedItem as DraftItem
|
|
179
195
|
}
|
|
180
196
|
}
|
|
@@ -254,26 +270,31 @@ export abstract class ListDraftNodeBase<
|
|
|
254
270
|
}
|
|
255
271
|
|
|
256
272
|
insert(index: number, item: Item): void {
|
|
273
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
257
274
|
// Update cache indices before performing the insert operation
|
|
258
275
|
this.updateCacheForInsert(index)
|
|
259
276
|
this.insertWithConversion(index, item)
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
delete(index: number, len: number): void {
|
|
280
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
263
281
|
// Update cache indices before performing the delete operation
|
|
264
282
|
this.updateCacheForDelete(index, len)
|
|
265
283
|
this.container.delete(index, len)
|
|
266
284
|
}
|
|
267
285
|
|
|
268
286
|
push(item: Item): void {
|
|
287
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
269
288
|
this.pushWithConversion(item)
|
|
270
289
|
}
|
|
271
290
|
|
|
272
291
|
pushContainer(container: Container): Container {
|
|
292
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
273
293
|
return this.container.pushContainer(container)
|
|
274
294
|
}
|
|
275
295
|
|
|
276
296
|
insertContainer(index: number, container: Container): Container {
|
|
297
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
277
298
|
return this.container.insertContainer(index, container)
|
|
278
299
|
}
|
|
279
300
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { createTypedDoc, Shape } from "../index.js"
|
|
3
|
+
|
|
4
|
+
describe("ListDraftNode", () => {
|
|
5
|
+
describe("set via index", () => {
|
|
6
|
+
it("should allow setting a plain object for a list item via index", () => {
|
|
7
|
+
const schema = Shape.doc({
|
|
8
|
+
users: Shape.list(
|
|
9
|
+
Shape.map({
|
|
10
|
+
name: Shape.plain.string(),
|
|
11
|
+
}),
|
|
12
|
+
),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const doc = createTypedDoc(schema)
|
|
16
|
+
|
|
17
|
+
doc.change(draft => {
|
|
18
|
+
draft.users.push({ name: "Alice" })
|
|
19
|
+
|
|
20
|
+
// Update via index
|
|
21
|
+
draft.users[0] = { name: "Bob" }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(doc.toJSON().users[0]).toEqual({ name: "Bob" })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("should allow setting a primitive value via index", () => {
|
|
28
|
+
const schema = Shape.doc({
|
|
29
|
+
tags: Shape.list(Shape.plain.string()),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const doc = createTypedDoc(schema)
|
|
33
|
+
|
|
34
|
+
doc.change(draft => {
|
|
35
|
+
draft.tags.push("a")
|
|
36
|
+
draft.tags.push("b")
|
|
37
|
+
draft.tags[1] = "c"
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(doc.toJSON().tags).toEqual(["a", "c"])
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
})
|
package/src/draft-nodes/list.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { LoroList } from "loro-crdt"
|
|
2
2
|
import type { ContainerOrValueShape } from "../shape.js"
|
|
3
|
+
import type { Infer } from "../types.js"
|
|
3
4
|
import { ListDraftNodeBase } from "./list-base.js"
|
|
4
5
|
|
|
5
6
|
// List draft node
|
|
6
7
|
export class ListDraftNode<
|
|
7
8
|
NestedShape extends ContainerOrValueShape,
|
|
8
9
|
> extends ListDraftNodeBase<NestedShape> {
|
|
10
|
+
[index: number]: Infer<NestedShape>
|
|
11
|
+
|
|
9
12
|
protected get container(): LoroList {
|
|
10
13
|
return super.container as LoroList
|
|
11
14
|
}
|
package/src/draft-nodes/map.ts
CHANGED
|
@@ -16,7 +16,10 @@ import type {
|
|
|
16
16
|
} from "../shape.js"
|
|
17
17
|
import { isContainerShape, isValueShape } from "../utils/type-guards.js"
|
|
18
18
|
import { DraftNode, type DraftNodeParams } from "./base.js"
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
assignPlainValueToDraftNode,
|
|
21
|
+
createContainerDraftNode,
|
|
22
|
+
} from "./utils.js"
|
|
20
23
|
|
|
21
24
|
const containerConstructor = {
|
|
22
25
|
counter: LoroCounter,
|
|
@@ -64,42 +67,66 @@ export class MapDraftNode<
|
|
|
64
67
|
key: string,
|
|
65
68
|
shape: S,
|
|
66
69
|
): DraftNodeParams<ContainerShape> {
|
|
67
|
-
const
|
|
70
|
+
const placeholder = (this.placeholder as any)?.[key]
|
|
68
71
|
|
|
69
72
|
const LoroContainer = containerConstructor[shape._type]
|
|
70
73
|
|
|
71
74
|
return {
|
|
72
75
|
shape,
|
|
73
|
-
|
|
76
|
+
placeholder,
|
|
74
77
|
getContainer: () =>
|
|
75
78
|
this.container.getOrCreateContainer(key, new (LoroContainer as any)()),
|
|
79
|
+
readonly: this.readonly,
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
getOrCreateNode<Shape extends ContainerShape | ValueShape>(
|
|
80
84
|
key: string,
|
|
81
85
|
shape: Shape,
|
|
82
|
-
):
|
|
86
|
+
): any {
|
|
83
87
|
let node = this.propertyCache.get(key)
|
|
84
88
|
if (!node) {
|
|
85
89
|
if (isContainerShape(shape)) {
|
|
86
90
|
node = createContainerDraftNode(this.getDraftNodeParams(key, shape))
|
|
91
|
+
// We cache container nodes even in readonly mode because they are just handles
|
|
92
|
+
this.propertyCache.set(key, node)
|
|
87
93
|
} else {
|
|
88
94
|
// For value shapes, first try to get the value from the container
|
|
89
95
|
const containerValue = this.container.get(key)
|
|
90
96
|
if (containerValue !== undefined) {
|
|
91
97
|
node = containerValue as Value
|
|
92
98
|
} else {
|
|
93
|
-
// Only fall back to
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
96
|
-
throw new Error("
|
|
99
|
+
// Only fall back to placeholder if the container doesn't have the value
|
|
100
|
+
const placeholder = (this.placeholder as any)?.[key]
|
|
101
|
+
if (placeholder === undefined) {
|
|
102
|
+
throw new Error("placeholder required")
|
|
97
103
|
}
|
|
98
|
-
node =
|
|
104
|
+
node = placeholder as Value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// In readonly mode, we DO NOT cache primitive values.
|
|
108
|
+
// This ensures we always get the latest value from the CRDT on next access.
|
|
109
|
+
if (!this.readonly) {
|
|
110
|
+
this.propertyCache.set(key, node)
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
if (node === undefined) throw new Error("no container made")
|
|
102
|
-
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this.readonly && isContainerShape(shape)) {
|
|
117
|
+
// In readonly mode, if the container doesn't exist, return the placeholder
|
|
118
|
+
// This ensures we respect default values (e.g. counter: 1)
|
|
119
|
+
const existing = this.container.get(key)
|
|
120
|
+
if (existing === undefined) {
|
|
121
|
+
return (this.placeholder as any)?.[key]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (shape._type === "counter") {
|
|
125
|
+
return (node as any).value
|
|
126
|
+
}
|
|
127
|
+
if (shape._type === "text") {
|
|
128
|
+
return (node as any).toString()
|
|
129
|
+
}
|
|
103
130
|
}
|
|
104
131
|
|
|
105
132
|
return node as Shape extends ContainerShape ? DraftNode<Shape> : Value
|
|
@@ -110,13 +137,24 @@ export class MapDraftNode<
|
|
|
110
137
|
const shape = this.shape.shapes[key]
|
|
111
138
|
Object.defineProperty(this, key, {
|
|
112
139
|
get: () => this.getOrCreateNode(key, shape),
|
|
113
|
-
set:
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
set: value => {
|
|
141
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
142
|
+
if (isValueShape(shape)) {
|
|
143
|
+
this.container.set(key, value)
|
|
144
|
+
this.propertyCache.set(key, value)
|
|
145
|
+
} else {
|
|
146
|
+
if (value && typeof value === "object") {
|
|
147
|
+
const node = this.getOrCreateNode(key, shape)
|
|
148
|
+
|
|
149
|
+
if (assignPlainValueToDraftNode(node as DraftNode<any>, value)) {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
118
152
|
}
|
|
119
|
-
|
|
153
|
+
throw new Error(
|
|
154
|
+
"Cannot set container directly, modify the draft node instead",
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
120
158
|
})
|
|
121
159
|
}
|
|
122
160
|
}
|
|
@@ -127,14 +165,17 @@ export class MapDraftNode<
|
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
set(key: string, value: Value): void {
|
|
168
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
130
169
|
this.container.set(key, value)
|
|
131
170
|
}
|
|
132
171
|
|
|
133
172
|
setContainer<C extends Container>(key: string, container: C): C {
|
|
173
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
134
174
|
return this.container.setContainer(key, container)
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
delete(key: string): void {
|
|
178
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
138
179
|
this.container.delete(key)
|
|
139
180
|
}
|
|
140
181
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { createTypedDoc, Shape } from "../index.js"
|
|
3
|
+
|
|
4
|
+
describe("MovableListDraftNode", () => {
|
|
5
|
+
describe("set via index", () => {
|
|
6
|
+
it("should allow setting a plain object for a list item via index", () => {
|
|
7
|
+
const schema = Shape.doc({
|
|
8
|
+
users: Shape.movableList(
|
|
9
|
+
Shape.map({
|
|
10
|
+
name: Shape.plain.string(),
|
|
11
|
+
}),
|
|
12
|
+
),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const doc = createTypedDoc(schema)
|
|
16
|
+
|
|
17
|
+
doc.change(draft => {
|
|
18
|
+
draft.users.push({ name: "Alice" })
|
|
19
|
+
|
|
20
|
+
// Update via index
|
|
21
|
+
draft.users[0] = { name: "Bob" }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(doc.toJSON().users[0]).toEqual({ name: "Bob" })
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Container, LoroMovableList } from "loro-crdt"
|
|
2
2
|
import type { ContainerOrValueShape } from "../shape.js"
|
|
3
|
+
import type { Infer } from "../types.js"
|
|
3
4
|
import { ListDraftNodeBase } from "./list-base.js"
|
|
4
5
|
|
|
5
6
|
// Movable list draft node
|
|
@@ -7,6 +8,8 @@ export class MovableListDraftNode<
|
|
|
7
8
|
NestedShape extends ContainerOrValueShape,
|
|
8
9
|
Item = NestedShape["_plain"],
|
|
9
10
|
> extends ListDraftNodeBase<NestedShape> {
|
|
11
|
+
[index: number]: Infer<NestedShape>
|
|
12
|
+
|
|
10
13
|
protected get container(): LoroMovableList {
|
|
11
14
|
return super.container as LoroMovableList
|
|
12
15
|
}
|
|
@@ -17,10 +20,12 @@ export class MovableListDraftNode<
|
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
move(from: number, to: number): void {
|
|
23
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
20
24
|
this.container.move(from, to)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
set(index: number, item: Exclude<Item, Container>) {
|
|
28
|
+
if (this.readonly) throw new Error("Cannot modify readonly doc")
|
|
24
29
|
return this.container.set(index, item)
|
|
25
30
|
}
|
|
26
31
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ListDraftNode } from "./list.js"
|
|
2
|
+
import type { MovableListDraftNode } from "./movable-list.js"
|
|
3
|
+
import type { RecordDraftNode } from "./record.js"
|
|
4
|
+
|
|
5
|
+
export const recordProxyHandler: ProxyHandler<RecordDraftNode<any>> = {
|
|
6
|
+
get: (target, prop) => {
|
|
7
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
8
|
+
return target.get(prop)
|
|
9
|
+
}
|
|
10
|
+
return Reflect.get(target, prop)
|
|
11
|
+
},
|
|
12
|
+
set: (target, prop, value) => {
|
|
13
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
14
|
+
target.set(prop, value)
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
return Reflect.set(target, prop, value)
|
|
18
|
+
},
|
|
19
|
+
deleteProperty: (target, prop) => {
|
|
20
|
+
if (typeof prop === "string" && !(prop in target)) {
|
|
21
|
+
target.delete(prop)
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
return Reflect.deleteProperty(target, prop)
|
|
25
|
+
},
|
|
26
|
+
ownKeys: target => {
|
|
27
|
+
return target.keys()
|
|
28
|
+
},
|
|
29
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
30
|
+
if (typeof prop === "string" && target.has(prop)) {
|
|
31
|
+
return {
|
|
32
|
+
configurable: true,
|
|
33
|
+
enumerable: true,
|
|
34
|
+
value: target.get(prop),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return Reflect.getOwnPropertyDescriptor(target, prop)
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const listProxyHandler: ProxyHandler<ListDraftNode<any>> = {
|
|
42
|
+
get: (target, prop) => {
|
|
43
|
+
if (typeof prop === "string") {
|
|
44
|
+
const index = Number(prop)
|
|
45
|
+
if (!Number.isNaN(index)) {
|
|
46
|
+
return target.get(index)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Reflect.get(target, prop)
|
|
50
|
+
},
|
|
51
|
+
set: (target, prop, value) => {
|
|
52
|
+
if (typeof prop === "string") {
|
|
53
|
+
const index = Number(prop)
|
|
54
|
+
if (!Number.isNaN(index)) {
|
|
55
|
+
// For lists, assignment to index implies replacement
|
|
56
|
+
target.delete(index, 1)
|
|
57
|
+
target.insert(index, value)
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return Reflect.set(target, prop, value)
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const movableListProxyHandler: ProxyHandler<MovableListDraftNode<any>> =
|
|
66
|
+
{
|
|
67
|
+
get: (target, prop) => {
|
|
68
|
+
if (typeof prop === "string") {
|
|
69
|
+
const index = Number(prop)
|
|
70
|
+
if (!Number.isNaN(index)) {
|
|
71
|
+
return target.get(index)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Reflect.get(target, prop)
|
|
75
|
+
},
|
|
76
|
+
set: (target, prop, value) => {
|
|
77
|
+
if (typeof prop === "string") {
|
|
78
|
+
const index = Number(prop)
|
|
79
|
+
if (!Number.isNaN(index)) {
|
|
80
|
+
// MovableList supports set directly
|
|
81
|
+
target.set(index, value)
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return Reflect.set(target, prop, value)
|
|
86
|
+
},
|
|
87
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
|
-
import { Shape, TypedDoc } from "
|
|
2
|
+
import { Shape, TypedDoc } from "../index.js"
|
|
3
3
|
|
|
4
4
|
describe("Record Types", () => {
|
|
5
5
|
describe("Shape.record (Container)", () => {
|
|
@@ -8,14 +8,14 @@ describe("Record Types", () => {
|
|
|
8
8
|
scores: Shape.record(Shape.counter()),
|
|
9
9
|
})
|
|
10
10
|
|
|
11
|
-
const doc = new TypedDoc(schema
|
|
11
|
+
const doc = new TypedDoc(schema)
|
|
12
12
|
|
|
13
13
|
doc.change(draft => {
|
|
14
14
|
draft.scores.getOrCreateNode("alice").increment(10)
|
|
15
15
|
draft.scores.getOrCreateNode("bob").increment(5)
|
|
16
16
|
})
|
|
17
17
|
|
|
18
|
-
expect(doc.
|
|
18
|
+
expect(doc.toJSON().scores).toEqual({
|
|
19
19
|
alice: 10,
|
|
20
20
|
bob: 5,
|
|
21
21
|
})
|
|
@@ -25,7 +25,7 @@ describe("Record Types", () => {
|
|
|
25
25
|
draft.scores.delete("bob")
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
expect(doc.
|
|
28
|
+
expect(doc.toJSON().scores).toEqual({
|
|
29
29
|
alice: 15,
|
|
30
30
|
})
|
|
31
31
|
})
|
|
@@ -35,14 +35,14 @@ describe("Record Types", () => {
|
|
|
35
35
|
notes: Shape.record(Shape.text()),
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
const doc = new TypedDoc(schema
|
|
38
|
+
const doc = new TypedDoc(schema)
|
|
39
39
|
|
|
40
40
|
doc.change(draft => {
|
|
41
41
|
draft.notes.getOrCreateNode("todo").insert(0, "Buy milk")
|
|
42
42
|
draft.notes.getOrCreateNode("reminders").insert(0, "Call mom")
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
expect(doc.
|
|
45
|
+
expect(doc.toJSON().notes).toEqual({
|
|
46
46
|
todo: "Buy milk",
|
|
47
47
|
reminders: "Call mom",
|
|
48
48
|
})
|
|
@@ -53,7 +53,7 @@ describe("Record Types", () => {
|
|
|
53
53
|
groups: Shape.record(Shape.list(Shape.plain.string())),
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
const doc = new TypedDoc(schema
|
|
56
|
+
const doc = new TypedDoc(schema)
|
|
57
57
|
|
|
58
58
|
doc.change(draft => {
|
|
59
59
|
const groupA = draft.groups.getOrCreateNode("groupA")
|
|
@@ -64,7 +64,7 @@ describe("Record Types", () => {
|
|
|
64
64
|
groupB.push("charlie")
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
-
expect(doc.
|
|
67
|
+
expect(doc.toJSON().groups).toEqual({
|
|
68
68
|
groupA: ["alice", "bob"],
|
|
69
69
|
groupB: ["charlie"],
|
|
70
70
|
})
|
|
@@ -79,14 +79,14 @@ describe("Record Types", () => {
|
|
|
79
79
|
}),
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
const doc = new TypedDoc(schema
|
|
82
|
+
const doc = new TypedDoc(schema)
|
|
83
83
|
|
|
84
84
|
doc.change(draft => {
|
|
85
85
|
draft.wrapper.config.theme = "dark"
|
|
86
86
|
draft.wrapper.config.lang = "en"
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
-
expect(doc.
|
|
89
|
+
expect(doc.toJSON().wrapper.config).toEqual({
|
|
90
90
|
theme: "dark",
|
|
91
91
|
lang: "en",
|
|
92
92
|
})
|
|
@@ -96,7 +96,7 @@ describe("Record Types", () => {
|
|
|
96
96
|
draft.wrapper.config.lang = "fr"
|
|
97
97
|
})
|
|
98
98
|
|
|
99
|
-
expect(doc.
|
|
99
|
+
expect(doc.toJSON().wrapper.config).toEqual({
|
|
100
100
|
lang: "fr",
|
|
101
101
|
})
|
|
102
102
|
})
|
|
@@ -108,15 +108,14 @@ describe("Record Types", () => {
|
|
|
108
108
|
}),
|
|
109
109
|
})
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
const doc = new TypedDoc(schema, { wrapper: { stats: {} } })
|
|
111
|
+
const doc = new TypedDoc(schema)
|
|
113
112
|
|
|
114
113
|
doc.change(draft => {
|
|
115
114
|
draft.wrapper.stats.visits = 100
|
|
116
115
|
draft.wrapper.stats.clicks = 50
|
|
117
116
|
})
|
|
118
117
|
|
|
119
|
-
expect(doc.
|
|
118
|
+
expect(doc.toJSON().wrapper.stats).toEqual({
|
|
120
119
|
visits: 100,
|
|
121
120
|
clicks: 50,
|
|
122
121
|
})
|
|
@@ -131,7 +130,7 @@ describe("Record Types", () => {
|
|
|
131
130
|
}),
|
|
132
131
|
})
|
|
133
132
|
|
|
134
|
-
const doc = new TypedDoc(schema
|
|
133
|
+
const doc = new TypedDoc(schema)
|
|
135
134
|
|
|
136
135
|
doc.change(draft => {
|
|
137
136
|
draft.wrapper.settings.ui = {
|
|
@@ -144,7 +143,7 @@ describe("Record Types", () => {
|
|
|
144
143
|
}
|
|
145
144
|
})
|
|
146
145
|
|
|
147
|
-
expect(doc.
|
|
146
|
+
expect(doc.toJSON().wrapper.settings).toEqual({
|
|
148
147
|
ui: {
|
|
149
148
|
darkMode: true,
|
|
150
149
|
sidebar: false,
|
|
@@ -168,7 +167,7 @@ describe("Record Types", () => {
|
|
|
168
167
|
),
|
|
169
168
|
})
|
|
170
169
|
|
|
171
|
-
const doc = new TypedDoc(schema
|
|
170
|
+
const doc = new TypedDoc(schema)
|
|
172
171
|
|
|
173
172
|
doc.change(draft => {
|
|
174
173
|
const alice = draft.users.getOrCreateNode("u1")
|
|
@@ -180,10 +179,91 @@ describe("Record Types", () => {
|
|
|
180
179
|
bob.age = 25
|
|
181
180
|
})
|
|
182
181
|
|
|
183
|
-
expect(doc.
|
|
182
|
+
expect(doc.toJSON().users).toEqual({
|
|
184
183
|
u1: { name: "Alice", age: 30 },
|
|
185
184
|
u2: { name: "Bob", age: 25 },
|
|
186
185
|
})
|
|
187
186
|
})
|
|
187
|
+
|
|
188
|
+
it("should allow setting a plain object for a record with map values", () => {
|
|
189
|
+
const schema = Shape.doc({
|
|
190
|
+
participants: Shape.record(
|
|
191
|
+
Shape.map({
|
|
192
|
+
id: Shape.plain.string(),
|
|
193
|
+
role: Shape.plain.string(),
|
|
194
|
+
name: Shape.plain.string(),
|
|
195
|
+
color: Shape.plain.string(),
|
|
196
|
+
}),
|
|
197
|
+
),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const doc = new TypedDoc(schema)
|
|
201
|
+
|
|
202
|
+
doc.change(draft => {
|
|
203
|
+
draft.participants["student-1"] = {
|
|
204
|
+
id: "student-1",
|
|
205
|
+
role: "student",
|
|
206
|
+
name: "Alice",
|
|
207
|
+
color: "indigo",
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
expect(doc.toJSON().participants["student-1"]).toEqual({
|
|
212
|
+
id: "student-1",
|
|
213
|
+
role: "student",
|
|
214
|
+
name: "Alice",
|
|
215
|
+
color: "indigo",
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it("should allow setting a plain object for a record with nested map values", () => {
|
|
220
|
+
const schema = Shape.doc({
|
|
221
|
+
data: Shape.record(
|
|
222
|
+
Shape.map({
|
|
223
|
+
info: Shape.map({
|
|
224
|
+
name: Shape.plain.string(),
|
|
225
|
+
}),
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const doc = new TypedDoc(schema)
|
|
231
|
+
|
|
232
|
+
doc.change(draft => {
|
|
233
|
+
draft.data["item-1"] = {
|
|
234
|
+
info: {
|
|
235
|
+
name: "Item 1",
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
expect(doc.toJSON().data["item-1"]).toEqual({
|
|
241
|
+
info: {
|
|
242
|
+
name: "Item 1",
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("should allow setting a plain array for a record with list values", () => {
|
|
248
|
+
const schema = Shape.doc({
|
|
249
|
+
histories: Shape.record(Shape.list(Shape.plain.string())),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const doc = new TypedDoc(schema)
|
|
253
|
+
|
|
254
|
+
doc.change(draft => {
|
|
255
|
+
draft.histories.user1 = ["a", "b"]
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expect(doc.toJSON().histories.user1).toEqual(["a", "b"])
|
|
259
|
+
|
|
260
|
+
doc.change(draft => {
|
|
261
|
+
// biome-ignore lint/complexity/useLiteralKeys: tests indexed assignment
|
|
262
|
+
draft.histories["user1"] = ["c"]
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
// biome-ignore lint/complexity/useLiteralKeys: tests indexed assignment
|
|
266
|
+
expect(doc.toJSON().histories["user1"]).toEqual(["c"])
|
|
267
|
+
})
|
|
188
268
|
})
|
|
189
269
|
})
|