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/model.js b/packages/server/src/api/controllers/model.js index 9d06c7a413..650342b33c 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,10 @@ 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 +79,16 @@ 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/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/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 65a44b677a..7134245fb3 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", () => { @@ -97,7 +98,6 @@ describe("/models", () => { instanceId: instance._id, }) }) - }); describe("destroy", () => { @@ -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) @@ -120,6 +124,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, 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