@voidhash/mimic 0.0.1-alpha.5 → 0.0.1-alpha.7
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/.turbo/turbo-build.log +13 -13
- package/dist/index.cjs +140 -46
- package/dist/index.d.cts +229 -115
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +229 -115
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +140 -46
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client/ClientDocument.ts +1 -1
- package/src/client/errors.ts +10 -10
- package/src/primitives/Array.ts +45 -21
- package/src/primitives/Boolean.ts +8 -6
- package/src/primitives/Either.ts +13 -11
- package/src/primitives/Literal.ts +7 -5
- package/src/primitives/Number.ts +13 -11
- package/src/primitives/String.ts +14 -12
- package/src/primitives/Struct.ts +66 -17
- package/src/primitives/Tree.ts +103 -32
- package/src/primitives/TreeNode.ts +49 -27
- package/src/primitives/Union.ts +44 -17
- package/src/primitives/shared.ts +106 -5
- package/src/server/errors.ts +6 -6
- package/tests/primitives/Struct.test.ts +250 -0
- package/tests/primitives/Tree.test.ts +122 -0
|
@@ -66,6 +66,46 @@ describe("StructPrimitive", () => {
|
|
|
66
66
|
expect(operations[0]!.payload).toEqual({ name: "Alice", age: "30" });
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it("set() only requires fields that are required and without defaults", () => {
|
|
70
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
71
|
+
const env = ProxyEnvironment.make((op) => {
|
|
72
|
+
operations.push(op);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const structPrimitive = Primitive.Struct({
|
|
76
|
+
name: Primitive.String().required().default("John Doe"),
|
|
77
|
+
age: Primitive.Number().required(),
|
|
78
|
+
email: Primitive.String(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
82
|
+
proxy.set({ age: 30 });
|
|
83
|
+
|
|
84
|
+
expect(operations).toHaveLength(1);
|
|
85
|
+
expect(operations[0]!.kind).toBe("struct.set");
|
|
86
|
+
expect(operations[0]!.payload).toEqual({ name: "John Doe", age: 30, email: undefined });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("set() only requires fields that are required and without defaults", () => {
|
|
90
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
91
|
+
const env = ProxyEnvironment.make((op) => {
|
|
92
|
+
operations.push(op);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const structPrimitive = Primitive.Struct({
|
|
96
|
+
name: Primitive.String().required(),
|
|
97
|
+
age: Primitive.Number().required(),
|
|
98
|
+
email: Primitive.String(),
|
|
99
|
+
}).default({ name: "John Doe" });
|
|
100
|
+
|
|
101
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
102
|
+
proxy.set({ age: 30 });
|
|
103
|
+
|
|
104
|
+
expect(operations).toHaveLength(1);
|
|
105
|
+
expect(operations[0]!.kind).toBe("struct.set");
|
|
106
|
+
expect(operations[0]!.payload).toEqual({ name: "John Doe", age: 30, email: undefined });
|
|
107
|
+
});
|
|
108
|
+
|
|
69
109
|
it("multiple field sets generate separate operations", () => {
|
|
70
110
|
const operations: Operation.Operation<any, any, any>[] = [];
|
|
71
111
|
const env = ProxyEnvironment.make((op) => {
|
|
@@ -304,6 +344,216 @@ describe("StructPrimitive", () => {
|
|
|
304
344
|
expect(typeof proxy.set).toBe("function");
|
|
305
345
|
});
|
|
306
346
|
});
|
|
347
|
+
|
|
348
|
+
describe("update", () => {
|
|
349
|
+
it("update() generates individual field operations", () => {
|
|
350
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
351
|
+
const env = ProxyEnvironment.make((op) => {
|
|
352
|
+
operations.push(op);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const structPrimitive = Primitive.Struct({
|
|
356
|
+
name: Primitive.String(),
|
|
357
|
+
email: Primitive.String(),
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
361
|
+
|
|
362
|
+
proxy.update({ name: "John" });
|
|
363
|
+
|
|
364
|
+
expect(operations).toHaveLength(1);
|
|
365
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
366
|
+
expect(operations[0]!.payload).toBe("John");
|
|
367
|
+
expect(operations[0]!.path.toTokens()).toEqual(["name"]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("update() with multiple fields generates multiple operations", () => {
|
|
371
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
372
|
+
const env = ProxyEnvironment.make((op) => {
|
|
373
|
+
operations.push(op);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const structPrimitive = Primitive.Struct({
|
|
377
|
+
firstName: Primitive.String(),
|
|
378
|
+
lastName: Primitive.String(),
|
|
379
|
+
email: Primitive.String(),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
383
|
+
|
|
384
|
+
proxy.update({ firstName: "John", lastName: "Doe" });
|
|
385
|
+
|
|
386
|
+
expect(operations).toHaveLength(2);
|
|
387
|
+
expect(operations.map((op) => op.payload)).toContain("John");
|
|
388
|
+
expect(operations.map((op) => op.payload)).toContain("Doe");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("update() skips undefined values", () => {
|
|
392
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
393
|
+
const env = ProxyEnvironment.make((op) => {
|
|
394
|
+
operations.push(op);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
const structPrimitive = Primitive.Struct({
|
|
398
|
+
name: Primitive.String(),
|
|
399
|
+
email: Primitive.String(),
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
403
|
+
|
|
404
|
+
proxy.update({ name: "John", email: undefined });
|
|
405
|
+
|
|
406
|
+
expect(operations).toHaveLength(1);
|
|
407
|
+
expect(operations[0]!.payload).toBe("John");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("update() recursively updates nested structs", () => {
|
|
411
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
412
|
+
const env = ProxyEnvironment.make((op) => {
|
|
413
|
+
operations.push(op);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const addressPrimitive = Primitive.Struct({
|
|
417
|
+
street: Primitive.String(),
|
|
418
|
+
city: Primitive.String(),
|
|
419
|
+
zip: Primitive.String(),
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const personPrimitive = Primitive.Struct({
|
|
423
|
+
name: Primitive.String(),
|
|
424
|
+
address: addressPrimitive,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const proxy = personPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
428
|
+
|
|
429
|
+
// Partial update of nested struct - only city should be updated
|
|
430
|
+
proxy.update({ address: { city: "New York" } });
|
|
431
|
+
|
|
432
|
+
expect(operations).toHaveLength(1);
|
|
433
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
434
|
+
expect(operations[0]!.payload).toBe("New York");
|
|
435
|
+
expect(operations[0]!.path.toTokens()).toEqual(["address", "city"]);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("update() handles deeply nested structs", () => {
|
|
439
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
440
|
+
const env = ProxyEnvironment.make((op) => {
|
|
441
|
+
operations.push(op);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const coordsPrimitive = Primitive.Struct({
|
|
445
|
+
lat: Primitive.String(),
|
|
446
|
+
lng: Primitive.String(),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const locationPrimitive = Primitive.Struct({
|
|
450
|
+
name: Primitive.String(),
|
|
451
|
+
coords: coordsPrimitive,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const personPrimitive = Primitive.Struct({
|
|
455
|
+
name: Primitive.String(),
|
|
456
|
+
location: locationPrimitive,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
const proxy = personPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
460
|
+
|
|
461
|
+
proxy.update({ location: { coords: { lat: "40.7128" } } });
|
|
462
|
+
|
|
463
|
+
expect(operations).toHaveLength(1);
|
|
464
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
465
|
+
expect(operations[0]!.payload).toBe("40.7128");
|
|
466
|
+
expect(operations[0]!.path.toTokens()).toEqual(["location", "coords", "lat"]);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("update() can update both nested and top-level fields", () => {
|
|
470
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
471
|
+
const env = ProxyEnvironment.make((op) => {
|
|
472
|
+
operations.push(op);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const addressPrimitive = Primitive.Struct({
|
|
476
|
+
city: Primitive.String(),
|
|
477
|
+
zip: Primitive.String(),
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const personPrimitive = Primitive.Struct({
|
|
481
|
+
name: Primitive.String(),
|
|
482
|
+
address: addressPrimitive,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const proxy = personPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
486
|
+
|
|
487
|
+
proxy.update({ name: "Jane", address: { city: "Boston" } });
|
|
488
|
+
|
|
489
|
+
expect(operations).toHaveLength(2);
|
|
490
|
+
|
|
491
|
+
const nameOp = operations.find((op) => op.path.toTokens().join("/") === "name");
|
|
492
|
+
const cityOp = operations.find((op) => op.path.toTokens().join("/") === "address/city");
|
|
493
|
+
|
|
494
|
+
expect(nameOp).toBeDefined();
|
|
495
|
+
expect(nameOp!.payload).toBe("Jane");
|
|
496
|
+
|
|
497
|
+
expect(cityOp).toBeDefined();
|
|
498
|
+
expect(cityOp!.payload).toBe("Boston");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("update() with empty object generates no operations", () => {
|
|
502
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
503
|
+
const env = ProxyEnvironment.make((op) => {
|
|
504
|
+
operations.push(op);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
const structPrimitive = Primitive.Struct({
|
|
508
|
+
name: Primitive.String(),
|
|
509
|
+
email: Primitive.String(),
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
513
|
+
|
|
514
|
+
proxy.update({});
|
|
515
|
+
|
|
516
|
+
expect(operations).toHaveLength(0);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("update() ignores unknown fields", () => {
|
|
520
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
521
|
+
const env = ProxyEnvironment.make((op) => {
|
|
522
|
+
operations.push(op);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const structPrimitive = Primitive.Struct({
|
|
526
|
+
name: Primitive.String(),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
530
|
+
|
|
531
|
+
// Cast to any to bypass type checking for testing unknown fields
|
|
532
|
+
(proxy as any).update({ name: "John", unknownField: "value" });
|
|
533
|
+
|
|
534
|
+
expect(operations).toHaveLength(1);
|
|
535
|
+
expect(operations[0]!.payload).toBe("John");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("update() with nested path prefix", () => {
|
|
539
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
540
|
+
const env = ProxyEnvironment.make((op) => {
|
|
541
|
+
operations.push(op);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const structPrimitive = Primitive.Struct({
|
|
545
|
+
name: Primitive.String(),
|
|
546
|
+
email: Primitive.String(),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make("users/0"));
|
|
550
|
+
|
|
551
|
+
proxy.update({ name: "Updated" });
|
|
552
|
+
|
|
553
|
+
expect(operations).toHaveLength(1);
|
|
554
|
+
expect(operations[0]!.path.toTokens()).toEqual(["users", "0", "name"]);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
307
557
|
});
|
|
308
558
|
|
|
309
559
|
// =============================================================================
|
|
@@ -460,6 +460,128 @@ describe("TreePrimitive", () => {
|
|
|
460
460
|
expect(withDefault._internal.getInitialState()).toEqual(defaultState);
|
|
461
461
|
});
|
|
462
462
|
});
|
|
463
|
+
|
|
464
|
+
describe("proxy - partial update", () => {
|
|
465
|
+
it("update() on TypedNodeProxy updates only specified fields", () => {
|
|
466
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
467
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
468
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
469
|
+
];
|
|
470
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
471
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
472
|
+
|
|
473
|
+
// Use the update method via as()
|
|
474
|
+
const fileProxy = proxy.node("file1")!.as(FileNode);
|
|
475
|
+
fileProxy.update({ name: "UpdatedName" });
|
|
476
|
+
|
|
477
|
+
// Should generate only a string.set operation for the name field
|
|
478
|
+
expect(operations).toHaveLength(1);
|
|
479
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
480
|
+
expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
|
|
481
|
+
expect(operations[0]!.payload).toBe("UpdatedName");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("update() preserves other fields when updating partial data", () => {
|
|
485
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
486
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
487
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
488
|
+
];
|
|
489
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
490
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
491
|
+
|
|
492
|
+
// Update only the size field
|
|
493
|
+
proxy.node("file1")!.as(FileNode).update({ size: 200 });
|
|
494
|
+
|
|
495
|
+
// Should generate only a number.set operation for the size field
|
|
496
|
+
expect(operations).toHaveLength(1);
|
|
497
|
+
expect(operations[0]!.kind).toBe("number.set");
|
|
498
|
+
expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
|
|
499
|
+
expect(operations[0]!.payload).toBe(200);
|
|
500
|
+
|
|
501
|
+
// The name should remain unchanged in the state
|
|
502
|
+
const updatedState = proxy.get();
|
|
503
|
+
const file1 = updatedState.find(n => n.id === "file1");
|
|
504
|
+
expect(file1!.data).toEqual({ name: "File1", size: 200 });
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("update() handles multiple fields at once", () => {
|
|
508
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
509
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
510
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
511
|
+
];
|
|
512
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
513
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
514
|
+
|
|
515
|
+
// Update both name and size
|
|
516
|
+
proxy.node("file1")!.as(FileNode).update({ name: "NewFile", size: 500 });
|
|
517
|
+
|
|
518
|
+
// Should generate two operations
|
|
519
|
+
expect(operations).toHaveLength(2);
|
|
520
|
+
|
|
521
|
+
// Verify both fields were updated
|
|
522
|
+
const updatedState = proxy.get();
|
|
523
|
+
const file1 = updatedState.find(n => n.id === "file1");
|
|
524
|
+
expect(file1!.data).toEqual({ name: "NewFile", size: 500 });
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("updateAt() provides convenient partial update by node id", () => {
|
|
528
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
529
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
530
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
531
|
+
];
|
|
532
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
533
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
534
|
+
|
|
535
|
+
// Use updateAt for convenience
|
|
536
|
+
proxy.updateAt("file1", FileNode, { name: "QuickUpdate" });
|
|
537
|
+
|
|
538
|
+
expect(operations).toHaveLength(1);
|
|
539
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
540
|
+
expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
|
|
541
|
+
expect(operations[0]!.payload).toBe("QuickUpdate");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("updateAt() throws for wrong node type", () => {
|
|
545
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
546
|
+
{ id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
|
|
547
|
+
];
|
|
548
|
+
const { env } = createEnvWithState(initialState);
|
|
549
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
550
|
+
|
|
551
|
+
// Trying to update a file node as a folder should throw
|
|
552
|
+
expect(() => proxy.updateAt("file1", FolderNode, { name: "NewName" })).toThrow(
|
|
553
|
+
Primitive.ValidationError
|
|
554
|
+
);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("updateAt() throws for non-existent node", () => {
|
|
558
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
559
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
560
|
+
];
|
|
561
|
+
const { env } = createEnvWithState(initialState);
|
|
562
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
563
|
+
|
|
564
|
+
expect(() => proxy.updateAt("nonexistent", FileNode, { name: "Name" })).toThrow(
|
|
565
|
+
Primitive.ValidationError
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("data.update() on at() proxy also works for partial updates", () => {
|
|
570
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
571
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
572
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
573
|
+
];
|
|
574
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
575
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
576
|
+
|
|
577
|
+
// The at() method returns the data proxy which has update()
|
|
578
|
+
proxy.at("file1", FileNode).update({ size: 999 });
|
|
579
|
+
|
|
580
|
+
expect(operations).toHaveLength(1);
|
|
581
|
+
expect(operations[0]!.kind).toBe("number.set");
|
|
582
|
+
expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
463
585
|
});
|
|
464
586
|
|
|
465
587
|
// =============================================================================
|