-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
prototype merge-on-save feature with session cookies
- Loading branch information
Showing
8 changed files
with
295 additions
and
12 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,69 @@ | ||
import fs from "fs/promises"; | ||
import { constants } from "fs"; | ||
|
||
import path from "path"; | ||
import { AppLocals, getUserId, getVfs } from "../../../utils/locals.js"; | ||
import uid from "../../../utils/uid.js"; | ||
import { Request, Response } from "express"; | ||
|
||
import { getUserId, getVfs } from "../../../utils/locals.js"; | ||
import { BadRequestError } from "../../../utils/errors.js"; | ||
|
||
import * as merge from "../../../utils/merge.js"; | ||
|
||
/** | ||
* Simple case of PUT /scenes/:scene/scene.svx.json | ||
* Overwrites the current document. | ||
* @param req | ||
* @param res | ||
* @returns | ||
*/ | ||
async function overwritePutDocument(req: Request, res :Response){ | ||
const uid = getUserId(req); | ||
const {scene} = req.params; | ||
const newDoc = req.body; | ||
|
||
console.log("Overwriting document"); | ||
|
||
let s = JSON.stringify(newDoc, null, 2); | ||
if(s == "{}") throw new BadRequestError(`Invalid json document`); | ||
const id = await getVfs(req).writeDoc(s, scene, uid); | ||
res.cookie("docID", `${id}`, {sameSite: "strict", path: path.dirname(req.originalUrl)}); | ||
return res.status(204).send(); | ||
} | ||
|
||
|
||
/** | ||
* Special handler for svx files to disallow the upload of invalid JSON. | ||
* @todo Should check against the official json schema using ajv | ||
* If the user provides a reference document ID and the document has been updated since, a diff is performed to try to merge the changes. | ||
*/ | ||
export default async function handlePutDocument(req :Request, res :Response){ | ||
const vfs = getVfs(req); | ||
const uid = getUserId(req); | ||
const {scene} = req.params; | ||
let s = JSON.stringify(req.body, null, 2); | ||
if(s == "{}") throw new BadRequestError(`Invalid json document`); | ||
await vfs.writeDoc(s, scene, uid); | ||
res.status(204).send(); | ||
const newDoc = req.body; | ||
|
||
const refId = parseInt(req.cookies["docID"] ?? "0"); | ||
if(!refId) return await overwritePutDocument(req, res); | ||
|
||
|
||
await getVfs(req).isolate(async (tr)=>{ | ||
// perform a diff of the document with the reference one | ||
const {id: scene_id} = await tr.getScene(scene); | ||
const {data: currentDocString} = await tr.getDoc(scene_id); | ||
const currentDoc = JSON.parse(currentDocString); | ||
const {data:refDocString} = await tr.getDocById(refId); | ||
const refDoc = JSON.parse(refDocString); | ||
|
||
const docDiff = merge.diff(refDoc, newDoc); | ||
if(Object.keys(docDiff).length == 0){ | ||
console.log("Nothing to do"); | ||
//Nothing to do | ||
return res.status(204).send(); | ||
} | ||
console.log("Merge changes : ", JSON.stringify(docDiff)); | ||
const mergedDoc = merge.apply(currentDoc, docDiff); | ||
let s = JSON.stringify(mergedDoc, null, 2); | ||
if(s == "{}") throw new BadRequestError(`Invalid json document`); | ||
let id = await tr.writeDoc(s, scene, uid); | ||
res.cookie("docID", `${id}`, {sameSite: "strict", path: path.dirname(req.originalUrl)}); | ||
res.status(204).send(); | ||
}) | ||
|
||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import {DELETE_KEY, apply, diff} from "./merge.js"; | ||
|
||
|
||
|
||
describe("merge.diff()", function(){ | ||
describe("handle native data types ", function(){ | ||
[ //All JSON-serializable data types with possible bounday values | ||
1, | ||
0, | ||
"1", | ||
"", | ||
true, | ||
false, | ||
null, | ||
{}, | ||
{a:1}, | ||
[], | ||
].forEach(v=>{ | ||
const v_clone :typeof v = JSON.parse(JSON.stringify(v)); | ||
it(`${JSON.stringify(v)} deep equals itself`, function(){ | ||
expect(diff({v}, {v:v_clone})).to.deep.equal({}); | ||
}); | ||
it(`${JSON.stringify({v})} compares from {}`, function(){ | ||
expect(diff({}, {v})).to.deep.equal({v:v_clone}); | ||
}); | ||
it(`${JSON.stringify({v})} compares from {}`, function(){ | ||
expect(diff({v}, {})).to.deep.equal({v:DELETE_KEY}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("arrays", function(){ | ||
//Arrays behave like objects with numeric keys, with some specific handling | ||
|
||
it("handles an array equality", function(){ | ||
expect(diff({a:["foo"]}, {a:["foo"]})).to.deep.equal({}); | ||
}); | ||
|
||
it("handles nested objects equality", function(){ | ||
expect(diff({a:[{v: "foo"}]}, {a:[{v:"foo"}]})).to.deep.equal({}); | ||
}); | ||
|
||
it("handle array.push", function(){ | ||
expect(diff({a:[]}, {a:[1]})).to.deep.equal({a:{0:1}}); | ||
expect(diff({a:[1]}, {a:[1, 2]})).to.deep.equal({a:{1:2}}); | ||
}); | ||
|
||
it("handle Array.pop", function(){ | ||
expect(diff({a:[1]}, {a:[]})).to.deep.equal({a:{0: DELETE_KEY}}); | ||
expect(diff({a:[1, 2]}, {a:[1]})).to.deep.equal({a:{1: DELETE_KEY}}); | ||
}); | ||
|
||
it("handle Array.replace", function(){ | ||
expect(diff({a:["foo", "bar"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz"}}); | ||
}); | ||
|
||
it("handle Array.splice", function(){ | ||
// Might be improved to detect a splice and use special syntax to represent it | ||
expect(diff({a:["foo", "bar", "baz"]}, {a:["foo", "baz"]})).to.deep.equal({a:{1: "baz", 2: DELETE_KEY}}); | ||
}); | ||
|
||
it("throws when diffing an array with an object", function(){ | ||
expect(()=>diff({a:[]}, {a:{}})).to.throw("Can't diff an array with an object"); | ||
}); | ||
|
||
|
||
}); | ||
}); | ||
|
||
|
||
describe("merge.apply()", function(){ | ||
it("merges a no-op", function(){ | ||
const a = {a:1}; | ||
expect(apply(a, {})).to.deep.equal(a); | ||
}); | ||
|
||
it("merges a deleted key", function(){ | ||
expect(apply({a:1, b:2}, {b:DELETE_KEY})).to.deep.equal({a:1}); | ||
}); | ||
|
||
it("merges an updated key", function(){ | ||
expect(apply({a:1, b:2}, {b:3})).to.deep.equal({a:1, b:3}); | ||
}); | ||
it("merges nested objects", function(){ | ||
expect(apply({a:{b:1}, c:3}, {a:{b:2}})).to.deep.equal({a:{b:2}, c:3}); | ||
}); | ||
it("merges updated arrays", function(){ | ||
expect(apply({a:[1,2]}, {a:{0:1, 1:3}})).to.deep.equal({a:[1,3]}); | ||
}); | ||
}); | ||
|
||
|
||
describe("three-way merge", function(){ | ||
// This is the real point of this module | ||
it("string update", function(){ | ||
/** @TODO could try to do string splicing for finer results */ | ||
const ref = {greet:"Hello", name:"World"}; | ||
const current = {greet:"Hi", name:"World"}; | ||
const next = {greet:"Hello", name:"Universe"}; | ||
expect(apply(current, diff(ref, next))).to.deep.equal({greet:"Hi", name:"Universe"}); | ||
}); | ||
}); |
Oops, something went wrong.