From 651f47813d553868c21ba59bb5c1ffdda209133c Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 8 Jul 2025 10:59:17 -0500 Subject: [PATCH 1/6] Handle 409 conflict error in overwrite function Added specific handling for HTTP 409 conflict errors in the overwrite function to trigger a custom event with conflict details. This improves error reporting when attempting to overwrite an object that has a version conflict. --- public/scripts/api.js | 4 ++++ 1 file changed, 4 insertions(+) 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) }) } From 87f461f9e90b75ce5bde984bed0a7fc6c4f9727e Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 8 Jul 2025 11:06:57 -0500 Subject: [PATCH 2/6] Improve overwrite route error handling and header support Enhanced the overwrite route to better handle errors, including 409 version conflicts, and to support the If-Overwritten-Version header from both request headers and the request body. Also improved validation for record IDs and refactored response handling for clarity. --- routes/overwrite.js | 54 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/routes/overwrite.js b/routes/overwrite.js index 9aea2a9..058c9ad 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -7,30 +7,60 @@ import rerumPropertiesWasher from "../preprocessor.js" router.put('/', checkAccessToken, rerumPropertiesWasher, 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)) { + throw Error("No record id to overwrite! (https://store.rerum.io/API.html#overwrite)") } - // 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['if-overwritten-version'] + if (ifOverwrittenVersion) { + overwriteOptions.headers['If-Overwritten-Version'] = ifOverwrittenVersion + } + + // Check for __rerum.isOverwritten in body and use as If-Overwritten-Version header + const isOverwrittenValue = req.body?.__rerum?.isOverwritten + if (isOverwrittenValue) { + 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) + 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() + if(response.status === 200) { + res.setHeader("Location", result["@id"] ?? result.id) + res.status(200) + } res.send(result) } - catch (err) { - next(err) + catch (err) { + console.log(err) + res.status(500).send("Caught Error:" + err) } }) From 07b2fb5ba352152837db2b660ec4fbfaf88958ec Mon Sep 17 00:00:00 2001 From: cubap Date: Tue, 8 Jul 2025 11:38:46 -0500 Subject: [PATCH 3/6] fixing tests --- jest.config.js | 4 +++- routes/__tests__/overwrite.test.js | 21 ++++++++++----------- routes/overwrite.js | 21 ++++++++------------- 3 files changed, 21 insertions(+), 25 deletions(-) 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/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js index 4caf484..15103ce 100644 --- a/routes/__tests__/overwrite.test.js +++ b/routes/__tests__/overwrite.test.js @@ -1,6 +1,8 @@ + +import { jest } from "@jest/globals" +process.env.RERUM_ID_PATTERN = "https://devstore.rerum.io/v1/id/" 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 +15,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 +48,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 +130,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/overwrite.js b/routes/overwrite.js index 058c9ad..5b3478b 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -1,10 +1,8 @@ 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('/', async (req, res, next) => { try { @@ -18,7 +16,8 @@ router.put('/', checkAccessToken, rerumPropertiesWasher, async (req, res, next) method: 'PUT', body: JSON.stringify(overwriteBody), headers: { - 'user-agent': 'Tiny-Things/1.0', + 'user-agent': 'TinyPen', + 'Origin': process.env.ORIGIN, 'Authorization': `Bearer ${process.env.ACCESS_TOKEN}`, 'Content-Type' : "application/json;charset=utf-8" } @@ -52,20 +51,16 @@ router.put('/', checkAccessToken, rerumPropertiesWasher, async (req, res, next) }) if(res.headersSent) return const result = await response.json() - if(response.status === 200) { - res.setHeader("Location", result["@id"] ?? result.id) - res.status(200) + const location = result?.["@id"] ?? result?.id + if (location) { + res.setHeader("Location", location) } - res.send(result) + res.status(response.status ?? 200) + res.json(result) } catch (err) { - console.log(err) res.status(500).send("Caught Error:" + err) } }) -router.all('/', (req, res, next) => { - res.status(405).send("Method Not Allowed") -}) - export default router From 10e9c9f07103bea2b7af43d1ed7b46a93e37090a Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 8 Jul 2025 14:26:18 -0500 Subject: [PATCH 4/6] changes during review and testing --- app.js | 3 ++- routes/__tests__/overwrite.test.js | 1 - routes/overwrite.js | 19 ++++++++++++------- 3 files changed, 14 insertions(+), 9 deletions(-) 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/routes/__tests__/overwrite.test.js b/routes/__tests__/overwrite.test.js index 15103ce..225c280 100644 --- a/routes/__tests__/overwrite.test.js +++ b/routes/__tests__/overwrite.test.js @@ -1,6 +1,5 @@ import { jest } from "@jest/globals" -process.env.RERUM_ID_PATTERN = "https://devstore.rerum.io/v1/id/" import express from "express" import request from "supertest" import overwriteRoute from "../overwrite.js" diff --git a/routes/overwrite.js b/routes/overwrite.js index 5b3478b..3f6f3ff 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -1,8 +1,10 @@ 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('/', async (req, res, next) => { +router.put('/', checkAccessToken, rerumPropertiesWasher, async (req, res, next) => { try { @@ -16,22 +18,21 @@ router.put('/', async (req, res, next) => { method: 'PUT', body: JSON.stringify(overwriteBody), headers: { - 'user-agent': 'TinyPen', - 'Origin': process.env.ORIGIN, + '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['if-overwritten-version'] - if (ifOverwrittenVersion) { + 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?.isOverwritten - if (isOverwrittenValue) { + const isOverwrittenValue = req.body?.__rerum?.hasOwnProperty("isOverwritten") ? req.body.__rerum.isOverwritten : null + if (isOverwrittenValue !== null) { overwriteOptions.headers['If-Overwritten-Version'] = isOverwrittenValue } @@ -63,4 +64,8 @@ router.put('/', async (req, res, next) => { } }) +router.all('/', (req, res, next) => { + res.status(405).send("Method Not Allowed") +}) + export default router From 8f93bbb9e5b69826f62021f36c206ef9744e6df4 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 8 Jul 2025 14:35:11 -0500 Subject: [PATCH 5/6] changes during review and testing --- routes/delete.js | 1 + routes/overwrite.js | 6 +++--- routes/update.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) 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 3f6f3ff..4d078f3 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -1,17 +1,17 @@ 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 { const overwriteBody = req.body // check for @id; any value is valid if (!(overwriteBody['@id'] ?? overwriteBody.id)) { - throw Error("No record id to overwrite! (https://store.rerum.io/API.html#overwrite)") + res.status(400).send("No record id to overwrite! (https://store.rerum.io/API.html#overwrite)") + return } const overwriteOptions = { 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) From c8222ba89bc233f66691b219ad11306f47a27f38 Mon Sep 17 00:00:00 2001 From: Bryan Haberberger Date: Tue, 8 Jul 2025 14:36:44 -0500 Subject: [PATCH 6/6] changes during review and testing --- routes/overwrite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/overwrite.js b/routes/overwrite.js index 4d078f3..8c66600 100644 --- a/routes/overwrite.js +++ b/routes/overwrite.js @@ -60,7 +60,7 @@ router.put('/', checkAccessToken, async (req, res, next) => { res.json(result) } catch (err) { - res.status(500).send("Caught Error:" + err) + next(err) } })