Merge branch 'master' of github.com:Budibase/budibase

This commit is contained in:
Michael Shanks 2020-06-12 12:15:32 +01:00
commit 71c6d737d9
10 changed files with 156 additions and 44 deletions

View File

@ -25,7 +25,7 @@
"scripts": { "scripts": {
"test": "jest routes --runInBand", "test": "jest routes --runInBand",
"test:integration": "jest workflow --runInBand", "test:integration": "jest workflow --runInBand",
"test:watch": "jest -w", "test:watch": "jest --watch",
"initialise": "node ../cli/bin/budi init -b local -q", "initialise": "node ../cli/bin/budi init -b local -q",
"budi": "node ../cli/bin/budi", "budi": "node ../cli/bin/budi",
"dev:builder": "nodemon ../cli/bin/budi run", "dev:builder": "nodemon ../cli/bin/budi run",

View File

@ -27,6 +27,23 @@ exports.create = async function(ctx) {
const result = await db.post(newModel) const result = await db.post(newModel)
newModel._rev = result.rev 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") const designDoc = await db.get("_design/database")
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,
@ -50,7 +67,10 @@ exports.update = async function() {}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) 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}` const modelViewId = `all_${ctx.params.modelId}`
// Delete all records for that model // Delete all records for that model
@ -59,6 +79,16 @@ exports.destroy = async function(ctx) {
records.rows.map(record => ({ id: record.id, _deleted: true })) 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 // delete the "all" view
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
delete designDoc.views[modelViewId] delete designDoc.views[modelViewId]

View File

@ -61,7 +61,7 @@ exports.fetchView = async function(ctx) {
ctx.body = response.rows.map(row => row.doc) 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 db = new CouchDB(ctx.params.instanceId)
const response = await db.query(`database/all_${ctx.params.modelId}`, { const response = await db.query(`database/all_${ctx.params.modelId}`, {
include_docs: true, include_docs: true,
@ -69,6 +69,15 @@ exports.fetchModel = async function(ctx) {
ctx.body = response.rows.map(row => row.doc) 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) { exports.find = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
const record = await db.get(ctx.params.recordId) const record = await db.get(ctx.params.recordId)

View File

@ -10,6 +10,7 @@ const {
instanceRoutes, instanceRoutes,
clientRoutes, clientRoutes,
applicationRoutes, applicationRoutes,
recordRoutes,
modelRoutes, modelRoutes,
viewRoutes, viewRoutes,
staticRoutes, staticRoutes,
@ -69,6 +70,9 @@ router.use(viewRoutes.allowedMethods())
router.use(modelRoutes.routes()) router.use(modelRoutes.routes())
router.use(modelRoutes.allowedMethods()) router.use(modelRoutes.allowedMethods())
router.use(recordRoutes.routes())
router.use(recordRoutes.allowedMethods())
router.use(userRoutes.routes()) router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods()) router.use(userRoutes.allowedMethods())

View File

@ -5,6 +5,7 @@ const instanceRoutes = require("./instance")
const clientRoutes = require("./client") const clientRoutes = require("./client")
const applicationRoutes = require("./application") const applicationRoutes = require("./application")
const modelRoutes = require("./model") const modelRoutes = require("./model")
const recordRoutes = require("./record")
const viewRoutes = require("./view") const viewRoutes = require("./view")
const staticRoutes = require("./static") const staticRoutes = require("./static")
const componentRoutes = require("./component") const componentRoutes = require("./component")
@ -18,6 +19,7 @@ module.exports = {
instanceRoutes, instanceRoutes,
clientRoutes, clientRoutes,
applicationRoutes, applicationRoutes,
recordRoutes,
modelRoutes, modelRoutes,
viewRoutes, viewRoutes,
staticRoutes, staticRoutes,

View File

@ -1,46 +1,10 @@
const Router = require("@koa/router") const Router = require("@koa/router")
const modelController = require("../controllers/model") const modelController = require("../controllers/model")
const recordController = require("../controllers/record")
const authorized = require("../../middleware/authorized") const authorized = require("../../middleware/authorized")
const { const { BUILDER } = require("../../utilities/accessLevels")
READ_MODEL,
WRITE_MODEL,
BUILDER,
} = require("../../utilities/accessLevels")
const router = Router() 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 router
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch) .get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
.get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find) .get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find)

View File

@ -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

View File

@ -253,3 +253,7 @@ exports.insertDocument = async (databaseId, document) => {
exports.destroyDocument = async (databaseId, documentId) => { exports.destroyDocument = async (databaseId, documentId) => {
return await new CouchDB(databaseId).destroy(documentId) return await new CouchDB(databaseId).destroy(documentId)
} }
exports.getDocument = async (databaseId, documentId) => {
return await new CouchDB(databaseId).get(documentId)
}

View File

@ -3,9 +3,10 @@ const {
createModel, createModel,
supertest, supertest,
createClientDatabase, createClientDatabase,
createApplication , createApplication,
defaultHeaders, defaultHeaders,
builderEndpointShouldBlockNormalUsers builderEndpointShouldBlockNormalUsers,
getDocument
} = require("./couchTestUtils") } = require("./couchTestUtils")
describe("/models", () => { describe("/models", () => {
@ -97,7 +98,6 @@ describe("/models", () => {
instanceId: instance._id, instanceId: instance._id,
}) })
}) })
}); });
describe("destroy", () => { describe("destroy", () => {
@ -108,7 +108,11 @@ describe("/models", () => {
testModel = await createModel(request, instance._id, testModel) 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 request
.delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`) .delete(`/api/${instance._id}/models/${testModel._id}/${testModel._rev}`)
.set(defaultHeaders) .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 () => { it("should apply authorization to endpoint", async () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,

View File

@ -110,6 +110,30 @@ describe("/records", () => {
expect(res.body.find(r => r.name === record.name)).toBeDefined() 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 () => { it("load should return 404 when record does not exist", async () => {
await createRecord() await createRecord()
await request await request