diff --git a/packages/server/src/api/controllers/accesslevel.js b/packages/server/src/api/controllers/accesslevel.js index 44b638ed41..a607ed13c3 100644 --- a/packages/server/src/api/controllers/accesslevel.js +++ b/packages/server/src/api/controllers/accesslevel.js @@ -6,13 +6,18 @@ const { POWERUSER_LEVEL_ID, ADMIN_LEVEL_ID, } = require("../../utilities/accessLevels") +const { + generateAccessLevelID, + getAccessLevelParams, +} = require("../../db/utils") exports.fetch = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) - const body = await db.query("database/by_type", { - include_docs: true, - key: ["accesslevel"], - }) + const body = await db.allDocs( + getAccessLevelParams(null, { + include_docs: true, + }) + ) const customAccessLevels = body.rows.map(row => row.doc) const staticAccessLevels = [ @@ -90,7 +95,7 @@ exports.create = async function(ctx) { name: ctx.request.body.name, _rev: ctx.request.body._rev, permissions: ctx.request.body.permissions || [], - _id: newid(), + _id: generateAccessLevelID(newid()), type: "accesslevel", } diff --git a/packages/server/src/api/controllers/application.js b/packages/server/src/api/controllers/application.js index eb5ca5ee95..b8d0d59344 100644 --- a/packages/server/src/api/controllers/application.js +++ b/packages/server/src/api/controllers/application.js @@ -12,17 +12,18 @@ const setBuilderToken = require("../../utilities/builder/setBuilderToken") const fs = require("fs-extra") const { promisify } = require("util") const chmodr = require("chmodr") +const { generateAppID, getAppParams } = require("../../db/utils") const { downloadExtractComponentLibraries, } = require("../../utilities/createAppPackage") exports.fetch = async function(ctx) { const db = new CouchDB(ClientDb.name(getClientId(ctx))) - const body = await db.query("client/by_type", { - include_docs: true, - key: ["app"], - }) - + const body = await db.allDocs( + getAppParams(null, { + include_docs: true, + }) + ) ctx.body = body.rows.map(row => row.doc) } @@ -48,7 +49,7 @@ exports.create = async function(ctx) { if (!clientId) { ctx.throw(400, "ClientId not suplied") } - const appId = newid() + const appId = generateAppID(newid()) // insert an appId -> clientId lookup const masterDb = new CouchDB("client_app_lookup") diff --git a/packages/server/src/api/controllers/automation.js b/packages/server/src/api/controllers/automation.js index 2c415fec8b..81b5a6b87b 100644 --- a/packages/server/src/api/controllers/automation.js +++ b/packages/server/src/api/controllers/automation.js @@ -3,6 +3,7 @@ const newid = require("../../db/newid") const actions = require("../../automations/actions") const logic = require("../../automations/logic") const triggers = require("../../automations/triggers") +const { getAutomationParams, generateAutomationID } = require("../../db/utils") /************************* * * @@ -34,7 +35,7 @@ exports.create = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) let automation = ctx.request.body - automation._id = newid() + automation._id = generateAutomationID(newid()) automation.type = "automation" automation = cleanAutomationInputs(automation) @@ -72,10 +73,11 @@ exports.update = async function(ctx) { exports.fetch = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) - const response = await db.query(`database/by_type`, { - key: ["automation"], - include_docs: true, - }) + const response = await db.allDocs( + getAutomationParams(null, { + include_docs: true, + }) + ) ctx.body = response.rows.map(row => row.doc) } diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index 0f38056753..59d2d66325 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -1,26 +1,31 @@ const CouchDB = require("../../db") const newid = require("../../db/newid") +const { + getRecordParams, + getModelParams, + generateModelID, +} = require("../../db/utils") exports.fetch = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) - const body = await db.query("database/by_type", { - include_docs: true, - key: ["model"], - }) + const body = await db.allDocs( + getModelParams(null, { + include_docs: true, + }) + ) ctx.body = body.rows.map(row => row.doc) } exports.find = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) - const model = await db.get(ctx.params.id) - ctx.body = model + ctx.body = await db.get(ctx.params.id) } exports.save = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) const modelToSave = { type: "model", - _id: newid(), + _id: generateModelID(newid()), views: {}, ...ctx.request.body, } @@ -28,9 +33,11 @@ exports.save = async function(ctx) { // rename record fields when table column is renamed const { _rename } = modelToSave if (_rename) { - const records = await db.query(`database/all_${modelToSave._id}`, { - include_docs: true, - }) + const records = await db.allDocs( + getRecordParams(modelToSave._id, null, { + include_docs: true, + }) + ) const docs = records.rows.map(({ doc }) => { doc[_rename.updated] = doc[_rename.old] delete doc[_rename.old] @@ -54,7 +61,7 @@ exports.save = async function(ctx) { modelToSave._rev = result.rev const { schema } = ctx.request.body - for (let key in schema) { + for (let key of Object.keys(schema)) { // model has a linked record if (schema[key].type === "link") { // create the link field in the other model @@ -71,19 +78,6 @@ exports.save = async function(ctx) { } } - const designDoc = await db.get("_design/database") - designDoc.views = { - ...designDoc.views, - [`all_${modelToSave._id}`]: { - map: `function(doc) { - if (doc.modelId === "${modelToSave._id}") { - emit(doc._id); - } - }`, - }, - } - await db.put(designDoc) - ctx.status = 200 ctx.message = `Model ${ctx.request.body.name} saved successfully.` ctx.body = modelToSave @@ -96,16 +90,18 @@ exports.destroy = async function(ctx) { await db.remove(modelToDelete) - const modelViewId = `all_${ctx.params.modelId}` - // Delete all records for that model - const records = await db.query(`database/${modelViewId}`) + const records = await db.allDocs( + getRecordParams(ctx.params.modelId, null, { + include_docs: true, + }) + ) await db.bulkDocs( records.rows.map(record => ({ _id: record.id, _deleted: true })) ) // Delete linked record fields in dependent models - for (let key in modelToDelete.schema) { + for (let key of Object.keys(modelToDelete.schema)) { const { type, modelId } = modelToDelete.schema[key] if (type === "link") { const linkedModel = await db.get(modelId) @@ -114,11 +110,6 @@ exports.destroy = async function(ctx) { } } - // delete the "all" view - const designDoc = await db.get("_design/database") - delete designDoc.views[modelViewId] - await db.put(designDoc) - ctx.status = 200 ctx.message = `Model ${ctx.params.modelId} deleted.` } diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index 2626667ef3..1344142d8e 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -1,6 +1,9 @@ const CouchDB = require("../../db") const validateJs = require("validate.js") const newid = require("../../db/newid") +const { getRecordParams, generateRecordID } = require("../../db/utils") + +const MODEL_VIEW_BEGINS_WITH = "all_model:" function emitEvent(eventType, ctx, record) { let event = { @@ -66,7 +69,7 @@ exports.save = async function(ctx) { record.modelId = ctx.params.modelId if (!record._rev && !record._id) { - record._id = newid() + record._id = generateRecordID(record.modelId, newid()) } const model = await db.get(record.modelId) @@ -133,7 +136,16 @@ exports.save = async function(ctx) { exports.fetchView = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) const { stats, group, field } = ctx.query - const response = await db.query(`database/${ctx.params.viewName}`, { + const viewName = ctx.params.viewName + + // if this is a model view being looked for just transfer to that + if (viewName.indexOf(MODEL_VIEW_BEGINS_WITH) === 0) { + ctx.params.modelId = viewName.substring(4) + await exports.fetchModelRecords(ctx) + return + } + + const response = await db.query(`database/${viewName}`, { include_docs: !stats, group, }) @@ -154,9 +166,11 @@ exports.fetchView = async function(ctx) { exports.fetchModelRecords = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) - const response = await db.query(`database/all_${ctx.params.modelId}`, { - include_docs: true, - }) + const response = await db.allDocs( + getRecordParams(ctx.params.modelId, null, { + include_docs: true, + }) + ) ctx.body = response.rows.map(row => row.doc) } diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index d1d393347c..4b213643d2 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -1,6 +1,7 @@ const CouchDB = require("../db") const emitter = require("../events/index") const InMemoryQueue = require("./queue/inMemoryQueue") +const { getAutomationParams } = require("../db/utils") let automationQueue = new InMemoryQueue() @@ -89,12 +90,8 @@ async function queueRelevantRecordAutomations(event, eventType) { throw `No instanceId specified for ${eventType} - check event emitters.` } const db = new CouchDB(event.instanceId) - const automationsToTrigger = await db.query( - "database/by_automation_trigger", - { - key: [eventType], - include_docs: true, - } + const automationsToTrigger = await db.allDocs( + getAutomationParams(null, { include_docs: true }) ) const automations = automationsToTrigger.rows.map(wf => wf.doc) diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js new file mode 100644 index 0000000000..8ea40b9757 --- /dev/null +++ b/packages/server/src/db/utils.js @@ -0,0 +1,89 @@ +const DocumentTypes = { + MODEL: "model", + RECORD: "record", + USER: "user", + AUTOMATION: "automation", + LINK: "link", + APP: "app", + ACCESS_LEVEL: "accesslevel", +} + +exports.DocumentTypes = DocumentTypes + +const UNICODE_MAX = "\ufff0" + +function singleInputParams(docType, input, otherProps = {}) { + if (input == null) { + input = "" + } + return { + ...otherProps, + startkey: `${docType}:${input}`, + endkey: `${docType}:${input}${UNICODE_MAX}`, + } +} + +exports.getModelParams = (modelId, otherProps = {}) => { + return singleInputParams(DocumentTypes.MODEL, modelId, otherProps) +} + +exports.generateModelID = modelId => { + return `${DocumentTypes.MODEL}:${modelId}` +} + +exports.getRecordParams = (modelId, recordId, otherProps = {}) => { + if (modelId == null && recordId != null) { + throw "Cannot build key for record ID without a model ID" + } + const endOfKey = recordId == null ? `${modelId}:` : `${modelId}:${recordId}` + return { + ...otherProps, + startkey: `${DocumentTypes.RECORD}:${endOfKey}`, + endkey: `${DocumentTypes.RECORD}:${endOfKey}${UNICODE_MAX}`, + } +} + +exports.generateRecordID = (modelId, recordId) => { + return `${DocumentTypes.RECORD}:${modelId}:${recordId}` +} + +exports.getUserParams = (username, otherProps = {}) => { + return singleInputParams(DocumentTypes.USER, username, otherProps) +} + +exports.generateUserID = username => { + return `${DocumentTypes.USER}:${username}` +} + +exports.getAutomationParams = (automationId, otherProps = {}) => { + return singleInputParams(DocumentTypes.AUTOMATION, automationId, otherProps) +} + +exports.generateAutomationID = automationId => { + return `${DocumentTypes.AUTOMATION}:${automationId}` +} + +// for now link records still have their own view as we can walk multiple directions +exports.generateLinkID = (modelId1, modelId2, recordId1, recordId2) => { + return `${DocumentTypes.AUTOMATION}:${modelId1}:${modelId2}:${recordId1}:${recordId2}` +} + +exports.generateAppID = appId => { + return `${DocumentTypes.APP}:${appId}` +} + +exports.getAppParams = (appId, otherProps = {}) => { + return singleInputParams(DocumentTypes.APP, appId, otherProps) +} + +exports.generateAccessLevelID = accessLevelId => { + return `${DocumentTypes.ACCESS_LEVEL}:${accessLevelId}` +} + +exports.getAccessLevelParams = (accessLevelId, otherProps) => { + return singleInputParams( + DocumentTypes.ACCESS_LEVEL, + accessLevelId, + otherProps + ) +}