diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index 5f0043081..4a270256a 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -63,308 +63,308 @@ exports.patch = async function (ctx) { } exports.save = async function (ctx) { + if (ctx.request.body.type === 'delete') { + await bulkDelete(ctx) + } else { + await saveRecords(ctx) + } +} + +exports.fetchView = async function (ctx) { const instanceId = ctx.user.instanceId const db = new CouchDB(instanceId) - let record = ctx.request.body - record.modelId = ctx.params.modelId + const { stats, group, field } = ctx.query + const viewName = ctx.params.viewName - if (!record._rev && !record._id) { - record._id = generateRecordID(record.modelId) + // 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 } - exports.save = async function (ctx) { - const db = new CouchDB(ctx.user.instanceId) - let record = ctx.request.body - record.modelId = ctx.params.modelId - - if (!record._rev && !record._id) { - record._id = generateRecordID(record.modelId) - } - const model = await db.get(record.modelId) - - record = coerceRecordValues(record, model) - - const validateResult = await validate({ - record, - model, - }) + const response = await db.query(`database/${viewName}`, { + include_docs: !stats, + group, + }) - if (!validateResult.valid) { - ctx.status = 400 - ctx.body = { - status: 400, - errors: validateResult.errors, - } - return - } + if (stats) { + response.rows = response.rows.map(row => ({ + group: row.key, + field, + ...row.value, + avg: row.value.sum / row.value.count, + })) + } else { + response.rows = response.rows.map(row => row.doc) + } - const existingRecord = record._rev && (await db.get(record._id)) + ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows) +} - // make sure link records are up to date - record = await linkRecords.updateLinks({ - instanceId, - eventType: linkRecords.EventType.RECORD_SAVE, - record, - modelId: record.modelId, - model, +exports.fetchModelRecords = async function (ctx) { + const instanceId = ctx.user.instanceId + const db = new CouchDB(instanceId) + const response = await db.allDocs( + getRecordParams(ctx.params.modelId, null, { + include_docs: true, }) + ) + ctx.body = response.rows.map(row => row.doc) + ctx.body = await linkRecords.attachLinkInfo( + instanceId, + response.rows.map(row => row.doc) + ) +} - if (existingRecord) { - const response = await db.put(record) - record._rev = response.rev - record.type = "record" - ctx.body = record - ctx.status = 200 - ctx.message = `${model.name} updated successfully.` - return - } - - record.type = "record" - const response = await db.post(record) - record._rev = response.rev +exports.search = async function (ctx) { + const instanceId = ctx.user.instanceId + const db = new CouchDB(instanceId) + const response = await db.allDocs({ + include_docs: true, + ...ctx.request.body, + }) + ctx.body = await linkRecords.attachLinkInfo( + instanceId, + response.rows.map(row => row.doc) + ) +} - ctx.eventEmitter && - ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model) - ctx.body = record - ctx.status = 200 - ctx.message = `${model.name} created successfully` +exports.find = async function (ctx) { + const instanceId = ctx.user.instanceId + const db = new CouchDB(instanceId) + const record = await db.get(ctx.params.recordId) + if (record.modelId !== ctx.params.modelId) { + ctx.throw(400, "Supplied modelId does not match the records modelId") + return } + ctx.body = await linkRecords.attachLinkInfo(instanceId, record) +} - exports.fetchView = async function (ctx) { - const instanceId = ctx.user.instanceId - const db = new CouchDB(instanceId) - const { stats, group, field } = ctx.query - 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, - }) +exports.destroy = async function (ctx) { + const instanceId = ctx.user.instanceId + const db = new CouchDB(instanceId) + const record = await db.get(ctx.params.recordId) + if (record.modelId !== ctx.params.modelId) { + ctx.throw(400, "Supplied modelId doesn't match the record's modelId") + return + } + await linkRecords.updateLinks({ + instanceId, + eventType: linkRecords.EventType.RECORD_DELETE, + record, + modelId: record.modelId, + }) + ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) + ctx.status = 200 - if (stats) { - response.rows = response.rows.map(row => ({ - group: row.key, - field, - ...row.value, - avg: row.value.sum / row.value.count, - })) - } else { - response.rows = response.rows.map(row => row.doc) - } + // for automations include the record that was deleted + ctx.record = record + ctx.eventEmitter && + ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record) +} - ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows) - } +exports.validate = async function (ctx) { + const errors = await validate({ + instanceId: ctx.user.instanceId, + modelId: ctx.params.modelId, + record: ctx.request.body, + }) + ctx.status = 200 + ctx.body = errors +} - exports.fetchModelRecords = async function (ctx) { - const instanceId = ctx.user.instanceId +async function validate({ instanceId, modelId, record, model }) { + if (!model) { const db = new CouchDB(instanceId) - const response = await db.allDocs( - getRecordParams(ctx.params.modelId, null, { - include_docs: true, - }) - ) - ctx.body = response.rows.map(row => row.doc) - ctx.body = await linkRecords.attachLinkInfo( - instanceId, - response.rows.map(row => row.doc) - ) + model = await db.get(modelId) } - - exports.search = async function (ctx) { - const instanceId = ctx.user.instanceId - const db = new CouchDB(instanceId) - const response = await db.allDocs({ - include_docs: true, - ...ctx.request.body, - }) - ctx.body = await linkRecords.attachLinkInfo( - instanceId, - response.rows.map(row => row.doc) + const errors = {} + for (let fieldName of Object.keys(model.schema)) { + const res = validateJs.single( + record[fieldName], + model.schema[fieldName].constraints ) + if (res) errors[fieldName] = res } + return { valid: Object.keys(errors).length === 0, errors } +} - exports.find = async function (ctx) { - const instanceId = ctx.user.instanceId - const db = new CouchDB(instanceId) - const record = await db.get(ctx.params.recordId) - if (record.modelId !== ctx.params.modelId) { - ctx.throw(400, "Supplied modelId does not match the records modelId") - return +exports.fetchEnrichedRecord = async function (ctx) { + const instanceId = ctx.user.instanceId + const db = new CouchDB(instanceId) + const modelId = ctx.params.modelId + const recordId = ctx.params.recordId + if (instanceId == null || modelId == null || recordId == null) { + ctx.status = 400 + ctx.body = { + status: 400, + error: + "Cannot handle request, URI params have not been successfully prepared.", } - ctx.body = await linkRecords.attachLinkInfo(instanceId, record) + return } - - exports.destroy = async function (ctx) { - const instanceId = ctx.user.instanceId - const db = new CouchDB(instanceId) - const record = await db.get(ctx.params.recordId) - if (record.modelId !== ctx.params.modelId) { - ctx.throw(400, "Supplied modelId doesn't match the record's modelId") - return + // need model to work out where links go in record + const [model, record] = await Promise.all([db.get(modelId), db.get(recordId)]) + // get the link docs + const linkVals = await linkRecords.getLinkDocuments({ + instanceId, + modelId, + recordId, + }) + // look up the actual records based on the ids + const response = await db.allDocs({ + include_docs: true, + keys: linkVals.map(linkVal => linkVal.id), + }) + // need to include the IDs in these records for any links they may have + let linkedRecords = await linkRecords.attachLinkInfo( + instanceId, + response.rows.map(row => row.doc) + ) + // insert the link records in the correct place throughout the main record + for (let fieldName of Object.keys(model.schema)) { + let field = model.schema[fieldName] + if (field.type === "link") { + record[fieldName] = linkedRecords.filter( + linkRecord => linkRecord.modelId === field.modelId + ) } - await linkRecords.updateLinks({ - instanceId, - eventType: linkRecords.EventType.RECORD_DELETE, - record, - modelId: record.modelId, - }) - ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) - ctx.status = 200 - - // for automations include the record that was deleted - ctx.record = record - ctx.eventEmitter && - ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record) } + ctx.body = record + ctx.status = 200 +} - async function bulkDelete(ctx) { - const { records } = ctx.request.body - const db = new CouchDB(ctx.user.instanceId) - - await db.bulkDocs( - records.map(record => ({ ...record, _deleted: true }), (err, res) => { - if (err) { - ctx.status = 500 - } else { - records.forEach(record => { - emitEvent(`record:delete`, ctx, record) - }) - ctx.status = 200 - } - })) +function coerceRecordValues(rec, model) { + const record = cloneDeep(rec) + for (let [key, value] of Object.entries(record)) { + const field = model.schema[key] + if (!field) continue + + // eslint-disable-next-line no-prototype-builtins + if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) { + record[key] = TYPE_TRANSFORM_MAP[field.type][value] + } else if (TYPE_TRANSFORM_MAP[field.type].parse) { + record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value) + } } + return record +} - exports.validate = async function (ctx) { - const errors = await validate({ - instanceId: ctx.user.instanceId, - modelId: ctx.params.modelId, - record: ctx.request.body, - }) - ctx.status = 200 - ctx.body = errors + +async function bulkDelete(ctx) { + const { records } = ctx.request.body + const db = new CouchDB(ctx.user.instanceId) + + await db.bulkDocs( + records.map(record => ({ ...record, _deleted: true }), (err, res) => { + if (err) { + ctx.status = 500 + } else { + records.forEach(record => { + emitEvent(`record:delete`, ctx, record) + }) + ctx.status = 200 + } + })) +} +async function saveRecords(ctx) { + const instanceId = ctx.user.instanceId + const db = new CouchDB(instanceId) + let record = ctx.request.body + record.modelId = ctx.params.modelId + + if (!record._rev && !record._id) { + record._id = generateRecordID(record.modelId) } - async function validate({ instanceId, modelId, record, model }) { - if (!model) { - const db = new CouchDB(instanceId) - model = await db.get(modelId) - } - const errors = {} - for (let fieldName of Object.keys(model.schema)) { - const res = validateJs.single( - record[fieldName], - model.schema[fieldName].constraints - ) - if (res) errors[fieldName] = res + const model = await db.get(record.modelId) + + record = coerceRecordValues(record, model) + + const validateResult = await validate({ + record, + model, + }) + + if (!validateResult.valid) { + ctx.status = 400 + ctx.body = { + status: 400, + errors: validateResult.errors, } - return { valid: Object.keys(errors).length === 0, errors } + return } - exports.fetchEnrichedRecord = async function (ctx) { - const instanceId = ctx.user.instanceId - const db = new CouchDB(instanceId) - const modelId = ctx.params.modelId - const recordId = ctx.params.recordId - if (instanceId == null || modelId == null || recordId == null) { - ctx.status = 400 - ctx.body = { - status: 400, - error: - "Cannot handle request, URI params have not been successfully prepared.", - } - return - } - // need model to work out where links go in record - const [model, record] = await Promise.all([db.get(modelId), db.get(recordId)]) - // get the link docs - const linkVals = await linkRecords.getLinkDocuments({ - instanceId, - modelId, - recordId, - }) - // look up the actual records based on the ids - const response = await db.allDocs({ - include_docs: true, - keys: linkVals.map(linkVal => linkVal.id), - }) - // need to include the IDs in these records for any links they may have - let linkedRecords = await linkRecords.attachLinkInfo( - instanceId, - response.rows.map(row => row.doc) - ) - // insert the link records in the correct place throughout the main record - for (let fieldName of Object.keys(model.schema)) { - let field = model.schema[fieldName] - if (field.type === "link") { - record[fieldName] = linkedRecords.filter( - linkRecord => linkRecord.modelId === field.modelId - ) - } - } + const existingRecord = record._rev && (await db.get(record._id)) + + // make sure link records are up to date + record = await linkRecords.updateLinks({ + instanceId, + eventType: linkRecords.EventType.RECORD_SAVE, + record, + modelId: record.modelId, + model, + }) + + if (existingRecord) { + const response = await db.put(record) + record._rev = response.rev + record.type = "record" ctx.body = record ctx.status = 200 + ctx.message = `${model.name} updated successfully.` + return } - function coerceRecordValues(rec, model) { - const record = cloneDeep(rec) - for (let [key, value] of Object.entries(record)) { - const field = model.schema[key] - if (!field) continue - - // eslint-disable-next-line no-prototype-builtins - if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) { - record[key] = TYPE_TRANSFORM_MAP[field.type][value] - } else if (TYPE_TRANSFORM_MAP[field.type].parse) { - record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value) - } - } - return record - } + record.type = "record" + const response = await db.post(record) + record._rev = response.rev - const TYPE_TRANSFORM_MAP = { - link: { - "": [], - [null]: [], - [undefined]: undefined, - }, - options: { - "": "", - [null]: "", - [undefined]: undefined, - }, - string: { - "": "", - [null]: "", - [undefined]: undefined, - }, - number: { - "": null, - [null]: null, - [undefined]: undefined, - parse: n => parseFloat(n), - }, - datetime: { - "": null, - [undefined]: undefined, - [null]: null, - }, - attachment: { - "": [], - [null]: [], - [undefined]: undefined, - }, - boolean: { - "": null, - [null]: null, - [undefined]: undefined, - true: true, - false: false, - }, - } + ctx.eventEmitter && + ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model) + ctx.body = record + ctx.status = 200 + ctx.message = `${model.name} created successfully` +} + +const TYPE_TRANSFORM_MAP = { + link: { + "": [], + [null]: [], + [undefined]: undefined, + }, + options: { + "": "", + [null]: "", + [undefined]: undefined, + }, + string: { + "": "", + [null]: "", + [undefined]: undefined, + }, + number: { + "": null, + [null]: null, + [undefined]: undefined, + parse: n => parseFloat(n), + }, + datetime: { + "": null, + [undefined]: undefined, + [null]: null, + }, + attachment: { + "": [], + [null]: [], + [undefined]: undefined, + }, + boolean: { + "": null, + [null]: null, + [undefined]: undefined, + true: true, + false: false, + }, +}