Merge branch 'master' of github.com:Budibase/budibase
This commit is contained in:
commit
71c6d737d9
|
@ -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",
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue