diff --git a/app.js b/app.js index bd56bbc..75e67b4 100644 --- a/app.js +++ b/app.js @@ -39,7 +39,8 @@ if(process.env.OPEN_API_CORS !== "false") { 'X-HTTP-Method-Override', 'Origin', 'Referrer', - 'User-Agent' + 'User-Agent', + 'If-Overwritten-Version' ], "exposedHeaders" : "*", "origin" : "*", diff --git a/jest.config.js b/jest.config.js index 130df2f..37e67a9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -24,6 +24,8 @@ const config = { name: 'TinyNode', color: 'cyan' }, + // extensionsToTreatAsEsm: [".js"], + testEnvironment: "node", // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, @@ -206,4 +208,4 @@ const config = { // watchman: true, } -export default config \ No newline at end of file +export default config diff --git a/public/scripts/api.js b/public/scripts/api.js index 4a366e9..b16b25b 100644 --- a/public/scripts/api.js +++ b/public/scripts/api.js @@ -208,6 +208,10 @@ function overwrite(form, objIn) { _customEvent("rerum-result", `URI ${uri} overwritten. See resulting object below:`, resultObj) }) .catch(err => { + if (err?.status === 409) { + _customEvent("rerum-error", "Conflict detected while trying to overwrite object at " + uri, err.currentVersion, err) + return + } _customEvent("rerum-error", "There was an error trying to overwrite object at " + uri, {}, err) }) } diff --git a/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js index 4caf484..225c280 100644 --- a/routes/__tests__/overwrite.test.js +++ b/routes/__tests__/overwrite.test.js @@ -1,6 +1,7 @@ + +import { jest } from "@jest/globals" import express from "express" import request from "supertest" -import { jest } from "@jest/globals" import overwriteRoute from "../overwrite.js" //import app from "../../app.js" @@ -13,22 +14,17 @@ routeTester.use("/app/overwrite", overwriteRoute) const rerum_tiny_test_obj_id = `${process.env.RERUM_ID_PATTERN}tiny_tester` beforeEach(() => { - /** - * Request/Response Mock Using manual fetch replacement - * This is overruling the fetch(store.rerum.io/v1/api/create) call in create.js - */ global.fetch = jest.fn(() => Promise.resolve({ + status: 200, + ok: true, json: () => Promise.resolve({ "@id": rerum_tiny_test_obj_id, "testing": "item", "__rerum": { "stuff": "here" } }) }) ) }) afterEach(() => { - /** - * Food for thought: delete data generated by tests? - * Make a test.store available that uses the same annotationStoreTesting as RERUM tests? - */ + }) /** @@ -51,7 +47,9 @@ describe("Check that the request/response behavior of the TinyNode overwrite rou .set("Content-Type", "application/json") .then(resp => resp) .catch(err => err) - expect(response.header.location).toBe(rerum_tiny_test_obj_id) + if (response.header.location !== rerum_tiny_test_obj_id) { + throw new Error(`Expected Location header to be '${rerum_tiny_test_obj_id}', but got '${response.header.location}'.\nAll headers: ${JSON.stringify(response.header)}\nResponse body: ${JSON.stringify(response.body)}`) + } expect(response.statusCode).toBe(200) expect(response.body.testing).toBe("item") }) @@ -131,4 +129,4 @@ describe("Check that the properly used overwrite endpoints function and interact expect(response.statusCode).toBe(200) expect(response.body.testing).toBe("item") }) -}) \ No newline at end of file +}) diff --git a/routes/delete.js b/routes/delete.js index b5481b6..ea941ef 100644 --- a/routes/delete.js +++ b/routes/delete.js @@ -10,6 +10,7 @@ router.delete('/', checkAccessToken, async (req, res, next) => { // check for @id in body. Any value is valid. Lack of value is a bad request. if (!req?.body || !(req.body['@id'] ?? req.body.id)) { res.status(400).send("No record id to delete! (https://store.rerum.io/v1/API.html#delete)") + return } const body = JSON.stringify(req.body) const deleteOptions = { diff --git a/routes/overwrite.js b/routes/overwrite.js index 9aea2a9..8c66600 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -1,35 +1,65 @@ import express from "express" import checkAccessToken from "../tokens.js" const router = express.Router() -import rerumPropertiesWasher from "../preprocessor.js" /* PUT an overwrite to the thing. */ -router.put('/', checkAccessToken, rerumPropertiesWasher, async (req, res, next) => { +router.put('/', checkAccessToken, async (req, res, next) => { try { - // check for @id in body. Any value is valid. Lack of value is a bad request. - if (!req?.body || !(req.body['@id'] ?? req.body.id)) { - res.status(400).send("No record id to overwrite! (https://store.rerum.io/v1/API.html#overwrite)") + + const overwriteBody = req.body + // check for @id; any value is valid + if (!(overwriteBody['@id'] ?? overwriteBody.id)) { + res.status(400).send("No record id to overwrite! (https://store.rerum.io/API.html#overwrite)") + return } - // check body for JSON - const body = JSON.stringify(req.body) + const overwriteOptions = { method: 'PUT', - body, + body: JSON.stringify(overwriteBody), headers: { 'user-agent': 'Tiny-Things/1.0', 'Authorization': `Bearer ${process.env.ACCESS_TOKEN}`, 'Content-Type' : "application/json;charset=utf-8" } } + + // Pass through If-Overwritten-Version header if present + const ifOverwrittenVersion = req.headers.hasOwnProperty('if-overwritten-version') ? req.headers['if-overwritten-version'] : null + if (ifOverwrittenVersion !== null) { + overwriteOptions.headers['If-Overwritten-Version'] = ifOverwrittenVersion + } + + // Check for __rerum.isOverwritten in body and use as If-Overwritten-Version header + const isOverwrittenValue = req.body?.__rerum?.hasOwnProperty("isOverwritten") ? req.body.__rerum.isOverwritten : null + if (isOverwrittenValue !== null) { + overwriteOptions.headers['If-Overwritten-Version'] = isOverwrittenValue + } + const overwriteURL = `${process.env.RERUM_API_ADDR}overwrite` - const result = await fetch(overwriteURL, overwriteOptions).then(res=>res.json()) - .catch(err=>next(err)) - res.setHeader("Location", result["@id"] ?? result.id) - res.status(200) - res.send(result) + const response = await fetch(overwriteURL, overwriteOptions) + .then(resp=>{ + if (!resp.ok) throw resp + return resp + }) + .catch(async err => { + // Handle 409 conflict error for version mismatch + if (err.status === 409) { + const currentVersion = await err.json() + return res.status(409).json(currentVersion) + } + throw new Error(`Error in overwrite request: ${err.status} ${err.statusText}`) + }) + if(res.headersSent) return + const result = await response.json() + const location = result?.["@id"] ?? result?.id + if (location) { + res.setHeader("Location", location) + } + res.status(response.status ?? 200) + res.json(result) } - catch (err) { + catch (err) { next(err) } }) diff --git a/routes/update.js b/routes/update.js index dd559fb..391b3ce 100644 --- a/routes/update.js +++ b/routes/update.js @@ -1,15 +1,15 @@ import express from "express" import checkAccessToken from "../tokens.js" const router = express.Router() -import rerumPropertiesWasher from "../preprocessor.js" /* PUT an update to the thing. */ -router.put('/', checkAccessToken, rerumPropertiesWasher, async (req, res, next) => { +router.put('/', checkAccessToken, async (req, res, next) => { try { // check for @id in body. Any value is valid. Lack of value is a bad request. if (!req?.body || !(req.body['@id'] ?? req.body.id)) { res.status(400).send("No record id to update! (https://store.rerum.io/v1/API.html#update)") + return } // check body for JSON const body = JSON.stringify(req.body)