From f8cfc8e7d7f347defe38ec3e0b0094e2d6c5d82f Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 11 Jun 2020 14:35:45 +0100 Subject: [PATCH 1/4] adding record models for brevity --- packages/server/package.json | 2 +- packages/server/src/api/controllers/record.js | 11 +++++- packages/server/src/api/index.js | 4 ++ packages/server/src/api/routes/index.js | 2 + packages/server/src/api/routes/model.js | 38 +------------------ packages/server/src/api/routes/record.js | 36 ++++++++++++++++++ .../server/src/api/routes/tests/model.spec.js | 1 - .../src/api/routes/tests/record.spec.js | 24 ++++++++++++ 8 files changed, 78 insertions(+), 40 deletions(-) create mode 100644 packages/server/src/api/routes/record.js diff --git a/packages/server/package.json b/packages/server/package.json index 0888ef2ae4..49a5626f6f 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -25,7 +25,7 @@ "scripts": { "test": "jest routes --runInBand", "test:integration": "jest workflow --runInBand", - "test:watch": "jest -w", + "test:watch": "jest --watch", "initialise": "node ../cli/bin/budi init -b local -q", "budi": "node ../cli/bin/budi", "dev:builder": "nodemon ../cli/bin/budi run", diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index 5fab413f04..cf8eb27606 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -61,7 +61,7 @@ exports.fetchView = async function(ctx) { ctx.body = response.rows.map(row => row.doc) } -exports.fetchModel = async function(ctx) { +exports.fetchModelRecords = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) const response = await db.query(`database/all_${ctx.params.modelId}`, { include_docs: true, @@ -69,6 +69,15 @@ exports.fetchModel = async function(ctx) { ctx.body = response.rows.map(row => row.doc) } +exports.search = async function(ctx) { + const db = new CouchDB(ctx.params.instanceId) + const response = await db.allDocs({ + include_docs: true, + ...ctx.request.body, + }) + ctx.body = response.rows.map(row => row.doc) +} + exports.find = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) const record = await db.get(ctx.params.recordId) diff --git a/packages/server/src/api/index.js b/packages/server/src/api/index.js index 990714fdc7..e6143d6725 100644 --- a/packages/server/src/api/index.js +++ b/packages/server/src/api/index.js @@ -10,6 +10,7 @@ const { instanceRoutes, clientRoutes, applicationRoutes, + recordRoutes, modelRoutes, viewRoutes, staticRoutes, @@ -69,6 +70,9 @@ router.use(viewRoutes.allowedMethods()) router.use(modelRoutes.routes()) router.use(modelRoutes.allowedMethods()) +router.use(recordRoutes.routes()) +router.use(recordRoutes.allowedMethods()) + router.use(userRoutes.routes()) router.use(userRoutes.allowedMethods()) diff --git a/packages/server/src/api/routes/index.js b/packages/server/src/api/routes/index.js index c515d5f437..b50fee788a 100644 --- a/packages/server/src/api/routes/index.js +++ b/packages/server/src/api/routes/index.js @@ -5,6 +5,7 @@ const instanceRoutes = require("./instance") const clientRoutes = require("./client") const applicationRoutes = require("./application") const modelRoutes = require("./model") +const recordRoutes = require("./record") const viewRoutes = require("./view") const staticRoutes = require("./static") const componentRoutes = require("./component") @@ -18,6 +19,7 @@ module.exports = { instanceRoutes, clientRoutes, applicationRoutes, + recordRoutes, modelRoutes, viewRoutes, staticRoutes, diff --git a/packages/server/src/api/routes/model.js b/packages/server/src/api/routes/model.js index 388f4618bd..f1ec46dbe5 100644 --- a/packages/server/src/api/routes/model.js +++ b/packages/server/src/api/routes/model.js @@ -1,46 +1,10 @@ const Router = require("@koa/router") const modelController = require("../controllers/model") -const recordController = require("../controllers/record") const authorized = require("../../middleware/authorized") -const { - READ_MODEL, - WRITE_MODEL, - BUILDER, -} = require("../../utilities/accessLevels") +const { BUILDER } = require("../../utilities/accessLevels") const router = Router() -// records - -router - .get( - "/api/:instanceId/:modelId/records", - authorized(READ_MODEL, ctx => ctx.params.modelId), - recordController.fetchModel - ) - .get( - "/api/:instanceId/:modelId/records/:recordId", - authorized(READ_MODEL, ctx => ctx.params.modelId), - recordController.find - ) - .post( - "/api/:instanceId/:modelId/records", - authorized(WRITE_MODEL, ctx => ctx.params.modelId), - recordController.save - ) - .post( - "/api/:instanceId/:modelId/records/validate", - authorized(WRITE_MODEL, ctx => ctx.params.modelId), - recordController.validate - ) - .delete( - "/api/:instanceId/:modelId/records/:recordId/:revId", - authorized(WRITE_MODEL, ctx => ctx.params.modelId), - recordController.destroy - ) - -// models - router .get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch) .get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find) diff --git a/packages/server/src/api/routes/record.js b/packages/server/src/api/routes/record.js new file mode 100644 index 0000000000..d555d3d8c8 --- /dev/null +++ b/packages/server/src/api/routes/record.js @@ -0,0 +1,36 @@ +const Router = require("@koa/router") +const recordController = require("../controllers/record") +const authorized = require("../../middleware/authorized") +const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels") + +const router = Router() + +router + .get( + "/api/:instanceId/:modelId/records", + authorized(READ_MODEL, ctx => ctx.params.modelId), + recordController.fetchModelRecords + ) + .get( + "/api/:instanceId/:modelId/records/:recordId", + authorized(READ_MODEL, ctx => ctx.params.modelId), + recordController.find + ) + .post("/api/:instanceId/records/search", recordController.search) + .post( + "/api/:instanceId/:modelId/records", + authorized(WRITE_MODEL, ctx => ctx.params.modelId), + recordController.save + ) + .post( + "/api/:instanceId/:modelId/records/validate", + authorized(WRITE_MODEL, ctx => ctx.params.modelId), + recordController.validate + ) + .delete( + "/api/:instanceId/:modelId/records/:recordId/:revId", + authorized(WRITE_MODEL, ctx => ctx.params.modelId), + recordController.destroy + ) + +module.exports = router diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js index 65a44b677a..df3b7d8b52 100644 --- a/packages/server/src/api/routes/tests/model.spec.js +++ b/packages/server/src/api/routes/tests/model.spec.js @@ -97,7 +97,6 @@ describe("/models", () => { instanceId: instance._id, }) }) - }); describe("destroy", () => { diff --git a/packages/server/src/api/routes/tests/record.spec.js b/packages/server/src/api/routes/tests/record.spec.js index 2c8c542715..22ac67ecdc 100644 --- a/packages/server/src/api/routes/tests/record.spec.js +++ b/packages/server/src/api/routes/tests/record.spec.js @@ -110,6 +110,30 @@ describe("/records", () => { expect(res.body.find(r => r.name === record.name)).toBeDefined() }) + it("lists records when queried by their ID", async () => { + const newRecord = { + modelId: model._id, + name: "Second Contact", + status: "new" + } + const record = await createRecord() + const secondRecord = await createRecord(newRecord) + + const recordIds = [record.body._id, secondRecord.body._id] + + const res = await request + .post(`/api/${instance._id}/records/search`) + .set(defaultHeaders) + .send({ + keys: recordIds + }) + .expect('Content-Type', /json/) + .expect(200) + + expect(res.body.length).toBe(2) + expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(recordIds)) + }) + it("load should return 404 when record does not exist", async () => { await createRecord() await request From 3900b216e999cc381e02d5b8941ed496848f0697 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 11 Jun 2020 17:24:09 +0100 Subject: [PATCH 2/4] remove other link fields when you delete a model --- packages/server/src/api/controllers/model.js | 32 ++++++++++++++- .../src/api/routes/tests/couchTestUtils.js | 4 ++ .../server/src/api/routes/tests/model.spec.js | 40 ++++++++++++++++++- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index 9d06c7a413..c35a0ddd49 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -27,6 +27,23 @@ exports.create = async function(ctx) { const result = await db.post(newModel) newModel._rev = result.rev + const { schema } = ctx.request.body + for (let key in schema) { + // model has a linked record + if (schema[key].type === "link") { + // create the link field in the other model + const linkedModel = await db.get(schema[key].modelId); + linkedModel.schema[newModel.name] = { + type: "link", + modelId: newModel._id, + constraints: { + type: "array" + } + } + await db.put(linkedModel); + } + } + const designDoc = await db.get("_design/database") designDoc.views = { ...designDoc.views, @@ -50,7 +67,9 @@ exports.update = async function() {} exports.destroy = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) - await db.remove(ctx.params.modelId, ctx.params.revId) + const modelToDelete = await db.get(ctx.params.modelId); + + await db.remove(modelToDelete) const modelViewId = `all_${ctx.params.modelId}` // Delete all records for that model @@ -59,6 +78,17 @@ exports.destroy = async function(ctx) { records.rows.map(record => ({ id: record.id, _deleted: true })) ) + // Delete linked record fields in dependent models + for (let key in modelToDelete.schema) { + const { type, modelId } = modelToDelete.schema[key]; + if (type === "link") { + const linkedModel = await db.get(modelId); + delete linkedModel.schema[modelToDelete.name] + await db.put(linkedModel) + } + } + + // delete the "all" view const designDoc = await db.get("_design/database") delete designDoc.views[modelViewId] diff --git a/packages/server/src/api/routes/tests/couchTestUtils.js b/packages/server/src/api/routes/tests/couchTestUtils.js index 6029e080cc..495b841b10 100644 --- a/packages/server/src/api/routes/tests/couchTestUtils.js +++ b/packages/server/src/api/routes/tests/couchTestUtils.js @@ -253,3 +253,7 @@ exports.insertDocument = async (databaseId, document) => { exports.destroyDocument = async (databaseId, documentId) => { return await new CouchDB(databaseId).destroy(documentId) } + +exports.getDocument = async (databaseId, documentId) => { + return await new CouchDB(databaseId).get(documentId) +} diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js index df3b7d8b52..1fb16beb8b 100644 --- a/packages/server/src/api/routes/tests/model.spec.js +++ b/packages/server/src/api/routes/tests/model.spec.js @@ -3,9 +3,10 @@ const { createModel, supertest, createClientDatabase, - createApplication , + createApplication, defaultHeaders, - builderEndpointShouldBlockNormalUsers + builderEndpointShouldBlockNormalUsers, + getDocument } = require("./couchTestUtils") describe("/models", () => { @@ -119,6 +120,41 @@ describe("/models", () => { }); }) + it("deletes linked references to the model after deletion", async done => { + const linkedModel = await createModel(request, instance._id, { + name: "LinkedModel", + type: "model", + key: "name", + schema: { + name: { + type: "text", + constraints: { + type: "string", + }, + }, + TestModel: { + type: "link", + modelId: testModel._id, + constraints: { + type: "array" + } + } + }, + }) + + request + .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) + .set(defaultHeaders) + .expect('Content-Type', /json/) + .expect(200) + .end(async (_, res) => { + expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`); + const dependentModel = await getDocument(instance._id, linkedModel._id) + expect(dependentModel.schema.TestModel).not.toBeDefined(); + done(); + }); + }) + it("should apply authorization to endpoint", async () => { await builderEndpointShouldBlockNormalUsers({ request, From 2b312d4c1f8ca1dfb61341c5790c9c85f26a2e5d Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 11 Jun 2020 17:28:19 +0100 Subject: [PATCH 3/4] lint :sparkles: --- packages/server/src/api/controllers/model.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index c35a0ddd49..f1e3f51747 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -32,15 +32,15 @@ exports.create = async function(ctx) { // model has a linked record if (schema[key].type === "link") { // create the link field in the other model - const linkedModel = await db.get(schema[key].modelId); + const linkedModel = await db.get(schema[key].modelId) linkedModel.schema[newModel.name] = { type: "link", modelId: newModel._id, constraints: { - type: "array" - } + type: "array", + }, } - await db.put(linkedModel); + await db.put(linkedModel) } } @@ -67,7 +67,7 @@ exports.update = async function() {} exports.destroy = async function(ctx) { const db = new CouchDB(ctx.params.instanceId) - const modelToDelete = await db.get(ctx.params.modelId); + const modelToDelete = await db.get(ctx.params.modelId) await db.remove(modelToDelete) const modelViewId = `all_${ctx.params.modelId}` @@ -80,15 +80,14 @@ exports.destroy = async function(ctx) { // Delete linked record fields in dependent models for (let key in modelToDelete.schema) { - const { type, modelId } = modelToDelete.schema[key]; + const { type, modelId } = modelToDelete.schema[key] if (type === "link") { - const linkedModel = await db.get(modelId); + const linkedModel = await db.get(modelId) delete linkedModel.schema[modelToDelete.name] await db.put(linkedModel) } } - // delete the "all" view const designDoc = await db.get("_design/database") delete designDoc.views[modelViewId] From 4ed4866bca75c020108dab5cab200de7027d6340 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Thu, 11 Jun 2020 18:11:56 +0100 Subject: [PATCH 4/4] update _rev for deleted test model --- packages/server/src/api/controllers/model.js | 1 + packages/server/src/api/routes/tests/model.spec.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index f1e3f51747..650342b33c 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -70,6 +70,7 @@ exports.destroy = async function(ctx) { const modelToDelete = await db.get(ctx.params.modelId) await db.remove(modelToDelete) + const modelViewId = `all_${ctx.params.modelId}` // Delete all records for that model diff --git a/packages/server/src/api/routes/tests/model.spec.js b/packages/server/src/api/routes/tests/model.spec.js index 1fb16beb8b..7134245fb3 100644 --- a/packages/server/src/api/routes/tests/model.spec.js +++ b/packages/server/src/api/routes/tests/model.spec.js @@ -108,7 +108,11 @@ describe("/models", () => { testModel = await createModel(request, instance._id, testModel) }); - it("returns a success response when a model is deleted.", done => { + afterEach(() => { + delete testModel._rev + }) + + it("returns a success response when a model is deleted.", async done => { request .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) .set(defaultHeaders)