mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
154 changed files with 3972 additions and 3203 deletions
@ -1,46 +0,0 @@ |
|||||
<script> |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import { notifier } from "builderStore/store/notifications" |
|
||||
import RecordFieldControl from "../RecordFieldControl.svelte" |
|
||||
import * as api from "../api" |
|
||||
import { ModalContent } from "@budibase/bbui" |
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte" |
|
||||
|
|
||||
export let record = {} |
|
||||
|
|
||||
let errors = [] |
|
||||
|
|
||||
$: creating = record?._id == null |
|
||||
$: model = record.modelId |
|
||||
? $backendUiStore.models.find(model => model._id === record?.modelId) |
|
||||
: $backendUiStore.selectedModel |
|
||||
$: modelSchema = Object.entries(model?.schema ?? {}) |
|
||||
|
|
||||
async function saveRecord() { |
|
||||
const recordResponse = await api.saveRecord( |
|
||||
{ ...record, modelId: model._id }, |
|
||||
model._id |
|
||||
) |
|
||||
if (recordResponse.errors) { |
|
||||
errors = Object.keys(recordResponse.errors) |
|
||||
.map(k => ({ dataPath: k, message: recordResponse.errors[k] })) |
|
||||
.flat() |
|
||||
// Prevent modal closing if there were errors |
|
||||
return false |
|
||||
} |
|
||||
notifier.success("Record saved successfully.") |
|
||||
backendUiStore.actions.records.save(recordResponse) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ModalContent |
|
||||
title={creating ? 'Create Row' : 'Edit Row'} |
|
||||
confirmText={creating ? 'Create Row' : 'Save Row'} |
|
||||
onConfirm={saveRecord}> |
|
||||
<ErrorsBox {errors} /> |
|
||||
{#each modelSchema as [key, meta]} |
|
||||
<div> |
|
||||
<RecordFieldControl {meta} bind:value={record[key]} /> |
|
||||
</div> |
|
||||
{/each} |
|
||||
</ModalContent> |
|
||||
@ -0,0 +1,46 @@ |
|||||
|
<script> |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
import { notifier } from "builderStore/store/notifications" |
||||
|
import RowFieldControl from "../RowFieldControl.svelte" |
||||
|
import * as api from "../api" |
||||
|
import { ModalContent } from "@budibase/bbui" |
||||
|
import ErrorsBox from "components/common/ErrorsBox.svelte" |
||||
|
|
||||
|
export let row = {} |
||||
|
|
||||
|
let errors = [] |
||||
|
|
||||
|
$: creating = row?._id == null |
||||
|
$: table = row.tableId |
||||
|
? $backendUiStore.tables.find(table => table._id === row?.tableId) |
||||
|
: $backendUiStore.selectedTable |
||||
|
$: tableSchema = Object.entries(table?.schema ?? {}) |
||||
|
|
||||
|
async function saveRow() { |
||||
|
const rowResponse = await api.saveRow( |
||||
|
{ ...row, tableId: table._id }, |
||||
|
table._id |
||||
|
) |
||||
|
if (rowResponse.errors) { |
||||
|
errors = Object.keys(rowResponse.errors) |
||||
|
.map(k => ({ dataPath: k, message: rowResponse.errors[k] })) |
||||
|
.flat() |
||||
|
// Prevent modal closing if there were errors |
||||
|
return false |
||||
|
} |
||||
|
notifier.success("Row saved successfully.") |
||||
|
backendUiStore.actions.rows.save(rowResponse) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
title={creating ? 'Create Row' : 'Edit Row'} |
||||
|
confirmText={creating ? 'Create Row' : 'Save Row'} |
||||
|
onConfirm={saveRow}> |
||||
|
<ErrorsBox {errors} /> |
||||
|
{#each tableSchema as [key, meta]} |
||||
|
<div> |
||||
|
<RowFieldControl {meta} bind:value={row[key]} /> |
||||
|
</div> |
||||
|
{/each} |
||||
|
</ModalContent> |
||||
@ -1,53 +0,0 @@ |
|||||
<script> |
|
||||
import { onMount } from "svelte" |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import api from "builderStore/api" |
|
||||
import { Select, Label, Multiselect } from "@budibase/bbui" |
|
||||
import { capitalise } from "../../helpers" |
|
||||
|
|
||||
export let schema |
|
||||
export let linkedRecords = [] |
|
||||
|
|
||||
let records = [] |
|
||||
|
|
||||
$: label = capitalise(schema.name) |
|
||||
$: linkedModelId = schema.modelId |
|
||||
$: linkedModel = $backendUiStore.models.find( |
|
||||
model => model._id === linkedModelId |
|
||||
) |
|
||||
$: fetchRecords(linkedModelId) |
|
||||
|
|
||||
async function fetchRecords(linkedModelId) { |
|
||||
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records` |
|
||||
try { |
|
||||
const response = await api.get(FETCH_RECORDS_URL) |
|
||||
records = await response.json() |
|
||||
} catch (error) { |
|
||||
console.log(error) |
|
||||
records = [] |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function getPrettyName(record) { |
|
||||
return record[linkedModel.primaryDisplay || "_id"] |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
{#if linkedModel.primaryDisplay == null} |
|
||||
<Label extraSmall grey>{label}</Label> |
|
||||
<Label small black> |
|
||||
Please choose a primary display column for the |
|
||||
<b>{linkedModel.name}</b> |
|
||||
table. |
|
||||
</Label> |
|
||||
{:else} |
|
||||
<Multiselect |
|
||||
secondary |
|
||||
bind:value={linkedRecords} |
|
||||
{label} |
|
||||
placeholder="Choose some options"> |
|
||||
{#each records as record} |
|
||||
<option value={record._id}>{getPrettyName(record)}</option> |
|
||||
{/each} |
|
||||
</Multiselect> |
|
||||
{/if} |
|
||||
@ -0,0 +1,53 @@ |
|||||
|
<script> |
||||
|
import { onMount } from "svelte" |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
import api from "builderStore/api" |
||||
|
import { Select, Label, Multiselect } from "@budibase/bbui" |
||||
|
import { capitalise } from "../../helpers" |
||||
|
|
||||
|
export let schema |
||||
|
export let linkedRows = [] |
||||
|
|
||||
|
let rows = [] |
||||
|
|
||||
|
$: label = capitalise(schema.name) |
||||
|
$: linkedTableId = schema.tableId |
||||
|
$: linkedTable = $backendUiStore.tables.find( |
||||
|
table => table._id === linkedTableId |
||||
|
) |
||||
|
$: fetchRows(linkedTableId) |
||||
|
|
||||
|
async function fetchRows(linkedTableId) { |
||||
|
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows` |
||||
|
try { |
||||
|
const response = await api.get(FETCH_ROWS_URL) |
||||
|
rows = await response.json() |
||||
|
} catch (error) { |
||||
|
console.log(error) |
||||
|
rows = [] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getPrettyName(row) { |
||||
|
return row[linkedTable.primaryDisplay || "_id"] |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
{#if linkedTable.primaryDisplay == null} |
||||
|
<Label extraSmall grey>{label}</Label> |
||||
|
<Label small black> |
||||
|
Please choose a primary display column for the |
||||
|
<b>{linkedTable.name}</b> |
||||
|
table. |
||||
|
</Label> |
||||
|
{:else} |
||||
|
<Multiselect |
||||
|
secondary |
||||
|
bind:value={linkedRows} |
||||
|
{label} |
||||
|
placeholder="Choose some options"> |
||||
|
{#each rows as row} |
||||
|
<option value={row._id}>{getPrettyName(row)}</option> |
||||
|
{/each} |
||||
|
</Multiselect> |
||||
|
{/if} |
||||
@ -1,6 +1,6 @@ |
|||||
<script> |
<script> |
||||
import { goto } from "@sveltech/routify" |
import { goto } from "@sveltech/routify" |
||||
$goto("../model") |
$goto("../table") |
||||
</script> |
</script> |
||||
|
|
||||
<!-- routify:options index=false --> |
<!-- routify:options index=false --> |
||||
|
|||||
@ -1,15 +0,0 @@ |
|||||
<script> |
|
||||
import { params } from "@sveltech/routify" |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
|
|
||||
if ($params.selectedModel) { |
|
||||
const model = $backendUiStore.models.find( |
|
||||
m => m._id === $params.selectedModel |
|
||||
) |
|
||||
if (model) { |
|
||||
backendUiStore.actions.models.select(model) |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<slot /> |
|
||||
@ -1,32 +0,0 @@ |
|||||
<script> |
|
||||
import { backendUiStore } from "builderStore" |
|
||||
import { goto, leftover } from "@sveltech/routify" |
|
||||
import { onMount } from "svelte" |
|
||||
|
|
||||
async function selectModel(model) { |
|
||||
backendUiStore.actions.models.select(model) |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
|
||||
// navigate to first model in list, if not already selected |
|
||||
// and this is the final url (i.e. no selectedModel) |
|
||||
if ( |
|
||||
!$leftover && |
|
||||
$backendUiStore.models.length > 0 && |
|
||||
(!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id) |
|
||||
) { |
|
||||
$goto(`./${$backendUiStore.models[0]._id}`) |
|
||||
} |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
<div class="root"> |
|
||||
<slot /> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.root { |
|
||||
height: 100%; |
|
||||
position: relative; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,35 +0,0 @@ |
|||||
<script> |
|
||||
import { store, backendUiStore } from "builderStore" |
|
||||
import { goto, leftover } from "@sveltech/routify" |
|
||||
import { onMount } from "svelte" |
|
||||
|
|
||||
async function selectModel(model) { |
|
||||
backendUiStore.actions.models.select(model) |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
|
||||
// navigate to first model in list, if not already selected |
|
||||
// and this is the final url (i.e. no selectedModel) |
|
||||
if ( |
|
||||
!$leftover && |
|
||||
$backendUiStore.models.length > 0 && |
|
||||
(!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id) |
|
||||
) { |
|
||||
// this file routes as .../models/index, so, go up one. |
|
||||
$goto(`../${$backendUiStore.models[0]._id}`) |
|
||||
} |
|
||||
}) |
|
||||
</script> |
|
||||
|
|
||||
{#if $backendUiStore.models.length === 0} |
|
||||
<i>Create your first table to start building</i> |
|
||||
{:else} |
|
||||
<i>Select a table to edit</i> |
|
||||
{/if} |
|
||||
|
|
||||
<style> |
|
||||
i { |
|
||||
font-size: var(--font-size-xl); |
|
||||
color: var(--grey-4); |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,15 @@ |
|||||
|
<script> |
||||
|
import { params } from "@sveltech/routify" |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
|
||||
|
if ($params.selectedTable) { |
||||
|
const table = $backendUiStore.tables.find( |
||||
|
m => m._id === $params.selectedTable |
||||
|
) |
||||
|
if (table) { |
||||
|
backendUiStore.actions.tables.select(table) |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<slot /> |
||||
@ -1,12 +1,12 @@ |
|||||
<script> |
<script> |
||||
import ModelDataTable from "components/backend/DataTable/ModelDataTable.svelte" |
import TableDataTable from "components/backend/DataTable/DataTable.svelte" |
||||
import { backendUiStore } from "builderStore" |
import { backendUiStore } from "builderStore" |
||||
|
|
||||
$: selectedModel = $backendUiStore.selectedModel |
$: selectedTable = $backendUiStore.selectedTable |
||||
</script> |
</script> |
||||
|
|
||||
{#if $backendUiStore.selectedDatabase._id && selectedModel.name} |
{#if $backendUiStore.selectedDatabase._id && selectedTable.name} |
||||
<ModelDataTable /> |
<TableDataTable /> |
||||
{:else} |
{:else} |
||||
<i>Create your first table to start building</i> |
<i>Create your first table to start building</i> |
||||
{/if} |
{/if} |
||||
@ -0,0 +1,32 @@ |
|||||
|
<script> |
||||
|
import { backendUiStore } from "builderStore" |
||||
|
import { goto, leftover } from "@sveltech/routify" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
async function selectTable(table) { |
||||
|
backendUiStore.actions.tables.select(table) |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
// navigate to first table in list, if not already selected |
||||
|
// and this is the final url (i.e. no selectedTable) |
||||
|
if ( |
||||
|
!$leftover && |
||||
|
$backendUiStore.tables.length > 0 && |
||||
|
(!$backendUiStore.selectedTable || !$backendUiStore.selectedTable._id) |
||||
|
) { |
||||
|
$goto(`./${$backendUiStore.tables[0]._id}`) |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
height: 100%; |
||||
|
position: relative; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,35 @@ |
|||||
|
<script> |
||||
|
import { store, backendUiStore } from "builderStore" |
||||
|
import { goto, leftover } from "@sveltech/routify" |
||||
|
import { onMount } from "svelte" |
||||
|
|
||||
|
async function selectTable(table) { |
||||
|
backendUiStore.actions.tables.select(table) |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
// navigate to first table in list, if not already selected |
||||
|
// and this is the final url (i.e. no selectedTable) |
||||
|
if ( |
||||
|
!$leftover && |
||||
|
$backendUiStore.tables.length > 0 && |
||||
|
(!$backendUiStore.selectedTable || !$backendUiStore.selectedTable._id) |
||||
|
) { |
||||
|
// this file routes as .../tables/index, so, go up one. |
||||
|
$goto(`../${$backendUiStore.tables[0]._id}`) |
||||
|
} |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
{#if $backendUiStore.tables.length === 0} |
||||
|
<i>Create your first table to start building</i> |
||||
|
{:else} |
||||
|
<i>Select a table to edit</i> |
||||
|
{/if} |
||||
|
|
||||
|
<style> |
||||
|
i { |
||||
|
font-size: var(--font-size-xl); |
||||
|
color: var(--grey-4); |
||||
|
} |
||||
|
</style> |
||||
@ -1,146 +0,0 @@ |
|||||
const CouchDB = require("../../db") |
|
||||
const linkRecords = require("../../db/linkedRecords") |
|
||||
const csvParser = require("../../utilities/csvParser") |
|
||||
const { |
|
||||
getRecordParams, |
|
||||
getModelParams, |
|
||||
generateModelID, |
|
||||
generateRecordID, |
|
||||
} = require("../../db/utils") |
|
||||
|
|
||||
exports.fetch = async function(ctx) { |
|
||||
const db = new CouchDB(ctx.user.instanceId) |
|
||||
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) |
|
||||
ctx.body = await db.get(ctx.params.id) |
|
||||
} |
|
||||
|
|
||||
exports.save = async function(ctx) { |
|
||||
const instanceId = ctx.user.instanceId |
|
||||
const db = new CouchDB(instanceId) |
|
||||
const { dataImport, ...rest } = ctx.request.body |
|
||||
const modelToSave = { |
|
||||
type: "model", |
|
||||
_id: generateModelID(), |
|
||||
views: {}, |
|
||||
...rest, |
|
||||
} |
|
||||
let renameDocs = [] |
|
||||
|
|
||||
// if the model obj had an _id then it will have been retrieved
|
|
||||
const oldModel = ctx.preExisting |
|
||||
|
|
||||
// rename record fields when table column is renamed
|
|
||||
const { _rename } = modelToSave |
|
||||
if (_rename && modelToSave.schema[_rename.updated].type === "link") { |
|
||||
throw "Cannot rename a linked field." |
|
||||
} else if (_rename && modelToSave.primaryDisplay === _rename.old) { |
|
||||
throw "Cannot rename the primary display field." |
|
||||
} else if (_rename) { |
|
||||
const records = await db.allDocs( |
|
||||
getRecordParams(modelToSave._id, null, { |
|
||||
include_docs: true, |
|
||||
}) |
|
||||
) |
|
||||
renameDocs = records.rows.map(({ doc }) => { |
|
||||
doc[_rename.updated] = doc[_rename.old] |
|
||||
delete doc[_rename.old] |
|
||||
return doc |
|
||||
}) |
|
||||
delete modelToSave._rename |
|
||||
} |
|
||||
|
|
||||
// update schema of non-statistics views when new columns are added
|
|
||||
for (let view in modelToSave.views) { |
|
||||
const modelView = modelToSave.views[view] |
|
||||
if (!modelView) continue |
|
||||
|
|
||||
if (modelView.schema.group || modelView.schema.field) continue |
|
||||
modelView.schema = modelToSave.schema |
|
||||
} |
|
||||
|
|
||||
// update linked records
|
|
||||
await linkRecords.updateLinks({ |
|
||||
instanceId, |
|
||||
eventType: oldModel |
|
||||
? linkRecords.EventType.MODEL_UPDATED |
|
||||
: linkRecords.EventType.MODEL_SAVE, |
|
||||
model: modelToSave, |
|
||||
oldModel: oldModel, |
|
||||
}) |
|
||||
|
|
||||
// don't perform any updates until relationships have been
|
|
||||
// checked by the updateLinks function
|
|
||||
if (renameDocs.length !== 0) { |
|
||||
await db.bulkDocs(renameDocs) |
|
||||
} |
|
||||
const result = await db.post(modelToSave) |
|
||||
modelToSave._rev = result.rev |
|
||||
|
|
||||
ctx.eventEmitter && |
|
||||
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave) |
|
||||
|
|
||||
if (dataImport && dataImport.path) { |
|
||||
// Populate the table with records imported from CSV in a bulk update
|
|
||||
const data = await csvParser.transform(dataImport) |
|
||||
|
|
||||
for (let row of data) { |
|
||||
row._id = generateRecordID(modelToSave._id) |
|
||||
row.modelId = modelToSave._id |
|
||||
} |
|
||||
|
|
||||
await db.bulkDocs(data) |
|
||||
} |
|
||||
|
|
||||
ctx.status = 200 |
|
||||
ctx.message = `Model ${ctx.request.body.name} saved successfully.` |
|
||||
ctx.body = modelToSave |
|
||||
} |
|
||||
|
|
||||
exports.destroy = async function(ctx) { |
|
||||
const instanceId = ctx.user.instanceId |
|
||||
const db = new CouchDB(instanceId) |
|
||||
const modelToDelete = await db.get(ctx.params.modelId) |
|
||||
|
|
||||
// Delete all records for that model
|
|
||||
const records = await db.allDocs( |
|
||||
getRecordParams(ctx.params.modelId, null, { |
|
||||
include_docs: true, |
|
||||
}) |
|
||||
) |
|
||||
await db.bulkDocs( |
|
||||
records.rows.map(record => ({ ...record.doc, _deleted: true })) |
|
||||
) |
|
||||
|
|
||||
// update linked records
|
|
||||
await linkRecords.updateLinks({ |
|
||||
instanceId, |
|
||||
eventType: linkRecords.EventType.MODEL_DELETE, |
|
||||
model: modelToDelete, |
|
||||
}) |
|
||||
|
|
||||
// don't remove the table itself until very end
|
|
||||
await db.remove(modelToDelete) |
|
||||
|
|
||||
ctx.eventEmitter && |
|
||||
ctx.eventEmitter.emitModel(`model:delete`, instanceId, modelToDelete) |
|
||||
ctx.status = 200 |
|
||||
ctx.message = `Model ${ctx.params.modelId} deleted.` |
|
||||
} |
|
||||
|
|
||||
exports.validateCSVSchema = async function(ctx) { |
|
||||
const { file, schema = {} } = ctx.request.body |
|
||||
const result = await csvParser.parse(file.path, schema) |
|
||||
ctx.body = { |
|
||||
schema: result, |
|
||||
path: file.path, |
|
||||
} |
|
||||
} |
|
||||
@ -1,382 +0,0 @@ |
|||||
const CouchDB = require("../../db") |
|
||||
const validateJs = require("validate.js") |
|
||||
const linkRecords = require("../../db/linkedRecords") |
|
||||
const { |
|
||||
getRecordParams, |
|
||||
generateRecordID, |
|
||||
DocumentTypes, |
|
||||
SEPARATOR, |
|
||||
} = require("../../db/utils") |
|
||||
const { cloneDeep } = require("lodash") |
|
||||
|
|
||||
const MODEL_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.MODEL}${SEPARATOR}` |
|
||||
|
|
||||
validateJs.extend(validateJs.validators.datetime, { |
|
||||
parse: function(value) { |
|
||||
return new Date(value).getTime() |
|
||||
}, |
|
||||
// Input is a unix timestamp
|
|
||||
format: function(value) { |
|
||||
return new Date(value).toISOString() |
|
||||
}, |
|
||||
}) |
|
||||
|
|
||||
exports.patch = async function(ctx) { |
|
||||
const instanceId = ctx.user.instanceId |
|
||||
const db = new CouchDB(instanceId) |
|
||||
let record = await db.get(ctx.params.id) |
|
||||
const model = await db.get(record.modelId) |
|
||||
const patchfields = ctx.request.body |
|
||||
record = coerceRecordValues(record, model) |
|
||||
|
|
||||
for (let key of Object.keys(patchfields)) { |
|
||||
if (!model.schema[key]) continue |
|
||||
record[key] = patchfields[key] |
|
||||
} |
|
||||
|
|
||||
const validateResult = await validate({ |
|
||||
record, |
|
||||
model, |
|
||||
}) |
|
||||
|
|
||||
if (!validateResult.valid) { |
|
||||
ctx.status = 400 |
|
||||
ctx.body = { |
|
||||
status: 400, |
|
||||
errors: validateResult.errors, |
|
||||
} |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// returned record is cleaned and prepared for writing to DB
|
|
||||
record = await linkRecords.updateLinks({ |
|
||||
instanceId, |
|
||||
eventType: linkRecords.EventType.RECORD_UPDATE, |
|
||||
record, |
|
||||
modelId: record.modelId, |
|
||||
model, |
|
||||
}) |
|
||||
const response = await db.put(record) |
|
||||
record._rev = response.rev |
|
||||
record.type = "record" |
|
||||
|
|
||||
ctx.eventEmitter && |
|
||||
ctx.eventEmitter.emitRecord(`record:update`, instanceId, record, model) |
|
||||
ctx.body = record |
|
||||
ctx.status = 200 |
|
||||
ctx.message = `${model.name} updated successfully.` |
|
||||
} |
|
||||
|
|
||||
exports.save = async function(ctx) { |
|
||||
if (ctx.request.body.type === "delete") { |
|
||||
await bulkDelete(ctx) |
|
||||
} else { |
|
||||
await saveRecord(ctx) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
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, |
|
||||
}) |
|
||||
|
|
||||
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) |
|
||||
} |
|
||||
|
|
||||
ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows) |
|
||||
} |
|
||||
|
|
||||
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) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
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) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
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.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 |
|
||||
|
|
||||
// for automations include the record that was deleted
|
|
||||
ctx.record = record |
|
||||
ctx.eventEmitter && |
|
||||
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, 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 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 |
|
||||
} |
|
||||
return { valid: Object.keys(errors).length === 0, errors } |
|
||||
} |
|
||||
|
|
||||
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 |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
ctx.body = 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 |
|
||||
} |
|
||||
|
|
||||
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, |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
async function bulkDelete(ctx) { |
|
||||
const instanceId = ctx.user.instanceId |
|
||||
const { records } = ctx.request.body |
|
||||
const db = new CouchDB(ctx.user.instanceId) |
|
||||
|
|
||||
await db.bulkDocs( |
|
||||
records.map( |
|
||||
record => ({ ...record, _deleted: true }), |
|
||||
err => { |
|
||||
if (err) { |
|
||||
ctx.status = 500 |
|
||||
} else { |
|
||||
records.forEach(record => { |
|
||||
ctx.eventEmitter && |
|
||||
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record) |
|
||||
}) |
|
||||
ctx.status = 200 |
|
||||
} |
|
||||
} |
|
||||
) |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
async function saveRecord(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) |
|
||||
} |
|
||||
|
|
||||
// if the record obj had an _id then it will have been retrieved
|
|
||||
const existingRecord = ctx.preExisting |
|
||||
|
|
||||
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 |
|
||||
} |
|
||||
|
|
||||
// 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 |
|
||||
} |
|
||||
|
|
||||
record.type = "record" |
|
||||
const response = await db.post(record) |
|
||||
record._rev = response.rev |
|
||||
|
|
||||
ctx.eventEmitter && |
|
||||
ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model) |
|
||||
ctx.body = record |
|
||||
ctx.status = 200 |
|
||||
ctx.message = `${model.name} created successfully` |
|
||||
} |
|
||||
@ -0,0 +1,350 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const validateJs = require("validate.js") |
||||
|
const linkRows = require("../../db/linkedRows") |
||||
|
const { |
||||
|
getRowParams, |
||||
|
generateRowID, |
||||
|
DocumentTypes, |
||||
|
SEPARATOR, |
||||
|
} = require("../../db/utils") |
||||
|
const { cloneDeep } = require("lodash") |
||||
|
|
||||
|
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}` |
||||
|
|
||||
|
validateJs.extend(validateJs.validators.datetime, { |
||||
|
parse: function(value) { |
||||
|
return new Date(value).getTime() |
||||
|
}, |
||||
|
// Input is a unix timestamp
|
||||
|
format: function(value) { |
||||
|
return new Date(value).toISOString() |
||||
|
}, |
||||
|
}) |
||||
|
|
||||
|
exports.patch = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
let row = await db.get(ctx.params.id) |
||||
|
const table = await db.get(row.tableId) |
||||
|
const patchfields = ctx.request.body |
||||
|
row = coerceRowValues(row, table) |
||||
|
|
||||
|
for (let key of Object.keys(patchfields)) { |
||||
|
if (!table.schema[key]) continue |
||||
|
row[key] = patchfields[key] |
||||
|
} |
||||
|
|
||||
|
const validateResult = await validate({ |
||||
|
row, |
||||
|
table, |
||||
|
}) |
||||
|
|
||||
|
if (!validateResult.valid) { |
||||
|
ctx.status = 400 |
||||
|
ctx.body = { |
||||
|
status: 400, |
||||
|
errors: validateResult.errors, |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// returned row is cleaned and prepared for writing to DB
|
||||
|
row = await linkRows.updateLinks({ |
||||
|
instanceId, |
||||
|
eventType: linkRows.EventType.ROW_UPDATE, |
||||
|
row, |
||||
|
tableId: row.tableId, |
||||
|
table, |
||||
|
}) |
||||
|
const response = await db.put(row) |
||||
|
row._rev = response.rev |
||||
|
row.type = "row" |
||||
|
|
||||
|
ctx.eventEmitter && |
||||
|
ctx.eventEmitter.emitRow(`row:update`, instanceId, row, table) |
||||
|
ctx.body = row |
||||
|
ctx.status = 200 |
||||
|
ctx.message = `${table.name} updated successfully.` |
||||
|
} |
||||
|
|
||||
|
exports.save = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
let row = ctx.request.body |
||||
|
row.tableId = ctx.params.tableId |
||||
|
|
||||
|
if (!row._rev && !row._id) { |
||||
|
row._id = generateRowID(row.tableId) |
||||
|
} |
||||
|
|
||||
|
// if the row obj had an _id then it will have been retrieved
|
||||
|
const existingRow = ctx.preExisting |
||||
|
|
||||
|
const table = await db.get(row.tableId) |
||||
|
|
||||
|
row = coerceRowValues(row, table) |
||||
|
|
||||
|
const validateResult = await validate({ |
||||
|
row, |
||||
|
table, |
||||
|
}) |
||||
|
|
||||
|
if (!validateResult.valid) { |
||||
|
ctx.status = 400 |
||||
|
ctx.body = { |
||||
|
status: 400, |
||||
|
errors: validateResult.errors, |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// make sure link rows are up to date
|
||||
|
row = await linkRows.updateLinks({ |
||||
|
instanceId, |
||||
|
eventType: linkRows.EventType.ROW_SAVE, |
||||
|
row, |
||||
|
tableId: row.tableId, |
||||
|
table, |
||||
|
}) |
||||
|
|
||||
|
if (existingRow) { |
||||
|
const response = await db.put(row) |
||||
|
row._rev = response.rev |
||||
|
row.type = "row" |
||||
|
ctx.body = row |
||||
|
ctx.status = 200 |
||||
|
ctx.message = `${table.name} updated successfully.` |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
row.type = "row" |
||||
|
const response = await db.post(row) |
||||
|
row._rev = response.rev |
||||
|
|
||||
|
ctx.eventEmitter && |
||||
|
ctx.eventEmitter.emitRow(`row:save`, instanceId, row, table) |
||||
|
ctx.body = row |
||||
|
ctx.status = 200 |
||||
|
ctx.message = `${table.name} created successfully` |
||||
|
} |
||||
|
|
||||
|
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 table view being looked for just transfer to that
|
||||
|
if (viewName.indexOf(TABLE_VIEW_BEGINS_WITH) === 0) { |
||||
|
ctx.params.tableId = viewName.substring(4) |
||||
|
await exports.fetchTableRows(ctx) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const response = await db.query(`database/${viewName}`, { |
||||
|
include_docs: !stats, |
||||
|
group, |
||||
|
}) |
||||
|
|
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows) |
||||
|
} |
||||
|
|
||||
|
exports.fetchTableRows = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const response = await db.allDocs( |
||||
|
getRowParams(ctx.params.tableId, null, { |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
ctx.body = response.rows.map(row => row.doc) |
||||
|
ctx.body = await linkRows.attachLinkInfo( |
||||
|
instanceId, |
||||
|
response.rows.map(row => row.doc) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
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 linkRows.attachLinkInfo( |
||||
|
instanceId, |
||||
|
response.rows.map(row => row.doc) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
exports.find = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const row = await db.get(ctx.params.rowId) |
||||
|
if (row.tableId !== ctx.params.tableId) { |
||||
|
ctx.throw(400, "Supplied tableId does not match the rows tableId") |
||||
|
return |
||||
|
} |
||||
|
ctx.body = await linkRows.attachLinkInfo(instanceId, row) |
||||
|
} |
||||
|
|
||||
|
exports.destroy = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const row = await db.get(ctx.params.rowId) |
||||
|
if (row.tableId !== ctx.params.tableId) { |
||||
|
ctx.throw(400, "Supplied tableId doesn't match the row's tableId") |
||||
|
return |
||||
|
} |
||||
|
await linkRows.updateLinks({ |
||||
|
instanceId, |
||||
|
eventType: linkRows.EventType.ROW_DELETE, |
||||
|
row, |
||||
|
tableId: row.tableId, |
||||
|
}) |
||||
|
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId) |
||||
|
ctx.status = 200 |
||||
|
|
||||
|
// for automations include the row that was deleted
|
||||
|
ctx.row = row |
||||
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, instanceId, row) |
||||
|
} |
||||
|
|
||||
|
exports.validate = async function(ctx) { |
||||
|
const errors = await validate({ |
||||
|
instanceId: ctx.user.instanceId, |
||||
|
tableId: ctx.params.tableId, |
||||
|
row: ctx.request.body, |
||||
|
}) |
||||
|
ctx.status = 200 |
||||
|
ctx.body = errors |
||||
|
} |
||||
|
|
||||
|
async function validate({ instanceId, tableId, row, table }) { |
||||
|
if (!table) { |
||||
|
const db = new CouchDB(instanceId) |
||||
|
table = await db.get(tableId) |
||||
|
} |
||||
|
const errors = {} |
||||
|
for (let fieldName of Object.keys(table.schema)) { |
||||
|
const res = validateJs.single( |
||||
|
row[fieldName], |
||||
|
table.schema[fieldName].constraints |
||||
|
) |
||||
|
if (res) errors[fieldName] = res |
||||
|
} |
||||
|
return { valid: Object.keys(errors).length === 0, errors } |
||||
|
} |
||||
|
|
||||
|
exports.fetchEnrichedRow = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const tableId = ctx.params.tableId |
||||
|
const rowId = ctx.params.rowId |
||||
|
if (instanceId == null || tableId == null || rowId == null) { |
||||
|
ctx.status = 400 |
||||
|
ctx.body = { |
||||
|
status: 400, |
||||
|
error: |
||||
|
"Cannot handle request, URI params have not been successfully prepared.", |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
// need table to work out where links go in row
|
||||
|
const [table, row] = await Promise.all([db.get(tableId), db.get(rowId)]) |
||||
|
// get the link docs
|
||||
|
const linkVals = await linkRows.getLinkDocuments({ |
||||
|
instanceId, |
||||
|
tableId, |
||||
|
rowId, |
||||
|
}) |
||||
|
// look up the actual rows 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 rows for any links they may have
|
||||
|
let linkedRows = await linkRows.attachLinkInfo( |
||||
|
instanceId, |
||||
|
response.rows.map(row => row.doc) |
||||
|
) |
||||
|
// insert the link rows in the correct place throughout the main row
|
||||
|
for (let fieldName of Object.keys(table.schema)) { |
||||
|
let field = table.schema[fieldName] |
||||
|
if (field.type === "link") { |
||||
|
row[fieldName] = linkedRows.filter( |
||||
|
linkRow => linkRow.tableId === field.tableId |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
ctx.body = row |
||||
|
ctx.status = 200 |
||||
|
} |
||||
|
|
||||
|
function coerceRowValues(rec, table) { |
||||
|
const row = cloneDeep(rec) |
||||
|
for (let [key, value] of Object.entries(row)) { |
||||
|
const field = table.schema[key] |
||||
|
if (!field) continue |
||||
|
|
||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||
|
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) { |
||||
|
row[key] = TYPE_TRANSFORM_MAP[field.type][value] |
||||
|
} else if (TYPE_TRANSFORM_MAP[field.type].parse) { |
||||
|
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value) |
||||
|
} |
||||
|
} |
||||
|
return row |
||||
|
} |
||||
|
|
||||
|
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, |
||||
|
}, |
||||
|
} |
||||
@ -0,0 +1,144 @@ |
|||||
|
const CouchDB = require("../../db") |
||||
|
const linkRows = require("../../db/linkedRows") |
||||
|
const csvParser = require("../../utilities/csvParser") |
||||
|
const { |
||||
|
getRowParams, |
||||
|
getTableParams, |
||||
|
generateTableID, |
||||
|
generateRowID, |
||||
|
} = require("../../db/utils") |
||||
|
|
||||
|
exports.fetch = async function(ctx) { |
||||
|
const db = new CouchDB(ctx.user.instanceId) |
||||
|
const body = await db.allDocs( |
||||
|
getTableParams(null, { |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
ctx.body = body.rows.map(row => row.doc) |
||||
|
} |
||||
|
|
||||
|
exports.find = async function(ctx) { |
||||
|
const db = new CouchDB(ctx.user.instanceId) |
||||
|
ctx.body = await db.get(ctx.params.id) |
||||
|
} |
||||
|
|
||||
|
exports.save = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const { dataImport, ...rest } = ctx.request.body |
||||
|
const tableToSave = { |
||||
|
type: "table", |
||||
|
_id: generateTableID(), |
||||
|
views: {}, |
||||
|
...rest, |
||||
|
} |
||||
|
let renameDocs = [] |
||||
|
|
||||
|
// if the table obj had an _id then it will have been retrieved
|
||||
|
const oldTable = ctx.preExisting |
||||
|
|
||||
|
// rename row fields when table column is renamed
|
||||
|
const { _rename } = tableToSave |
||||
|
if (_rename && tableToSave.schema[_rename.updated].type === "link") { |
||||
|
throw "Cannot rename a linked field." |
||||
|
} else if (_rename && tableToSave.primaryDisplay === _rename.old) { |
||||
|
throw "Cannot rename the primary display field." |
||||
|
} else if (_rename) { |
||||
|
const rows = await db.allDocs( |
||||
|
getRowParams(tableToSave._id, null, { |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
renameDocs = rows.rows.map(({ doc }) => { |
||||
|
doc[_rename.updated] = doc[_rename.old] |
||||
|
delete doc[_rename.old] |
||||
|
return doc |
||||
|
}) |
||||
|
delete tableToSave._rename |
||||
|
} |
||||
|
|
||||
|
// update schema of non-statistics views when new columns are added
|
||||
|
for (let view in tableToSave.views) { |
||||
|
const tableView = tableToSave.views[view] |
||||
|
if (!tableView) continue |
||||
|
|
||||
|
if (tableView.schema.group || tableView.schema.field) continue |
||||
|
tableView.schema = tableToSave.schema |
||||
|
} |
||||
|
|
||||
|
// update linked rows
|
||||
|
await linkRows.updateLinks({ |
||||
|
instanceId, |
||||
|
eventType: oldTable |
||||
|
? linkRows.EventType.TABLE_UPDATED |
||||
|
: linkRows.EventType.TABLE_SAVE, |
||||
|
table: tableToSave, |
||||
|
oldTable: oldTable, |
||||
|
}) |
||||
|
|
||||
|
// don't perform any updates until relationships have been
|
||||
|
// checked by the updateLinks function
|
||||
|
if (renameDocs.length !== 0) { |
||||
|
await db.bulkDocs(renameDocs) |
||||
|
} |
||||
|
const result = await db.post(tableToSave) |
||||
|
tableToSave._rev = result.rev |
||||
|
|
||||
|
ctx.eventEmitter && |
||||
|
ctx.eventEmitter.emitTable(`table:save`, instanceId, tableToSave) |
||||
|
|
||||
|
if (dataImport && dataImport.path) { |
||||
|
// Populate the table with rows imported from CSV in a bulk update
|
||||
|
const data = await csvParser.transform(dataImport) |
||||
|
|
||||
|
for (let row of data) { |
||||
|
row._id = generateRowID(tableToSave._id) |
||||
|
row.tableId = tableToSave._id |
||||
|
} |
||||
|
|
||||
|
await db.bulkDocs(data) |
||||
|
} |
||||
|
|
||||
|
ctx.status = 200 |
||||
|
ctx.message = `Table ${ctx.request.body.name} saved successfully.` |
||||
|
ctx.body = tableToSave |
||||
|
} |
||||
|
|
||||
|
exports.destroy = async function(ctx) { |
||||
|
const instanceId = ctx.user.instanceId |
||||
|
const db = new CouchDB(instanceId) |
||||
|
const tableToDelete = await db.get(ctx.params.tableId) |
||||
|
|
||||
|
// Delete all rows for that table
|
||||
|
const rows = await db.allDocs( |
||||
|
getRowParams(ctx.params.tableId, null, { |
||||
|
include_docs: true, |
||||
|
}) |
||||
|
) |
||||
|
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true }))) |
||||
|
|
||||
|
// update linked rows
|
||||
|
await linkRows.updateLinks({ |
||||
|
instanceId, |
||||
|
eventType: linkRows.EventType.TABLE_DELETE, |
||||
|
table: tableToDelete, |
||||
|
}) |
||||
|
|
||||
|
// don't remove the table itself until very end
|
||||
|
await db.remove(tableToDelete) |
||||
|
|
||||
|
ctx.eventEmitter && |
||||
|
ctx.eventEmitter.emitTable(`table:delete`, instanceId, tableToDelete) |
||||
|
ctx.status = 200 |
||||
|
ctx.message = `Table ${ctx.params.tableId} deleted.` |
||||
|
} |
||||
|
|
||||
|
exports.validateCSVSchema = async function(ctx) { |
||||
|
const { file, schema = {} } = ctx.request.body |
||||
|
const result = await csvParser.parse(file.path, schema) |
||||
|
ctx.body = { |
||||
|
schema: result, |
||||
|
path: file.path, |
||||
|
} |
||||
|
} |
||||
@ -1,27 +0,0 @@ |
|||||
const Router = require("@koa/router") |
|
||||
const modelController = require("../controllers/model") |
|
||||
const authorized = require("../../middleware/authorized") |
|
||||
const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels") |
|
||||
|
|
||||
const router = Router() |
|
||||
|
|
||||
router |
|
||||
.get("/api/models", authorized(BUILDER), modelController.fetch) |
|
||||
.get( |
|
||||
"/api/models/:id", |
|
||||
authorized(READ_MODEL, ctx => ctx.params.id), |
|
||||
modelController.find |
|
||||
) |
|
||||
.post("/api/models", authorized(BUILDER), modelController.save) |
|
||||
.post( |
|
||||
"/api/models/csv/validate", |
|
||||
authorized(BUILDER), |
|
||||
modelController.validateCSVSchema |
|
||||
) |
|
||||
.delete( |
|
||||
"/api/models/:modelId/:revId", |
|
||||
authorized(BUILDER), |
|
||||
modelController.destroy |
|
||||
) |
|
||||
|
|
||||
module.exports = router |
|
||||
@ -1,49 +0,0 @@ |
|||||
const Router = require("@koa/router") |
|
||||
const recordController = require("../controllers/record") |
|
||||
const authorized = require("../../middleware/authorized") |
|
||||
const usage = require("../../middleware/usageQuota") |
|
||||
const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels") |
|
||||
|
|
||||
const router = Router() |
|
||||
|
|
||||
router |
|
||||
.get( |
|
||||
"/api/:modelId/:recordId/enrich", |
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId), |
|
||||
recordController.fetchEnrichedRecord |
|
||||
) |
|
||||
.get( |
|
||||
"/api/:modelId/records", |
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId), |
|
||||
recordController.fetchModelRecords |
|
||||
) |
|
||||
.get( |
|
||||
"/api/:modelId/records/:recordId", |
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId), |
|
||||
recordController.find |
|
||||
) |
|
||||
.post("/api/records/search", recordController.search) |
|
||||
.post( |
|
||||
"/api/:modelId/records", |
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId), |
|
||||
usage, |
|
||||
recordController.save |
|
||||
) |
|
||||
.patch( |
|
||||
"/api/:modelId/records/:id", |
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId), |
|
||||
recordController.patch |
|
||||
) |
|
||||
.post( |
|
||||
"/api/:modelId/records/validate", |
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId), |
|
||||
recordController.validate |
|
||||
) |
|
||||
.delete( |
|
||||
"/api/:modelId/records/:recordId/:revId", |
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId), |
|
||||
usage, |
|
||||
recordController.destroy |
|
||||
) |
|
||||
|
|
||||
module.exports = router |
|
||||
@ -0,0 +1,49 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const rowController = require("../controllers/row") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const usage = require("../../middleware/usageQuota") |
||||
|
const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
router |
||||
|
.get( |
||||
|
"/api/:tableId/:rowId/enrich", |
||||
|
authorized(READ_TABLE, ctx => ctx.params.tableId), |
||||
|
rowController.fetchEnrichedRow |
||||
|
) |
||||
|
.get( |
||||
|
"/api/:tableId/rows", |
||||
|
authorized(READ_TABLE, ctx => ctx.params.tableId), |
||||
|
rowController.fetchTableRows |
||||
|
) |
||||
|
.get( |
||||
|
"/api/:tableId/rows/:rowId", |
||||
|
authorized(READ_TABLE, ctx => ctx.params.tableId), |
||||
|
rowController.find |
||||
|
) |
||||
|
.post("/api/rows/search", rowController.search) |
||||
|
.post( |
||||
|
"/api/:tableId/rows", |
||||
|
authorized(WRITE_TABLE, ctx => ctx.params.tableId), |
||||
|
usage, |
||||
|
rowController.save |
||||
|
) |
||||
|
.patch( |
||||
|
"/api/:tableId/rows/:id", |
||||
|
authorized(WRITE_TABLE, ctx => ctx.params.tableId), |
||||
|
rowController.patch |
||||
|
) |
||||
|
.post( |
||||
|
"/api/:tableId/rows/validate", |
||||
|
authorized(WRITE_TABLE, ctx => ctx.params.tableId), |
||||
|
rowController.validate |
||||
|
) |
||||
|
.delete( |
||||
|
"/api/:tableId/rows/:rowId/:revId", |
||||
|
authorized(WRITE_TABLE, ctx => ctx.params.tableId), |
||||
|
usage, |
||||
|
rowController.destroy |
||||
|
) |
||||
|
|
||||
|
module.exports = router |
||||
@ -0,0 +1,27 @@ |
|||||
|
const Router = require("@koa/router") |
||||
|
const tableController = require("../controllers/table") |
||||
|
const authorized = require("../../middleware/authorized") |
||||
|
const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels") |
||||
|
|
||||
|
const router = Router() |
||||
|
|
||||
|
router |
||||
|
.get("/api/tables", authorized(BUILDER), tableController.fetch) |
||||
|
.get( |
||||
|
"/api/tables/:id", |
||||
|
authorized(READ_TABLE, ctx => ctx.params.id), |
||||
|
tableController.find |
||||
|
) |
||||
|
.post("/api/tables", authorized(BUILDER), tableController.save) |
||||
|
.post( |
||||
|
"/api/tables/csv/validate", |
||||
|
authorized(BUILDER), |
||||
|
tableController.validateCSVSchema |
||||
|
) |
||||
|
.delete( |
||||
|
"/api/tables/:tableId/:revId", |
||||
|
authorized(BUILDER), |
||||
|
tableController.destroy |
||||
|
) |
||||
|
|
||||
|
module.exports = router |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue