@loro-extended/change 0.2.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/LICENSE +21 -0
- package/README.md +565 -0
- package/dist/index.d.ts +339 -0
- package/dist/index.js +1491 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/change.test.ts +2006 -0
- package/src/change.ts +105 -0
- package/src/conversion.test.ts +728 -0
- package/src/conversion.ts +220 -0
- package/src/draft-nodes/base.ts +34 -0
- package/src/draft-nodes/counter.ts +21 -0
- package/src/draft-nodes/doc.ts +81 -0
- package/src/draft-nodes/list-base.ts +326 -0
- package/src/draft-nodes/list.ts +18 -0
- package/src/draft-nodes/map.ts +156 -0
- package/src/draft-nodes/movable-list.ts +26 -0
- package/src/draft-nodes/record.ts +215 -0
- package/src/draft-nodes/text.ts +48 -0
- package/src/draft-nodes/tree.ts +31 -0
- package/src/draft-nodes/utils.ts +55 -0
- package/src/index.ts +33 -0
- package/src/json-patch.test.ts +697 -0
- package/src/json-patch.ts +391 -0
- package/src/overlay.ts +90 -0
- package/src/record.test.ts +188 -0
- package/src/schema.fixtures.ts +138 -0
- package/src/shape.ts +348 -0
- package/src/types.ts +15 -0
- package/src/utils/type-guards.ts +210 -0
- package/src/validation.ts +261 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ArrayValueShape,
|
|
3
|
+
ContainerOrValueShape,
|
|
4
|
+
DocShape,
|
|
5
|
+
ListContainerShape,
|
|
6
|
+
MapContainerShape,
|
|
7
|
+
MovableListContainerShape,
|
|
8
|
+
ObjectValueShape,
|
|
9
|
+
RecordContainerShape,
|
|
10
|
+
RecordValueShape,
|
|
11
|
+
UnionValueShape,
|
|
12
|
+
ValueShape,
|
|
13
|
+
} from "./shape.js"
|
|
14
|
+
import type { InferPlainType } from "./types.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates a value against a ContainerShape or ValueShape schema
|
|
18
|
+
*/
|
|
19
|
+
export function validateValue(
|
|
20
|
+
value: unknown,
|
|
21
|
+
schema: ContainerOrValueShape,
|
|
22
|
+
path: string = "",
|
|
23
|
+
): unknown {
|
|
24
|
+
if (!schema || typeof schema !== "object" || !("_type" in schema)) {
|
|
25
|
+
throw new Error(`Invalid schema at path ${path}: missing _type`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const currentPath = path || "root"
|
|
29
|
+
|
|
30
|
+
// Handle ContainerShape types
|
|
31
|
+
if (schema._type === "text") {
|
|
32
|
+
if (typeof value !== "string") {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Expected string at path ${currentPath}, got ${typeof value}`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (schema._type === "counter") {
|
|
41
|
+
if (typeof value !== "number") {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Expected number at path ${currentPath}, got ${typeof value}`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return value
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (schema._type === "list" || schema._type === "movableList") {
|
|
50
|
+
if (!Array.isArray(value)) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Expected array at path ${currentPath}, got ${typeof value}`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
const listSchema = schema as ListContainerShape | MovableListContainerShape
|
|
56
|
+
return value.map((item, index) =>
|
|
57
|
+
validateValue(item, listSchema.shape, `${currentPath}[${index}]`),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (schema._type === "map") {
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Expected object at path ${currentPath}, got ${typeof value}`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
const mapSchema = schema as MapContainerShape
|
|
68
|
+
const result: Record<string, unknown> = {}
|
|
69
|
+
|
|
70
|
+
// Validate each property in the map shape
|
|
71
|
+
for (const [key, nestedSchema] of Object.entries(mapSchema.shapes)) {
|
|
72
|
+
const nestedPath = `${currentPath}.${key}`
|
|
73
|
+
const nestedValue = (value as Record<string, unknown>)[key]
|
|
74
|
+
result[key] = validateValue(nestedValue, nestedSchema, nestedPath)
|
|
75
|
+
}
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (schema._type === "record") {
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Expected object at path ${currentPath}, got ${typeof value}`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
const recordSchema = schema as RecordContainerShape
|
|
86
|
+
const result: Record<string, unknown> = {}
|
|
87
|
+
|
|
88
|
+
// Validate each property in the record
|
|
89
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
90
|
+
const nestedPath = `${currentPath}.${key}`
|
|
91
|
+
result[key] = validateValue(nestedValue, recordSchema.shape, nestedPath)
|
|
92
|
+
}
|
|
93
|
+
return result
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (schema._type === "tree") {
|
|
97
|
+
if (!Array.isArray(value)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Expected array for tree at path ${currentPath}, got ${typeof value}`,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
// Trees can contain any structure, so we just validate it's an array
|
|
103
|
+
return value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle ValueShape types
|
|
107
|
+
if (schema._type === "value") {
|
|
108
|
+
const valueSchema = schema as ValueShape
|
|
109
|
+
|
|
110
|
+
switch (valueSchema.valueType) {
|
|
111
|
+
case "string":
|
|
112
|
+
if (typeof value !== "string") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Expected string at path ${currentPath}, got ${typeof value}`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
case "number":
|
|
120
|
+
if (typeof value !== "number") {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Expected number at path ${currentPath}, got ${typeof value}`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
|
|
127
|
+
case "boolean":
|
|
128
|
+
if (typeof value !== "boolean") {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Expected boolean at path ${currentPath}, got ${typeof value}`,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
case "null":
|
|
136
|
+
if (value !== null) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Expected null at path ${currentPath}, got ${typeof value}`,
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
return value
|
|
142
|
+
|
|
143
|
+
case "undefined":
|
|
144
|
+
if (value !== undefined) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Expected undefined at path ${currentPath}, got ${typeof value}`,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return value
|
|
150
|
+
|
|
151
|
+
case "uint8array":
|
|
152
|
+
if (!(value instanceof Uint8Array)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Expected Uint8Array at path ${currentPath}, got ${typeof value}`,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
case "object": {
|
|
160
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Expected object at path ${currentPath}, got ${typeof value}`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
const objectSchema = valueSchema as ObjectValueShape
|
|
166
|
+
const result: Record<string, unknown> = {}
|
|
167
|
+
|
|
168
|
+
// Validate each property in the object shape
|
|
169
|
+
for (const [key, nestedSchema] of Object.entries(objectSchema.shape)) {
|
|
170
|
+
const nestedPath = `${currentPath}.${key}`
|
|
171
|
+
const nestedValue = (value as Record<string, unknown>)[key]
|
|
172
|
+
result[key] = validateValue(nestedValue, nestedSchema, nestedPath)
|
|
173
|
+
}
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case "record": {
|
|
178
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Expected object at path ${currentPath}, got ${typeof value}`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
const recordSchema = valueSchema as RecordValueShape
|
|
184
|
+
const result: Record<string, unknown> = {}
|
|
185
|
+
|
|
186
|
+
// Validate each property in the record
|
|
187
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
188
|
+
const nestedPath = `${currentPath}.${key}`
|
|
189
|
+
result[key] = validateValue(
|
|
190
|
+
nestedValue,
|
|
191
|
+
recordSchema.shape,
|
|
192
|
+
nestedPath,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
return result
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case "array": {
|
|
199
|
+
if (!Array.isArray(value)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Expected array at path ${currentPath}, got ${typeof value}`,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
const arraySchema = valueSchema as ArrayValueShape
|
|
205
|
+
return value.map((item, index) =>
|
|
206
|
+
validateValue(item, arraySchema.shape, `${currentPath}[${index}]`),
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "union": {
|
|
211
|
+
const unionSchema = valueSchema as UnionValueShape
|
|
212
|
+
let lastError: Error | null = null
|
|
213
|
+
|
|
214
|
+
// Try to validate against each shape in the union
|
|
215
|
+
for (const shape of unionSchema.shapes) {
|
|
216
|
+
try {
|
|
217
|
+
return validateValue(value, shape, currentPath)
|
|
218
|
+
} catch (error) {
|
|
219
|
+
lastError = error as Error
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Value at path ${currentPath} does not match any union type: ${lastError?.message}`,
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
default:
|
|
229
|
+
throw new Error(`Unknown value type: ${(valueSchema as any).valueType}`)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
throw new Error(`Unknown schema type: ${(schema as any)._type}`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validates empty state against schema structure without using Zod
|
|
238
|
+
* Combines the functionality of createEmptyStateValidator and createValueValidator
|
|
239
|
+
*/
|
|
240
|
+
export function validateEmptyState<T extends DocShape>(
|
|
241
|
+
emptyState: unknown,
|
|
242
|
+
schema: T,
|
|
243
|
+
): InferPlainType<T> {
|
|
244
|
+
if (
|
|
245
|
+
!emptyState ||
|
|
246
|
+
typeof emptyState !== "object" ||
|
|
247
|
+
Array.isArray(emptyState)
|
|
248
|
+
) {
|
|
249
|
+
throw new Error("Empty state must be an object")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result: Record<string, unknown> = {}
|
|
253
|
+
|
|
254
|
+
// Validate each property in the document schema
|
|
255
|
+
for (const [key, schemaValue] of Object.entries(schema.shapes)) {
|
|
256
|
+
const value = (emptyState as Record<string, unknown>)[key]
|
|
257
|
+
result[key] = validateValue(value, schemaValue, key)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result as InferPlainType<T>
|
|
261
|
+
}
|