-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathoverwrite.js
More file actions
127 lines (120 loc) · 5.87 KB
/
overwrite.js
File metadata and controls
127 lines (120 loc) · 5.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
#!/usr/bin/env node
/**
* Overwrite controller for RERUM operations
* Handles overwrite operations with optimistic locking
* @author Claude Sonnet 4, cubap, thehabes
*/
import { newID, isValidID, db } from '../database/index.js'
import utils from '../utils.js'
import { _contextid, ObjectID, createExpressError, getAgentClaim, parseDocumentID, idNegotiation } from './utils.js'
/**
* Replace some existing object in MongoDB with the JSON object in the request body.
* Order the properties to preference @context and @id. Put __rerum and _id last.
* DO NOT Track History
* Respond RESTfully
* */
const overwrite = async function (req, res, next) {
const perfStart = Date.now()
console.log(`\x1b[36m[PERF] RERUM overwrite started for ${req.body?.["@id"] || req.body?.id}\x1b[0m`)
let err = { message: `` }
res.set("Content-Type", "application/json; charset=utf-8")
let objectReceived = JSON.parse(JSON.stringify(req.body))
let agentRequestingOverwrite = getAgentClaim(req, next)
const receivedID = objectReceived["@id"] ?? objectReceived.id
if (receivedID) {
console.log("OVERWRITE")
let id = parseDocumentID(receivedID)
let originalObject
try {
const findStart = Date.now()
const query = {"$or":[{"_id": id}, {"__rerum.slug": id}]}
console.log(`\x1b[90m[DEBUG] RERUM findOne query: ${JSON.stringify(query)}\x1b[0m`)
originalObject = await db.findOne(query)
console.log(`\x1b[34m[PERF] RERUM overwrite: db.findOne took ${Date.now() - findStart}ms\x1b[0m`)
} catch (error) {
next(createExpressError(error))
return
}
if (null === originalObject) {
err = Object.assign(err, {
message: `No object with this id could be found in RERUM. Cannot overwrite. ${err.message}`,
status: 404
})
}
else if (utils.isDeleted(originalObject)) {
err = Object.assign(err, {
message: `The object you are trying to overwrite is deleted. ${err.message}`,
status: 403
})
}
else if (utils.isReleased(originalObject)) {
err = Object.assign(err, {
message: `The object you are trying to overwrite is released. Fork with /update to make changes. ${err.message}`,
status: 403
})
}
else if (!utils.isGenerator(originalObject, agentRequestingOverwrite)) {
err = Object.assign(err, {
message: `You are not the generating agent for this object. You cannot overwrite it. Fork with /update to make changes. ${err.message}`,
status: 401
})
}
else {
// Optimistic locking check - no expected version is a brutal overwrite
const expectedVersion = req.get('If-Overwritten-Version') ?? req.body.__rerum?.isOverwritten
const currentVersionTS = originalObject.__rerum?.isOverwritten ?? ""
if (expectedVersion !== undefined && expectedVersion !== currentVersionTS) {
console.log(`\x1b[33m[PERF] RERUM overwrite: VERSION CONFLICT (409) - expected ${expectedVersion}, got ${currentVersionTS}\x1b[0m`)
res.status(409)
res.json({
currentVersion: originalObject
})
return
}
else {
let context = objectReceived["@context"] ? { "@context": objectReceived["@context"] } : {}
let rerumProp = { "__rerum": originalObject["__rerum"] }
rerumProp["__rerum"].isOverwritten = new Date(Date.now()).toISOString().replace("Z", "")
const id = originalObject["_id"]
//Get rid of them so we can enforce the order
delete objectReceived["@id"]
delete objectReceived["_id"]
delete objectReceived["__rerum"]
// id is also protected in this case, so it can't be set.
if(_contextid(objectReceived["@context"])) delete objectReceived.id
delete objectReceived["@context"]
let newObject = Object.assign(context, { "@id": originalObject["@id"] }, objectReceived, rerumProp, { "_id": id })
let result
try {
const replaceStart = Date.now()
result = await db.replaceOne({ "_id": id }, newObject)
console.log(`\x1b[34m[PERF] RERUM overwrite: db.replaceOne took ${Date.now() - replaceStart}ms\x1b[0m`)
} catch (error) {
next(createExpressError(error))
return
}
if (result.modifiedCount == 0) {
//result didn't error out, the action was not performed. Sometimes, this is a neutral thing. Sometimes it is indicative of an error.
}
console.log(`\x1b[32m[PERF] RERUM overwrite total: ${Date.now() - perfStart}ms\x1b[0m`)
// Include current version in response headers for future optimistic locking
res.set('Current-Overwritten-Version', rerumProp["__rerum"].isOverwritten)
res.set(utils.configureWebAnnoHeadersFor(newObject))
newObject = idNegotiation(newObject)
newObject.new_obj_state = JSON.parse(JSON.stringify(newObject))
res.location(newObject[_contextid(newObject["@context"]) ? "id":"@id"])
res.json(newObject)
return
}
}
}
else {
//This is a custom one, the http module will not detect this as a 400 on its own
err = Object.assign(err, {
message: `Object in request body must have the property '@id' or 'id'. ${err.message}`,
status: 400
})
}
next(createExpressError(err))
}
export { overwrite }