diff --git a/package-lock.json b/package-lock.json index 2384ade..cab9946 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3999,6 +3999,11 @@ "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==", "dev": true }, + "node_modules/@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -5824,6 +5829,21 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/core-js-pure": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz", @@ -6072,6 +6092,11 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -8720,6 +8745,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -9070,6 +9107,33 @@ "node": ">=6" } }, + "node_modules/jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "dependencies": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "bin": { + "jsondiffpatch": "bin/jsondiffpatch.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/jsondiffpatch/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -13140,6 +13204,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/supertap": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", @@ -15040,12 +15116,14 @@ "dependencies": { "fast-json-patch": "^3.1.1", "fflate": "^0.8.2", + "jsondiffpatch": "^0.6.0", "tslib": "^2.6.2" }, "devDependencies": { "@types/node": "^20.12.5", "@types/uuid": "^8.3.4", "nyc": "^15.1.0", + "superjson": "^2.2.2", "tap": "^18.7.0", "ts-node": "^10.8.1", "tsx": "^4.7.1", @@ -15661,7 +15739,9 @@ "@types/uuid": "^8.3.4", "fast-json-patch": "^3.1.1", "fflate": "^0.8.2", + "jsondiffpatch": "^0.6.0", "nyc": "^15.1.0", + "superjson": "^2.2.2", "tap": "^18.7.0", "ts-node": "^10.8.1", "tslib": "^2.6.2", @@ -17792,6 +17872,11 @@ "integrity": "sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg==", "dev": true }, + "@types/diff-match-patch": { + "version": "1.0.36", + "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", + "integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -19117,6 +19202,15 @@ "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "dev": true }, + "copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "requires": { + "is-what": "^4.1.8" + } + }, "core-js-pure": { "version": "3.21.1", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.21.1.tgz", @@ -19307,6 +19401,11 @@ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -21242,6 +21341,12 @@ "get-intrinsic": "^1.1.1" } }, + "is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -21513,6 +21618,23 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsondiffpatch": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz", + "integrity": "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==", + "requires": { + "@types/diff-match-patch": "^1.0.36", + "chalk": "^5.3.0", + "diff-match-patch": "^1.0.5" + }, + "dependencies": { + "chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" + } + } + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -24483,6 +24605,15 @@ } } }, + "superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "requires": { + "copy-anything": "^3.0.2" + } + }, "supertap": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz", diff --git a/packages/ogre/package.json b/packages/ogre/package.json index b304479..b422c77 100644 --- a/packages/ogre/package.json +++ b/packages/ogre/package.json @@ -30,6 +30,7 @@ "license": "MIT", "dependencies": { "fast-json-patch": "^3.1.1", + "jsondiffpatch": "^0.6.0", "fflate": "^0.8.2", "tslib": "^2.6.2" }, @@ -41,7 +42,8 @@ "ts-node": "^10.8.1", "tsx": "^4.7.1", "typescript": "^5.4.4", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "superjson": "^2.2.2" }, "publishConfig": { "registry": "https://registry.npmjs.org/", diff --git a/packages/ogre/src/repository.test.ts b/packages/ogre/src/repository.test.ts index 3cfa026..6d74c47 100644 --- a/packages/ogre/src/repository.test.ts +++ b/packages/ogre/src/repository.test.ts @@ -1,4 +1,5 @@ import { test } from "tap"; +import superjson from "superjson"; import { Repository } from "./repository.js"; import { @@ -12,6 +13,7 @@ import { } from "./test.utils.js"; import { Reference } from "./interfaces.js"; import { compare } from "fast-json-patch"; +import {objectToTree, treeToObject} from "./serialize.js"; test("diff is ok", async (t) => { const [repo, obj] = await getBaseline(); @@ -44,10 +46,10 @@ test("restore", async (t) => { t.test("history check", async (t) => { const [repo, wrapped] = await getBaseline(); - let changeEntries = updateHeaderData(wrapped); + updateHeaderData(wrapped); await repo.commit("header data", testAuthor); - changeEntries += addOneNested(wrapped); + addOneNested(wrapped); const firstStep = await repo.commit("first step", testAuthor); const history = repo.getHistory(); @@ -76,6 +78,62 @@ test("restore", async (t) => { ); }); + t.test("date stays date", async (t) => { + /** + * Serialize an object to a string using the Ogre library. + * This is useful for storing and retrieving objects in a repository. + * @param obj + */ + const serializeObject = async (obj: any) => { + return objectToTree(obj, superjson.stringify) + } + + /** + * Deserialize an object from a string using the Ogre library. + * This is useful for retrieving objects from a repository. + * @param str + */ + const deserializeObject = async (str: string) => { + return treeToObject(str, superjson.parse) + } + + const [repo, wrapped] = await getBaseline( + undefined, + serializeObject, + deserializeObject + ); + + updateHeaderData(wrapped); + wrapped.aDate = new Date(); + await repo.commit("header data", testAuthor); + + t.equal(repo.getHistory().commits.length, 1, "incorrect # of commits"); + + const repo2 = new Repository( + {}, + { + history: repo.getHistory(), + overrides: { + serializeObjectFn: serializeObject, + deserializeObjectFn: deserializeObject + }, + }, + ); + await repo2.isReady(); + + t.equal(repo2.data.aDate instanceof Date, true, "date is not a date"); + t.matchStrict( + repo2.data, + repo.data, + "restored object does not equal last version.", + ); + t.equal( + sumChanges(repo2.getHistory().commits), + sumChanges(repo.getHistory().commits), + "incorrect # of changelog entries", + ); + }); + t.test("reconstruct with 2 commits", async (t) => { const [repo, wrapped] = await getBaseline(); diff --git a/packages/ogre/src/repository.ts b/packages/ogre/src/repository.ts index 7d21079..a4ace2d 100644 --- a/packages/ogre/src/repository.ts +++ b/packages/ogre/src/repository.ts @@ -33,6 +33,7 @@ import { validateBranchName, validateRef, } from "./utils.js"; +import * as jsondiffpatch from 'jsondiffpatch'; export interface RepositoryOptions { history?: History; @@ -321,15 +322,21 @@ export class Repository private async moveTo(commit: Commit) { const deserializeFn = this.deserializeObjectFn ?? defaultDeserializeFn; const targetTree = await deserializeFn(commit.tree); - const patchToTarget = compare(this.data, targetTree); - if (!patchToTarget || patchToTarget.length < 1) { + + // using object patching instead of json-patch, + // because deserializeFn might use e.g. superjson for extended type support + // and json-patch does not support extended types + const patchToTarget = jsondiffpatch.diff(this.data, targetTree); + if (!patchToTarget) { return; } + this.observer.unobserve(); - patchToTarget.reduce(applyReducer, this.data); + jsondiffpatch.patch(this.data, patchToTarget); this.observer = observe(this.data); } + // FIXME: refactor this to use jsondiffpatch delta instead of json-patch for advanced type support apply(patch: Array): JsonPatchError | undefined { const p = deepClone(patch) as Array; const err = validate(p, this.data); @@ -402,6 +409,7 @@ export class Repository return REFS_HEAD_KEY; // detached state } + // FIXME: refactor this to use jsondiffpatch delta instead of json-patch for advanced type support async status() { const commit = this.commitAtHead(); if (!commit) { @@ -411,7 +419,8 @@ export class Repository return this.diff(commit.hash); } - async diff(shaishFrom: string, shaishTo?: string): Promise> { + // FIXME: refactor this to use jsondiffpatch delta instead of json-patch for advanced type support + async diff(shaishFrom: string, shaishTo?: string): Promise> { const [cFrom] = shaishToCommit(shaishFrom, this.refs, this.commits); let target: T; const deserializeFn = this.deserializeObjectFn ?? defaultDeserializeFn; @@ -467,6 +476,7 @@ export class Repository } } + // FIXME: refactor this to use parent tree vs this.data instead to get json-patch const patch = generate(this.observer); if ( (patch.length === 0 && !amend) || diff --git a/packages/ogre/src/test.utils.ts b/packages/ogre/src/test.utils.ts index ef7d6b3..2003625 100644 --- a/packages/ogre/src/test.utils.ts +++ b/packages/ogre/src/test.utils.ts @@ -11,6 +11,7 @@ export type ComplexObject = { uuid?: string; name?: string; description?: string; + aDate?: Date; nested: NestedObject[]; }; @@ -18,6 +19,8 @@ export const testAuthor = "User name "; export async function getBaseline( obj?: Partial, + serializeFn?: (obj: any) => Promise, + deserializeFn?: (str: string) => Promise, ): Promise<[RepositoryObject, ComplexObject]> { const co: ComplexObject = { uuid: undefined, @@ -26,7 +29,14 @@ export async function getBaseline( nested: [], ...obj, }; - const repo = new Repository(co, {}); + const repo = new Repository(co, { + overrides: serializeFn && + deserializeFn && { + calculateCommitHashFn: undefined, + serializeObjectFn: serializeFn, + deserializeObjectFn: deserializeFn, + }, + }); return [repo, co]; }