Skip to content

Commit b55cecd

Browse files
@id and id negotiation (#191)
* Good id negotiation in the db controller for a create request/response * touch em all * touch em all * touch em all * Get rid of _id regularly * Since it is somewhat likely @context will be an array * tests * Tests we will need TODO * Good error throughput * Changes from manual testing * Error redo * Error redo * Error redo * Now errors are handled in rest.js without unnecessary interceptions * documentation and cleanup * documentation and cleanup * documentation and cleanup * can put this back now * can put this back now * can put this back now * 65 bulk update (#192) * The shape of it * straighten out errors, take into other branch * ew the nasties * Register the bulkUpdate route. Make the errors behave. * Functional bulkUpdate and much improved error reporting. * bulkUpdate test entries. skipping end to end test for it, for now. * Harder checks against objects supplied in the bodies of bulk endpoints. * Harder checks against objects supplied in the bodies of bulk endpoints. * ah ')' * overachieving just a lil bit * Performance update for bulkCreate and bulkUpdate, which may not process in the same order as submitted. * Bump @babel/helpers from 7.23.5 to 7.27.0 (#193) Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.23.5 to 7.27.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.0/packages/babel-helpers) --- updated-dependencies: - dependency-name: "@babel/helpers" dependency-version: 7.27.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * no prezi 2 * yes oa * cmon * lint this bad indent * formally skip() this test * API wording around /bulkUpdate and /bulkCreate. * oh boy missed this * oh boy missed this * oh boy missed this --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 6e257e9 commit b55cecd

File tree

16 files changed

+591
-479
lines changed

16 files changed

+591
-479
lines changed

__tests__/routes_mounted.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ describe('Check to see that all /v1/api/ route patterns exist.', () => {
120120
expect(exists).toBe(true)
121121
})
122122

123+
it('/v1/api/bulkUpdate -- mounted ', () => {
124+
let exists = false
125+
for (const middleware of api_stack) {
126+
if (middleware.regexp
127+
&& middleware.regexp.toString().includes("/api")
128+
&& middleware.regexp.toString().includes("/bulkUpdate")){
129+
exists = true
130+
break
131+
}
132+
}
133+
expect(exists).toBe(true)
134+
})
135+
123136
it('/v1/api/patch -- mounted ', () => {
124137
let exists = false
125138
for (const middleware of api_stack) {

database/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dotenv.config()
44

55
const client = new MongoClient(process.env.MONGO_CONNECTION_STRING)
66
const newID = () => new ObjectId().toHexString()
7+
const isValidID = (id) => ObjectId.isValid(id)
78
const connected = async function () {
89
// Send a ping to confirm a successful connection
910
await client.db("admin").command({ ping: 1 }).catch(err => err)
@@ -50,6 +51,7 @@ function isValidURL(url) {
5051

5152
export {
5253
newID,
54+
isValidID,
5355
connected,
5456
db
5557
}

db-controller.js

Lines changed: 305 additions & 108 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 78 additions & 329 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/API.html

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,11 @@ <h3 id="bulk-create">Bulk Create</h3>
420420

421421
<p>
422422
Add multiple completely new objects to RERUM and receive an array of the complete records as the response body.
423-
Accepts only a single array of JSON objects in the request body.
424-
The array of JSON objects passed in will be created in the order submitted and the response will have the URI
425-
of the new resource or an error message as an array in the same order. When errors are encountered,
426-
the batch process will attempt to continue for all submitted items.
423+
Accepts only a single array of JSON objects in the request body. The '@id' property must not be present on the objects.
424+
In cases where the Linked Data @context property maps '@id' to 'id', the 'id' property also must not be present.
425+
The array of JSON objects passed may not be created in the order submitted. The response will have the URI
426+
of the new resource or an error message as an array in the order the order the objects were processed.
427+
When errors are encountered, the batch process will attempt to continue for all submitted items.
427428
</p>
428429

429430
<p>
@@ -730,6 +731,80 @@ <h3 id="update">Update</h3>
730731
</p>
731732
</p>
732733

734+
<h3 id="bulk-update">Bulk Update</h3>
735+
736+
<table>
737+
<thead>
738+
<tr>
739+
<th>Patterns</th>
740+
<th>Payloads</th>
741+
<th>Responses</th>
742+
</tr>
743+
</thead>
744+
<tbody>
745+
<tr>
746+
<td><code class="language-plaintext highlighter-rouge">/bulkUpdate</code></td>
747+
<td><code class="language-plaintext highlighter-rouge">[{JSON}]</code></td>
748+
<td>201 <code>
749+
<code class="language-plaintext highlighter-rouge">[{JSON}]</code></td>
750+
</tr>
751+
</tbody>
752+
</table>
753+
754+
<ul>
755+
<li><strong><code class="language-plaintext highlighter-rouge">[{JSON}]</code></strong>—an array RERUM objects
756+
to be updated.</li>
757+
<li><strong>Response: <code class="language-plaintext highlighter-rouge">[{JSON}]</code></strong>—an array
758+
of the resolved records from the update process</li>
759+
</ul>
760+
761+
<p>
762+
Update multiple existing RERUM objects at once and recieve an array of the complete records as the response body.
763+
Accepts only a single array of JSON objects in the request body. The '@id' property must be present for each object.
764+
In cases where the Linked Data @context property maps '@id' to 'id' either of these properties will be sufficient.
765+
The array of JSON objects passed in may not be updated in the order submitted. The response will have the URI
766+
of the new resource or an error message as an array in the order the objects were processed. When errors are encountered, the batch process will attempt to continue for all submitted items.
767+
</p>
768+
769+
<p>
770+
<div class="exHeading">Javascript Example</div>
771+
<pre><code class="jsExample">
772+
<span>const saved_obj = await fetch("https://devstore.rerum.io/v1/api/bulkUpdate", {</span>
773+
<span class="ind1">method: "POST",</span>
774+
<span class="ind1">headers:{</span>
775+
<span class="ind2">"Authorization": "Bearer eyJz93a...k4laUWw" </span>
776+
<span class="ind2">"Content-Type": "application/json; charset=utf-8"</span>
777+
<span class="ind1">},</span>
778+
<span class="ind1">body: JSON.stringify([
779+
<span class="ind2">"@id": "https://devstore.rerum.io/v1/id/abcdef1234567890",</span>
780+
<span class="ind2">"hello": "new world",</span>
781+
<span class="ind2">"@id": "https://devstore.rerum.io/v1/id/1234567890abcdef",</span>
782+
<span class="ind2">"goodbye": "old planet"</span>
783+
<span class="ind1">])</span>
784+
<span>})</span>
785+
<span>.then(resp => resp.json())</span>
786+
<span>.catch(err => {throw err})
787+
</code></pre>
788+
</p>
789+
790+
<p>
791+
<div class="exHeading">Here is what the response <code>resp</code> looks like:</div>
792+
<pre><code class="respExample">
793+
<span>[</span>
794+
<span class="ind1">{</span>
795+
<span class="ind2">"@id": "https://devstore.rerum.io/v1/id/abcabc1231231230",</span>
796+
<span class="ind2">"hello": "new world",</span>
797+
<span class="ind2">"__rerum":{...}</span>
798+
<span class="ind1">},</span>
799+
<span class="ind1">{</span>
800+
<span class="ind2">"@id": "https://devstore.rerum.io/v1/id/defdef4564564567",</span>
801+
<span class="ind2">"goodbye": "old planet",</span>
802+
<span class="ind2"> "__rerum":{...}</span>
803+
<span class="ind1">}</span>
804+
<span>]</span>
805+
</code></pre>
806+
</p>
807+
733808
<h3 id="overwrite">Overwrite</h3>
734809

735810
<p>

rest.js

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,54 +29,52 @@ const checkPatchOverrideSupport = function (req, res) {
2929
* REST is all about communication. The response code and the textual body are particular.
3030
* RERUM is all about being clear. It will build custom responses sometimes for certain scenarios, will remaining RESTful.
3131
*
32-
* Note that the res upstream from this has been converted into err. res will not have what you are looking for, check err instead.
32+
* You have likely reached this with a next(createExpressError(err)) call. End here and send the error.
3333
*/
3434
const messenger = function (err, req, res, next) {
3535
if (res.headersSent) {
36-
next(err)
3736
return
3837
}
39-
err.message = err.message ?? res.message ?? ``
40-
if (err.statusCode === 401) {
38+
let error = {}
39+
error.message = err.statusMessage ?? err.message ?? ``
40+
error.status = err.statusCode ?? err.status ?? 500
41+
if (error.status === 401) {
4142
//Special handler for token errors from the oauth module
4243
//Token errors come through with a message that we want. That message is in the error's WWW-Authenticate header
4344
//Other 401s from our app come through with a status message. They may not have headers.
4445
if (err.headers?.["WWW-Authenticate"]) {
45-
err.message += err.headers["WWW-Authenticate"]
46+
error.message += err.headers["WWW-Authenticate"]
4647
}
4748
}
48-
let genericMessage = ""
4949
let token = req.header("Authorization")
5050
if(token && !token.startsWith("Bearer ")){
51-
err.message +=`
51+
error.message +=`
5252
Your token is not in the correct format. It should be a Bearer token formatted like: "Bearer <token>"`
53-
next(err)
54-
return
5553
}
56-
switch (err.statusCode) {
54+
switch (error.status) {
5755
case 400:
5856
//"Bad Request", most likely because the body and Content-Type are not aligned. Could be bad JSON.
59-
err.message += `
57+
error.message += `
6058
The body of your request was invalid. Please make sure it is a valid content-type and that the body matches that type.
6159
If the body is JSON, make sure it is valid JSON.`
6260
break
6361
case 401:
6462
//The requesting agent is known from the request. That agent does not match __rerum.generatedBy. Unauthorized.
6563
if (token) {
66-
err.message += `
64+
error.message += `
6765
The token provided is Unauthorized. Please check that it is your token and that it is not expired.
6866
Token: ${token} `
6967
}
7068
else {
71-
err.message += `
69+
error.message += `
7270
The request does not contain an "Authorization" header and so is Unauthorized. Please include a token with your requests
7371
like "Authorization: Bearer <token>". Make sure you have registered at ${process.env.RERUM_PREFIX}.`
7472
}
7573
break
7674
case 403:
7775
//Forbidden to use this. The provided Bearer does not have the required privileges.
7876
if (token) {
79-
err.message += `
77+
error.message += `
8078
You are Forbidden from performing this action. Check your privileges.
8179
Token: ${token}`
8280
}
@@ -87,24 +85,31 @@ You are Forbidden from performing this action. The request does not contain an "
8785
Make sure you have registered at ${process.env.RERUM_PREFIX}. `
8886
}
8987
case 404:
90-
err.message += `
88+
error.message += `
9189
The requested web page or resource could not be found.`
9290
break
9391
case 405:
9492
// These are all handled in api-routes.js already.
9593
break
94+
case 409:
95+
// These are all handled in db-controller.js already.
96+
break
97+
case 501:
98+
// Not implemented. Handled upstream.
99+
break
96100
case 503:
97101
//RERUM is down or readonly. Handled upstream.
98102
break
99103
case 500:
100104
default:
101105
//Really bad, probably not specifically caught.
102-
err.message += `
106+
error.message += `
103107
RERUM experienced a server issue while performing this action.
104108
It may not have completed at all, and most likely did not complete successfully.`
105109
}
106-
// res.status(statusCode).send(err.statusMessage)
107-
next(err)
110+
console.error(error)
111+
res.set("Content-Type", "text/plain; charset=utf-8")
112+
res.status(error.status).send(error.message)
108113
}
109114

110115
export default { checkPatchOverrideSupport, messenger }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { jest } from "@jest/globals"
2+
3+
// Only real way to test an express route is to mount it and call it so that we can use the req, res, next.
4+
import express from "express"
5+
import request from "supertest"
6+
import controller from '../../db-controller.js'
7+
8+
// Here is the auth mock so we get a req.user and the controller can function without a NPE.
9+
const addAuth = (req, res, next) => {
10+
req.user = {"http://store.rerum.io/agent": "https://store.rerum.io/v1/id/agent007"}
11+
next()
12+
}
13+
14+
const routeTester = new express()
15+
routeTester.use(express.json())
16+
routeTester.use(express.urlencoded({ extended: false }))
17+
18+
// Mount our own /bulkCreate route without auth that will use controller.bulkCreate
19+
routeTester.use("/bulkUpdate", [addAuth, controller.bulkUpdate])
20+
21+
it.skip("'/bulkUpdate' route functions", async () => {
22+
// TODO without hitting the v1/id/11111 object because it is already abused.
23+
})

routes/__tests__/create.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ it("'/create' route functions", async () => {
3838
expect(response.headers["link"]).toBeTruthy()
3939

4040
})
41+
42+
it.skip("Support setting valid '_id' on '/create' request body.", async () => {
43+
// TODO
44+
})

routes/__tests__/id.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ it("'/id/:id' route functions", async () => {
3232
expect(response.headers["location"]).toBeTruthy()
3333

3434
})
35+
36+
it.skip("Proper '@id-id' negotation on GET by URI.", async () => {
37+
// TODO
38+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { jest } from "@jest/globals"
2+
import dotenv from "dotenv"
3+
import controller from '../../db-controller.js'
4+
5+
it("Functional '@id-id' negotiation on objects returned.", async () => {
6+
let negotiate = {
7+
"@context": "http://iiif.io/api/presentation/3/context.json",
8+
"_id": "example",
9+
"@id": `${process.env.RERUM_ID_PREFIX}example`,
10+
"test": "item"
11+
}
12+
negotiate = controller.idNegotiation(negotiate)
13+
expect(negotiate._id).toBeUndefined()
14+
expect(negotiate["@id"]).toBeUndefined()
15+
expect(negotiate.id).toBe(`${process.env.RERUM_ID_PREFIX}example`)
16+
expect(negotiate.test).toBe("item")
17+
18+
let nonegotiate = {
19+
"@context":"http://example.org/context.json",
20+
"_id": "example",
21+
"@id": `${process.env.RERUM_ID_PREFIX}example`,
22+
"id": "test_example",
23+
"test":"item"
24+
}
25+
nonegotiate = controller.idNegotiation(nonegotiate)
26+
expect(nonegotiate._id).toBeUndefined()
27+
expect(nonegotiate["@id"]).toBe(`${process.env.RERUM_ID_PREFIX}example`)
28+
expect(nonegotiate.id).toBe("test_example")
29+
expect(nonegotiate.test).toBe("item")
30+
})

0 commit comments

Comments
 (0)