Changing record -> row in this update, completing the update of renaming in the builder, this release needs further testing.

This commit is contained in:
mike12345567 2020-10-09 19:10:28 +01:00
parent fdaa69ee7f
commit 5d49d529e3
84 changed files with 816 additions and 816 deletions

View File

@ -145,7 +145,7 @@ The HTML and CSS for your apps runtime pages, as well as the budibase client lib
#### Backend
The backend schema, tables and records are stored using PouchDB when developing locally, and in [CouchDB](https://pouchdb.com/) when running in production.
/he backend schema, tables and rows are stored using PouchDB when developing locally, and in [CouchDB](https://pouchdb.com/) when running in production.
### Publishing Budibase to NPM

View File

@ -16,7 +16,7 @@ context("Create a automation", () => {
cy.contains("automate").click()
cy.contains("Create New Automation").click()
cy.get(".modal").within(() => {
cy.get("input").type("Add Record")
cy.get("input").type("Add Row")
cy.get(".buttons")
.contains("Create")
.click()
@ -24,7 +24,7 @@ context("Create a automation", () => {
// Add trigger
cy.get("[data-cy=add-automation-component]").click()
cy.get("[data-cy=RECORD_SAVED]").click()
cy.get("[data-cy=ROW_SAVED]").click()
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
@ -32,7 +32,7 @@ context("Create a automation", () => {
})
// Create action
cy.get("[data-cy=CREATE_RECORD]").click()
cy.get("[data-cy=CREATE_ROW]").click()
cy.get("[data-cy=automation-block-setup]").within(() => {
cy.get("select")
.first()
@ -53,9 +53,9 @@ context("Create a automation", () => {
cy.get(".stop-button.highlighted").should("be.visible")
})
it("should add record when a new record is added", () => {
it("should add row when a new row is added", () => {
cy.contains("backend").click()
cy.addRecord(["Rover", 15])
cy.addRow(["Rover", 15])
cy.reload()
cy.contains("goodboy").should("have.text", "goodboy")
})

View File

@ -6,7 +6,7 @@ xcontext('Create Components', () => {
// https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description')
cy.createTable('dog', 'name', 'age')
cy.addRecord('bob', '15')
cy.addRow('bob', '15')
})
// https://on.cypress.io/interacting-with-elements

View File

@ -16,8 +16,8 @@ context("Create a Table", () => {
cy.contains("name").should("be.visible")
})
it("creates a record in the table", () => {
cy.addRecord(["Rover"])
it("creates a row in the table", () => {
cy.addRow(["Rover"])
cy.contains("Rover").should("be.visible")
})
@ -32,7 +32,7 @@ context("Create a Table", () => {
cy.contains("nameupdated").should("have.text", "nameupdated")
})
it("edits a record", () => {
it("edits a row", () => {
cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=edit-row]").click()
cy.get(".modal input").type("Updated")
@ -40,7 +40,7 @@ context("Create a Table", () => {
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
})
it("deletes a record", () => {
it("deletes a row", () => {
cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=delete-row]").click()
cy.contains("Delete Row").click()

View File

@ -7,13 +7,13 @@ context("Create a View", () => {
cy.addColumn("data", "age", "Number")
cy.addColumn("data", "rating", "Number")
// 6 Records
cy.addRecord(["Students", 25, 1])
cy.addRecord(["Students", 20, 3])
cy.addRecord(["Students", 18, 6])
cy.addRecord(["Students", 25, 2])
cy.addRecord(["Teachers", 49, 5])
cy.addRecord(["Teachers", 36, 3])
// 6 Rows
cy.addRow(["Students", 25, 1])
cy.addRow(["Students", 20, 3])
cy.addRow(["Students", 18, 6])
cy.addRow(["Students", 25, 2])
cy.addRow(["Teachers", 49, 5])
cy.addRow(["Teachers", 36, 3])
})
it("creates a view", () => {

View File

@ -92,7 +92,7 @@ Cypress.Commands.add("addColumn", (tableName, columnName, type) => {
})
})
Cypress.Commands.add("addRecord", values => {
Cypress.Commands.add("addRow", values => {
cy.contains("Create New Row").click()
cy.get(".modal").within(() => {

View File

@ -26,11 +26,11 @@ export default {
],
trigger: {
id: "iRzYMOqND",
name: "Record Saved",
event: "record:save",
name: "Row Saved",
event: "row:save",
icon: "ri-save-line",
tagline: "Record is added to <b>{{table.name}}</b>",
description: "Fired when a record is saved to your database.",
tagline: "Row is added to <b>{{table.name}}</b>",
description: "Fired when a row is saved to your database.",
params: { table: "table" },
type: "TRIGGER",
args: {
@ -65,7 +65,7 @@ export default {
_rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff",
},
},
stepId: "RECORD_SAVED",
stepId: "ROW_SAVED",
},
},
type: "automation",

View File

@ -27,7 +27,7 @@ export const getBackendUiStore = () => {
})
},
},
records: {
rows: {
save: () =>
store.update(state => {
state.selectedView = state.selectedView
@ -38,9 +38,9 @@ export const getBackendUiStore = () => {
state.selectedView = state.selectedView
return state
}),
select: record =>
select: row =>
store.update(state => {
state.selectedRecord = record
state.selectedRow = row
return state
}),
},

View File

@ -13,7 +13,7 @@
function enrichInputs(inputs) {
let enrichedInputs = { ...inputs, enriched: {} }
const tableId = inputs.tableId || inputs.record?.tableId
const tableId = inputs.tableId || inputs.row?.tableId
if (tableId) {
enrichedInputs.enriched.table = $backendUiStore.tables.find(
table => table._id === tableId

View File

@ -1,6 +1,6 @@
<script>
import TableSelector from "./ParamInputs/TableSelector.svelte"
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
import RowSelector from "./ParamInputs/RowSelector.svelte"
import { Input, TextArea, Select, Label } from "@budibase/bbui"
import { automationStore } from "builderStore"
import BindableInput from "../../userInterface/BindableInput.svelte"
@ -62,8 +62,8 @@
<Input type="password" thin bind:value={block.inputs[key]} />
{:else if value.customType === 'table'}
<TableSelector bind:value={block.inputs[key]} />
{:else if value.customType === 'record'}
<RecordSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.customType === 'row'}
<RowSelector bind:value={block.inputs[key]} {bindings} />
{:else if value.type === 'string' || value.type === 'number'}
<BindableInput
type="string"

View File

@ -17,12 +17,12 @@
name: $backendUiStore.selectedView.name,
}
// Fetch records for specified table
// Fetch rows for specified table
$: {
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
loading = true
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
api.fetchDataForView($backendUiStore.selectedView).then(rows => {
data = rows || []
loading = false
})
}

View File

@ -5,36 +5,36 @@
import { backendUiStore } from "builderStore"
export let tableId
export let recordId
export let rowId
export let fieldName
let record
let row
let title
$: data = record?.[fieldName] ?? []
$: data = row?.[fieldName] ?? []
$: linkedTableId = data?.length ? data[0].tableId : null
$: linkedTable = $backendUiStore.tables.find(
table => table._id === linkedTableId
)
$: schema = linkedTable?.schema
$: table = $backendUiStore.tables.find(table => table._id === tableId)
$: fetchData(tableId, recordId)
$: fetchData(tableId, rowId)
$: {
let recordLabel = record?.[table?.primaryDisplay]
if (recordLabel) {
title = `${recordLabel} - ${fieldName}`
let rowLabel = row?.[table?.primaryDisplay]
if (rowLabel) {
title = `${rowLabel} - ${fieldName}`
} else {
title = fieldName
}
}
async function fetchData(tableId, recordId) {
const QUERY_VIEW_URL = `/api/${tableId}/${recordId}/enrich`
async function fetchData(tableId, rowId) {
const QUERY_VIEW_URL = `/api/${tableId}/${rowId}/enrich`
const response = await api.get(QUERY_VIEW_URL)
record = await response.json()
row = await response.json()
}
</script>
{#if record && record._id === recordId}
{#if row && row._id === rowId}
<Table {title} {schema} {data} />
{/if}

View File

@ -2,7 +2,7 @@
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let meta
export let value = meta.type === "boolean" ? false : ""
@ -28,7 +28,7 @@
{:else if type === 'boolean'}
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
{:else if type === 'link'}
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} />
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
{:else}
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
{/if}

View File

@ -10,7 +10,7 @@
import ActionButton from "components/common/ActionButton.svelte"
import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.svelte"
import CreateEditRecordModal from "./modals/CreateEditRecordModal.svelte"
import CreateEditRowModal from "./modals/CreateEditRowModal.svelte"
import RowPopover from "./buttons/CreateRowButton.svelte"
import ColumnPopover from "./buttons/CreateColumnButton.svelte"
import ViewPopover from "./buttons/CreateViewButton.svelte"
@ -41,12 +41,12 @@
: []
$: tableId = data?.length ? data[0].tableId : null
function selectRelationship(record, fieldName) {
if (!record?.[fieldName]?.length) {
function selectRelationship(row, fieldName) {
if (!row?.[fieldName]?.length) {
return
}
$goto(
`/${$params.application}/backend/table/${tableId}/relationship/${record._id}/${fieldName}`
`/${$params.application}/backend/table/${tableId}/relationship/${row._id}/${fieldName}`
)
}
</script>

View File

@ -12,7 +12,7 @@
$: name = view.name
// Fetch records for specified view
// Fetch rows for specified view
$: {
if (!name.startsWith("all_")) {
fetchViewData(name, view.field, view.groupBy)

View File

@ -6,22 +6,22 @@ export async function createUser(user) {
return await response.json()
}
export async function saveRecord(record, tableId) {
const SAVE_RECORDS_URL = `/api/${tableId}/records`
const response = await api.post(SAVE_RECORDS_URL, record)
export async function saveRow(row, tableId) {
const SAVE_ROWS_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_ROWS_URL, row)
return await response.json()
}
export async function deleteRecord(record) {
const DELETE_RECORDS_URL = `/api/${record.tableId}/records/${record._id}/${record._rev}`
const response = await api.delete(DELETE_RECORDS_URL)
export async function deleteRow(row) {
const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}`
const response = await api.delete(DELETE_ROWS_URL)
return response
}
export async function fetchDataForView(view) {
const FETCH_RECORDS_URL = `/api/views/${view.name}`
const FETCH_ROWS_URL = `/api/views/${view.name}`
const response = await api.get(FETCH_RECORDS_URL)
const response = await api.get(FETCH_ROWS_URL)
return await response.json()
}

View File

@ -1,6 +1,6 @@
<script>
import { TextButton as Button, Icon, Modal } from "@budibase/bbui"
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
let modal
</script>
@ -12,5 +12,5 @@
</Button>
</div>
<Modal bind:this={modal}>
<CreateEditRecordModal />
<CreateEditRowModal />
</Modal>

View File

@ -1,46 +1,46 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import RecordFieldControl from "../RecordFieldControl.svelte"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
export let row = {}
let errors = []
$: creating = record?._id == null
$: table = record.tableId
? $backendUiStore.tables.find(table => table._id === record?.tableId)
$: creating = row?._id == null
$: table = row.tableId
? $backendUiStore.tables.find(table => table._id === row?.tableId)
: $backendUiStore.selectedTable
$: tableSchema = Object.entries(table?.schema ?? {})
async function saveRecord() {
const recordResponse = await api.saveRecord(
{ ...record, tableId: table._id },
async function saveRow() {
const rowResponse = await api.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (recordResponse.errors) {
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
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("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
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={saveRecord}>
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#each tableSchema as [key, meta]}
<div>
<RecordFieldControl {meta} bind:value={record[key]} />
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{/each}
</ModalContent>

View File

@ -19,7 +19,7 @@
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
import * as api from "../api"
let fieldDefinitions = cloneDeep(FIELDS)

View File

@ -1,7 +1,7 @@
<script>
import { backendUiStore } from "builderStore"
import { DropdownMenu, Icon, Modal } from "@budibase/bbui"
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
import * as api from "../api"
import { notifier } from "builderStore/store/notifications"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -24,9 +24,9 @@
}
async function deleteRow() {
await api.deleteRecord(row)
notifier.success("Record deleted")
backendUiStore.actions.records.delete(row)
await api.deleteRow(row)
notifier.success("Row deleted")
backendUiStore.actions.rows.delete(row)
}
</script>
@ -52,7 +52,7 @@
onOk={deleteRow}
title="Confirm Delete" />
<Modal bind:this={modal}>
<CreateEditRecordModal record={row} />
<CreateEditRowModal row={row} />
</Modal>
<style>

View File

@ -6,30 +6,30 @@
import { capitalise } from "../../helpers"
export let schema
export let linkedRecords = []
export let linkedRows = []
let records = []
let rows = []
$: label = capitalise(schema.name)
$: linkedTableId = schema.tableId
$: linkedTable = $backendUiStore.tables.find(
table => table._id === linkedTableId
)
$: fetchRecords(linkedTableId)
$: fetchRows(linkedTableId)
async function fetchRecords(linkedTableId) {
const FETCH_RECORDS_URL = `/api/${linkedTableId}/records`
async function fetchRows(linkedTableId) {
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
try {
const response = await api.get(FETCH_RECORDS_URL)
records = await response.json()
const response = await api.get(FETCH_ROWS_URL)
rows = await response.json()
} catch (error) {
console.log(error)
records = []
rows = []
}
}
function getPrettyName(record) {
return record[linkedTable.primaryDisplay || "_id"]
function getPrettyName(row) {
return row[linkedTable.primaryDisplay || "_id"]
}
</script>
@ -43,11 +43,11 @@
{:else}
<Multiselect
secondary
bind:value={linkedRecords}
bind:value={linkedRows}
{label}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{#each rows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/if}

View File

@ -18,25 +18,25 @@
})
let idFields
let recordId
let rowId
$: {
idFields = bindableProperties.filter(
bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
)
// ensure recordId is always defaulted - there is usually only one option
// ensure rowId is always defaulted - there is usually only one option
if (idFields.length > 0 && !parameters._id) {
recordId = idFields[0].runtimeBinding
rowId = idFields[0].runtimeBinding
parameters = parameters
} else if (!recordId && parameters._id) {
recordId = parameters._id
} else if (!rowId && parameters._id) {
rowId = parameters._id
.replace("{{", "")
.replace("}}", "")
.trim()
}
}
$: parameters._id = `{{ ${recordId} }}`
$: parameters._id = `{{ ${rowId} }}`
// just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
@ -44,11 +44,11 @@
// finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to.
// then returns the field names for that schema
const schemaFromIdBinding = recordId => {
if (!recordId) return []
const schemaFromIdBinding = rowId => {
if (!rowId) return []
const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === recordId
prop => prop.runtimeBinding === rowId
)
if (!idBinding) return []
@ -71,8 +71,8 @@
let schemaFields
$: {
if (parameters && recordId) {
schemaFields = schemaFromIdBinding(recordId)
if (parameters && rowId) {
schemaFields = schemaFromIdBinding(rowId)
} else {
schemaFields = []
}
@ -86,12 +86,12 @@
<div class="root">
{#if idFields.length === 0}
<div class="cannot-use">
Update record can only be used within a component that provides data, such
Update row can only be used within a component that provides data, such
as a List
</div>
{:else}
<Label size="m" color="dark">Record Id</Label>
<Select secondary bind:value={recordId}>
<Label size="m" color="dark">Row Id</Label>
<Select secondary bind:value={rowId}>
<option value="" />
{#each idFields as idField}
<option value={idField.runtimeBinding}>
@ -101,7 +101,7 @@
</Select>
{/if}
{#if recordId}
{#if rowId}
<SaveFields
parameterFields={parameters.fields}
{schemaFields}

View File

@ -1,6 +1,6 @@
import NavigateTo from "./NavigateTo.svelte"
import UpdateRecord from "./UpdateRecord.svelte"
import CreateRecord from "./CreateRecord.svelte"
import UpdateRow from "./UpdateRow.svelte"
import CreateRow from "./CreateRow.svelte"
// defines what actions are available, when adding a new one
// the component is the setup panel for the action
@ -9,15 +9,15 @@ import CreateRecord from "./CreateRecord.svelte"
export default [
{
name: "Create Record",
component: CreateRecord,
name: "Create Row",
component: CreateRow,
},
{
name: "Navigate To",
component: NavigateTo,
},
{
name: "Update Record",
component: UpdateRecord,
name: "Update Row",
component: UpdateRow,
},
]

View File

@ -4568,8 +4568,8 @@ export default [
label: "receipt",
},
{
value: "fas fa-record-vinyl",
label: "record-vinyl",
value: "fas fa-row-vinyl",
label: "row-vinyl",
},
{
value: "fas fa-recycle",

View File

@ -299,7 +299,7 @@ export default {
{
name: "List",
_component: "@budibase/standard-components/list",
description: "Renders all children once per record, of a given table",
description: "Renders all children once per row, of a given table",
icon: "ri-file-list-line",
properties: {
design: { ...all },
@ -1125,10 +1125,10 @@ export default {
// children: [],
// },
{
name: "Record Detail",
_component: "@budibase/standard-components/recorddetail",
name: "Row Detail",
_component: "@budibase/standard-components/rowdetail",
description:
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
"Loads a row, using an id from the URL, which can be used with {{ context }}, in children",
icon: "ri-profile-line",
properties: {
design: { ...all },

View File

@ -5,5 +5,5 @@
<RelationshipDataTable
tableId={$params.selectedTable}
recordId={$params.selectedRecord}
rowId={$params.selectedRow}
fieldName={decodeURI($params.selectedField)} />

View File

@ -29,8 +29,8 @@ export const componentsAndScreens = () => ({
},
},
{
_instanceName: "Record View",
tags: ["record"],
_instanceName: "Row View",
tags: ["row"],
props: {
data: "state",
},

View File

@ -52,22 +52,22 @@ const apiOpts = {
delete: del,
}
const createRecord = async params =>
const createRow = async params =>
await post({
url: `/api/${params.tableId}/records`,
body: makeRecordRequestBody(params),
url: `/api/${params.tableId}/rows`,
body: makeRowRequestBody(params),
})
const updateRecord = async params => {
const record = makeRecordRequestBody(params)
record._id = params._id
const updateRow = async params => {
const row = makeRowRequestBody(params)
row._id = params._id
await patch({
url: `/api/${params.tableId}/records/${params._id}`,
body: record,
url: `/api/${params.tableId}/rows/${params._id}`,
body: row,
})
}
const makeRecordRequestBody = parameters => {
const makeRowRequestBody = parameters => {
const body = {}
for (let fieldName in parameters.fields) {
const field = parameters.fields[fieldName]
@ -95,6 +95,6 @@ const makeRecordRequestBody = parameters => {
export default {
authenticate: authenticate(apiOpts),
createRecord,
updateRecord,
createRow,
updateRow,
}

View File

@ -6,8 +6,8 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = routeTo => {
const handlers = {
"Navigate To": param => routeTo(param && param.url),
"Create Record": api.createRecord,
"Update Record": api.updateRecord,
"Create Row": api.createRow,
"Update Row": api.updateRow,
"Trigger Workflow": api.triggerWorkflow,
}

View File

@ -24,14 +24,14 @@ async function run() {
quotaReset: Date.now() + 2592000000,
usageQuota: {
automationRuns: 0,
records: 0,
rows: 0,
storage: 0,
users: 0,
views: 0,
},
usageLimits: {
automationRuns: 10,
records: 10,
rows: 10,
storage: 1000,
users: 10,
views: 10,
@ -48,8 +48,8 @@ async function run() {
run()
.then(() => {
console.log("Records should have been created.")
console.log("Rows should have been created.")
})
.catch(err => {
console.error("Cannot create records - " + err)
console.error("Cannot create rows - " + err)
})

View File

@ -2,7 +2,7 @@ const fs = require("fs")
const CouchDB = require("../../db")
const client = require("../../db/clientDb")
const newid = require("../../db/newid")
const { createLinkView } = require("../../db/linkedRecords")
const { createLinkView } = require("../../db/linkedRows")
const { join } = require("../../utilities/centralPath")
const { downloadTemplate } = require("../../utilities/templates")
@ -27,7 +27,7 @@ exports.create = async function(ctx) {
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
views: {},
})
// add view for linked records
// add view for linked rows
await createLinkView(instanceId)
// Add the new instance under the app clientDB

View File

@ -1,9 +1,9 @@
const CouchDB = require("../../db")
const validateJs = require("validate.js")
const linkRecords = require("../../db/linkedRecords")
const linkRows = require("../../db/linkedRows")
const {
getRecordParams,
generateRecordID,
getRowParams,
generateRowID,
DocumentTypes,
SEPARATOR,
} = require("../../db/utils")
@ -24,18 +24,18 @@ validateJs.extend(validateJs.validators.datetime, {
exports.patch = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
let record = await db.get(ctx.params.id)
const table = await db.get(record.tableId)
let row = await db.get(ctx.params.id)
const table = await db.get(row.tableId)
const patchfields = ctx.request.body
record = coerceRecordValues(record, table)
row = coerceRowValues(row, table)
for (let key of Object.keys(patchfields)) {
if (!table.schema[key]) continue
record[key] = patchfields[key]
row[key] = patchfields[key]
}
const validateResult = await validate({
record,
row,
table,
})
@ -48,21 +48,21 @@ exports.patch = async function(ctx) {
return
}
// returned record is cleaned and prepared for writing to DB
record = await linkRecords.updateLinks({
// returned row is cleaned and prepared for writing to DB
row = await linkRows.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_UPDATE,
record,
tableId: record.tableId,
eventType: linkRows.EventType.ROW_UPDATE,
row,
tableId: row.tableId,
table,
})
const response = await db.put(record)
record._rev = response.rev
record.type = "record"
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:update`, instanceId, record, table)
ctx.body = record
ctx.eventEmitter.emitRow(`row:update`, instanceId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
}
@ -70,22 +70,22 @@ exports.patch = async function(ctx) {
exports.save = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
let record = ctx.request.body
record.tableId = ctx.params.tableId
let row = ctx.request.body
row.tableId = ctx.params.tableId
if (!record._rev && !record._id) {
record._id = generateRecordID(record.tableId)
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
}
// if the record obj had an _id then it will have been retrieved
const existingRecord = ctx.preExisting
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
const table = await db.get(record.tableId)
const table = await db.get(row.tableId)
record = coerceRecordValues(record, table)
row = coerceRowValues(row, table)
const validateResult = await validate({
record,
row,
table,
})
@ -98,32 +98,32 @@ exports.save = async function(ctx) {
return
}
// make sure link records are up to date
record = await linkRecords.updateLinks({
// make sure link rows are up to date
row = await linkRows.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_SAVE,
record,
tableId: record.tableId,
eventType: linkRows.EventType.ROW_SAVE,
row,
tableId: row.tableId,
table,
})
if (existingRecord) {
const response = await db.put(record)
record._rev = response.rev
record.type = "record"
ctx.body = record
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
}
record.type = "record"
const response = await db.post(record)
record._rev = response.rev
row.type = "row"
const response = await db.post(row)
row._rev = response.rev
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, table)
ctx.body = record
ctx.eventEmitter.emitRow(`row:save`, instanceId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} created successfully`
}
@ -137,7 +137,7 @@ exports.fetchView = async function(ctx) {
// 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.fetchTableRecords(ctx)
await exports.fetchTableRows(ctx)
return
}
@ -157,19 +157,19 @@ exports.fetchView = async function(ctx) {
response.rows = response.rows.map(row => row.doc)
}
ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows)
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
}
exports.fetchTableRecords = async function(ctx) {
exports.fetchTableRows = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const response = await db.allDocs(
getRecordParams(ctx.params.tableId, null, {
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
ctx.body = await linkRecords.attachLinkInfo(
ctx.body = await linkRows.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
@ -182,7 +182,7 @@ exports.search = async function(ctx) {
include_docs: true,
...ctx.request.body,
})
ctx.body = await linkRecords.attachLinkInfo(
ctx.body = await linkRows.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
@ -191,48 +191,48 @@ exports.search = async function(ctx) {
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.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId does not match the records tableId")
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 linkRecords.attachLinkInfo(instanceId, record)
ctx.body = await linkRows.attachLinkInfo(instanceId, row)
}
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.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId doesn't match the record's tableId")
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 linkRecords.updateLinks({
await linkRows.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_DELETE,
record,
tableId: record.tableId,
eventType: linkRows.EventType.ROW_DELETE,
row,
tableId: row.tableId,
})
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
ctx.status = 200
// for automations include the record that was deleted
ctx.record = record
// for automations include the row that was deleted
ctx.row = row
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record)
ctx.eventEmitter.emitRow(`row:delete`, instanceId, row)
}
exports.validate = async function(ctx) {
const errors = await validate({
instanceId: ctx.user.instanceId,
tableId: ctx.params.tableId,
record: ctx.request.body,
row: ctx.request.body,
})
ctx.status = 200
ctx.body = errors
}
async function validate({ instanceId, tableId, record, table }) {
async function validate({ instanceId, tableId, row, table }) {
if (!table) {
const db = new CouchDB(instanceId)
table = await db.get(tableId)
@ -240,7 +240,7 @@ async function validate({ instanceId, tableId, record, table }) {
const errors = {}
for (let fieldName of Object.keys(table.schema)) {
const res = validateJs.single(
record[fieldName],
row[fieldName],
table.schema[fieldName].constraints
)
if (res) errors[fieldName] = res
@ -248,12 +248,12 @@ async function validate({ instanceId, tableId, record, table }) {
return { valid: Object.keys(errors).length === 0, errors }
}
exports.fetchEnrichedRecord = async function(ctx) {
exports.fetchEnrichedRow = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const tableId = ctx.params.tableId
const recordId = ctx.params.recordId
if (instanceId == null || tableId == null || recordId == null) {
const rowId = ctx.params.rowId
if (instanceId == null || tableId == null || rowId == null) {
ctx.status = 400
ctx.body = {
status: 400,
@ -262,51 +262,51 @@ exports.fetchEnrichedRecord = async function(ctx) {
}
return
}
// need table to work out where links go in record
const [table, record] = await Promise.all([db.get(tableId), db.get(recordId)])
// 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 linkRecords.getLinkDocuments({
const linkVals = await linkRows.getLinkDocuments({
instanceId,
tableId,
recordId,
rowId,
})
// look up the actual records based on the ids
// 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 records for any links they may have
let linkedRecords = await linkRecords.attachLinkInfo(
// 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 records in the correct place throughout the main record
// 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") {
record[fieldName] = linkedRecords.filter(
linkRecord => linkRecord.tableId === field.tableId
row[fieldName] = linkedRows.filter(
linkRow => linkRow.tableId === field.tableId
)
}
}
ctx.body = record
ctx.body = row
ctx.status = 200
}
function coerceRecordValues(rec, table) {
const record = cloneDeep(rec)
for (let [key, value] of Object.entries(record)) {
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)) {
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
row[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return record
return row
}
const TYPE_TRANSFORM_MAP = {

View File

@ -1,11 +1,11 @@
const CouchDB = require("../../db")
const linkRecords = require("../../db/linkedRecords")
const linkRows = require("../../db/linkedRows")
const csvParser = require("../../utilities/csvParser")
const {
getRecordParams,
getRowParams,
getTableParams,
generateTableID,
generateRecordID,
generateRowID,
} = require("../../db/utils")
exports.fetch = async function(ctx) {
@ -37,20 +37,20 @@ exports.save = async function(ctx) {
// if the table obj had an _id then it will have been retrieved
const oldTable = ctx.preExisting
// rename record fields when table column is renamed
// 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 records = await db.allDocs(
getRecordParams(tableToSave._id, null, {
const rows = await db.allDocs(
getRowParams(tableToSave._id, null, {
include_docs: true,
})
)
const docs = records.rows.map(({ doc }) => {
const docs = rows.rows.map(({ doc }) => {
doc[_rename.updated] = doc[_rename.old]
delete doc[_rename.old]
return doc
@ -72,12 +72,12 @@ exports.save = async function(ctx) {
const result = await db.post(tableToSave)
tableToSave._rev = result.rev
// update linked records
await linkRecords.updateLinks({
// update linked rows
await linkRows.updateLinks({
instanceId,
eventType: oldTable
? linkRecords.EventType.TABLE_UPDATED
: linkRecords.EventType.TABLE_SAVE,
? linkRows.EventType.TABLE_UPDATED
: linkRows.EventType.TABLE_SAVE,
table: tableToSave,
oldTable: oldTable,
})
@ -86,11 +86,11 @@ exports.save = async function(ctx) {
ctx.eventEmitter.emitTable(`table:save`, instanceId, tableToSave)
if (dataImport && dataImport.path) {
// Populate the table with records imported from CSV in a bulk update
// 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 = generateRecordID(tableToSave._id)
row._id = generateRowID(tableToSave._id)
row.tableId = tableToSave._id
}
@ -110,20 +110,20 @@ exports.destroy = async function(ctx) {
await db.remove(tableToDelete)
// Delete all records for that table
const records = await db.allDocs(
getRecordParams(ctx.params.tableId, null, {
// Delete all rows for that table
const rows = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
await db.bulkDocs(
records.rows.map(record => ({ _id: record.id, _deleted: true }))
rows.rows.map(row => ({ _id: row.id, _deleted: true }))
)
// update linked records
await linkRecords.updateLinks({
// update linked rows
await linkRows.updateLinks({
instanceId,
eventType: linkRecords.EventType.TABLE_DELETE,
eventType: linkRows.EventType.TABLE_DELETE,
table: tableToDelete,
})

View File

@ -4,7 +4,7 @@ const fs = require("fs")
const { join } = require("../../../utilities/centralPath")
const os = require("os")
const exporters = require("./exporters")
const { fetchView } = require("../record")
const { fetchView } = require("../row")
const controller = {
fetch: async ctx => {
@ -85,7 +85,7 @@ const controller = {
const view = ctx.request.body
const format = ctx.query.format
// Fetch view records
// Fetch view rows
ctx.params.viewName = view.name
ctx.query.group = view.groupBy
if (view.field) {

View File

@ -11,7 +11,7 @@ const {
instanceRoutes,
clientRoutes,
applicationRoutes,
recordRoutes,
rowRoutes,
tableRoutes,
viewRoutes,
staticRoutes,
@ -77,8 +77,8 @@ router.use(viewRoutes.allowedMethods())
router.use(tableRoutes.routes())
router.use(tableRoutes.allowedMethods())
router.use(recordRoutes.routes())
router.use(recordRoutes.allowedMethods())
router.use(rowRoutes.routes())
router.use(rowRoutes.allowedMethods())
router.use(userRoutes.routes())
router.use(userRoutes.allowedMethods())

View File

@ -5,7 +5,7 @@ const instanceRoutes = require("./instance")
const clientRoutes = require("./client")
const applicationRoutes = require("./application")
const tableRoutes = require("./table")
const recordRoutes = require("./record")
const rowRoutes = require("./row")
const viewRoutes = require("./view")
const staticRoutes = require("./static")
const componentRoutes = require("./component")
@ -24,7 +24,7 @@ module.exports = {
instanceRoutes,
clientRoutes,
applicationRoutes,
recordRoutes,
rowRoutes,
tableRoutes,
viewRoutes,
staticRoutes,

View File

@ -1,5 +1,5 @@
const Router = require("@koa/router")
const recordController = require("../controllers/record")
const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized")
const usage = require("../../middleware/usageQuota")
const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels")
@ -8,42 +8,42 @@ const router = Router()
router
.get(
"/api/:tableId/:recordId/enrich",
"/api/:tableId/:rowId/enrich",
authorized(READ_TABLE, ctx => ctx.params.tableId),
recordController.fetchEnrichedRecord
rowController.fetchEnrichedRow
)
.get(
"/api/:tableId/records",
"/api/:tableId/rows",
authorized(READ_TABLE, ctx => ctx.params.tableId),
recordController.fetchTableRecords
rowController.fetchTableRows
)
.get(
"/api/:tableId/records/:recordId",
"/api/:tableId/rows/:rowId",
authorized(READ_TABLE, ctx => ctx.params.tableId),
recordController.find
rowController.find
)
.post("/api/records/search", recordController.search)
.post("/api/rows/search", rowController.search)
.post(
"/api/:tableId/records",
"/api/:tableId/rows",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
usage,
recordController.save
rowController.save
)
.patch(
"/api/:tableId/records/:id",
"/api/:tableId/rows/:id",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
recordController.patch
rowController.patch
)
.post(
"/api/:tableId/records/validate",
"/api/:tableId/rows/validate",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
recordController.validate
rowController.validate
)
.delete(
"/api/:tableId/records/:recordId/:revId",
"/api/:tableId/rows/:rowId/:revId",
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
usage,
recordController.destroy
rowController.destroy
)
module.exports = router

View File

@ -126,10 +126,10 @@ describe("/automations", () => {
describe("create", () => {
it("should setup the automation fully", () => {
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
let trigger = TRIGGER_DEFINITIONS["ROW_SAVED"]
trigger.id = "wadiawdo34"
let createAction = ACTION_DEFINITIONS["CREATE_RECORD"]
createAction.inputs.record = {
let createAction = ACTION_DEFINITIONS["CREATE_ROW"]
createAction.inputs.row = {
name: "{{trigger.name}}",
description: "{{trigger.description}}"
}
@ -169,7 +169,7 @@ describe("/automations", () => {
it("trigger the automation successfully", async () => {
let table = await createTable(request, app._id, instance._id)
TEST_AUTOMATION.definition.trigger.inputs.tableId = table._id
TEST_AUTOMATION.definition.steps[0].inputs.record.tableId = table._id
TEST_AUTOMATION.definition.steps[0].inputs.row.tableId = table._id
await createAutomation()
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
// know that it has finished all of its actions - this is currently the best way
@ -189,7 +189,7 @@ describe("/automations", () => {
return
}
}
throw "Failed to find the records"
throw "Failed to find the rows"
})
})

View File

@ -72,7 +72,7 @@ exports.createTable = async (request, appId, instanceId, table) => {
exports.getAllFromTable = async (request, appId, instanceId, tableId) => {
const res = await request
.get(`/api/${tableId}/records`)
.get(`/api/${tableId}/rows`)
.set(exports.defaultHeaders(appId, instanceId))
return res.body
}

View File

@ -7,12 +7,12 @@ const {
defaultHeaders,
} = require("./couchTestUtils");
describe("/records", () => {
describe("/rows", () => {
let request
let server
let instance
let table
let record
let row
let app
beforeAll(async () => {
@ -28,7 +28,7 @@ describe("/records", () => {
beforeEach(async () => {
instance = await createInstance(request, app._id)
table = await createTable(request, app._id, instance._id)
record = {
row = {
name: "Test Contact",
description: "original description",
status: "new",
@ -36,17 +36,17 @@ describe("/records", () => {
}
})
const createRecord = async r =>
const createRow = async r =>
await request
.post(`/api/${r ? r.tableId : record.tableId}/records`)
.send(r || record)
.post(`/api/${r ? r.tableId : row.tableId}/rows`)
.send(r || row)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
const loadRecord = async id =>
const loadRow = async id =>
await request
.get(`/api/${table._id}/records/${id}`)
.get(`/api/${table._id}/rows/${id}`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -55,19 +55,19 @@ describe("/records", () => {
describe("save, load, update, delete", () => {
it("returns a success message when the record is created", async () => {
const res = await createRecord()
it("returns a success message when the row is created", async () => {
const res = await createRow()
expect(res.res.statusMessage).toEqual(`${table.name} created successfully`)
expect(res.body.name).toEqual("Test Contact")
expect(res.body._rev).toBeDefined()
})
it("updates a record successfully", async () => {
const rec = await createRecord()
it("updates a row successfully", async () => {
const rec = await createRow()
const existing = rec.body
const res = await request
.post(`/api/${table._id}/records`)
.post(`/api/${table._id}/rows`)
.send({
_id: existing._id,
_rev: existing._rev,
@ -82,78 +82,78 @@ describe("/records", () => {
expect(res.body.name).toEqual("Updated Name")
})
it("should load a record", async () => {
const rec = await createRecord()
it("should load a row", async () => {
const rec = await createRow()
const existing = rec.body
const res = await request
.get(`/api/${table._id}/records/${existing._id}`)
.get(`/api/${table._id}/rows/${existing._id}`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body).toEqual({
...record,
...row,
_id: existing._id,
_rev: existing._rev,
type: "record",
type: "row",
})
})
it("should list all records for given tableId", async () => {
const newRecord = {
it("should list all rows for given tableId", async () => {
const newRow = {
tableId: table._id,
name: "Second Contact",
status: "new"
}
await createRecord()
await createRecord(newRecord)
await createRow()
await createRow(newRow)
const res = await request
.get(`/api/${table._id}/records`)
.get(`/api/${table._id}/rows`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(2)
expect(res.body.find(r => r.name === newRecord.name)).toBeDefined()
expect(res.body.find(r => r.name === record.name)).toBeDefined()
expect(res.body.find(r => r.name === newRow.name)).toBeDefined()
expect(res.body.find(r => r.name === row.name)).toBeDefined()
})
it("lists records when queried by their ID", async () => {
const newRecord = {
it("lists rows when queried by their ID", async () => {
const newRow = {
tableId: table._id,
name: "Second Contact",
status: "new"
}
const record = await createRecord()
const secondRecord = await createRecord(newRecord)
const row = await createRow()
const secondRow = await createRow(newRow)
const recordIds = [record.body._id, secondRecord.body._id]
const rowIds = [row.body._id, secondRow.body._id]
const res = await request
.post(`/api/records/search`)
.post(`/api/rows/search`)
.set(defaultHeaders(app._id, instance._id))
.send({
keys: recordIds
keys: rowIds
})
.expect('Content-Type', /json/)
.expect(200)
expect(res.body.length).toBe(2)
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(recordIds))
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(rowIds))
})
it("load should return 404 when record does not exist", async () => {
await createRecord()
it("load should return 404 when row does not exist", async () => {
await createRow()
await request
.get(`/api/${table._id}/records/not-a-valid-id`)
.get(`/api/${table._id}/rows/not-a-valid-id`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(404)
})
it("record values are coerced", async () => {
it("row values are coerced", async () => {
const str = {type:"string", constraints: { type: "string", presence: false }}
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
@ -189,8 +189,8 @@ describe("/records", () => {
},
})
record = {
name: "Test Record",
row = {
name: "Test Row",
stringUndefined: undefined,
stringNull: null,
stringString: "i am a string",
@ -215,9 +215,9 @@ describe("/records", () => {
attachmentEmpty : "",
}
const id = (await createRecord(record)).body._id
const id = (await createRow(row)).body._id
const saved = (await loadRecord(id)).body
const saved = (await loadRow(id)).body
expect(saved.stringUndefined).toBe(undefined)
expect(saved.stringNull).toBe("")
@ -230,8 +230,8 @@ describe("/records", () => {
expect(saved.datetimeEmptyString).toBe(null)
expect(saved.datetimeNull).toBe(null)
expect(saved.datetimeUndefined).toBe(undefined)
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
expect(saved.datetimeString).toBe(new Date(row.datetimeString).toISOString())
expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString())
expect(saved.boolNull).toBe(null)
expect(saved.boolEmpty).toBe(null)
expect(saved.boolUndefined).toBe(undefined)
@ -245,11 +245,11 @@ describe("/records", () => {
describe("patch", () => {
it("should update only the fields that are supplied", async () => {
const rec = await createRecord()
const rec = await createRow()
const existing = rec.body
const res = await request
.patch(`/api/${table._id}/records/${existing._id}`)
.patch(`/api/${table._id}/rows/${existing._id}`)
.send({
_id: existing._id,
_rev: existing._rev,
@ -264,18 +264,18 @@ describe("/records", () => {
expect(res.body.name).toEqual("Updated Name")
expect(res.body.description).toEqual(existing.description)
const savedRecord = await loadRecord(res.body._id)
const savedRow = await loadRow(res.body._id)
expect(savedRecord.body.description).toEqual(existing.description)
expect(savedRecord.body.name).toEqual("Updated Name")
expect(savedRow.body.description).toEqual(existing.description)
expect(savedRow.body.name).toEqual("Updated Name")
})
})
describe("validate", () => {
it("should return no errors on valid record", async () => {
it("should return no errors on valid row", async () => {
const result = await request
.post(`/api/${table._id}/records/validate`)
.post(`/api/${table._id}/rows/validate`)
.send({ name: "ivan" })
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
@ -285,9 +285,9 @@ describe("/records", () => {
expect(Object.keys(result.body.errors)).toEqual([])
})
it("should errors on invalid record", async () => {
it("should errors on invalid row", async () => {
const result = await request
.post(`/api/${table._id}/records/validate`)
.post(`/api/${table._id}/rows/validate`)
.send({ name: 1 })
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)

View File

@ -50,11 +50,11 @@ describe("/tables", () => {
});
})
it("renames all the record fields for a table when a schema key is renamed", async () => {
it("renames all the row fields for a table when a schema key is renamed", async () => {
const testTable = await createTable(request, app._id, instance._id);
const testRecord = await request
.post(`/api/${testTable._id}/records`)
const testRow = await request
.post(`/api/${testTable._id}/rows`)
.send({
name: "test"
})
@ -85,7 +85,7 @@ describe("/tables", () => {
expect(updatedTable.body.name).toEqual("TestTable");
const res = await request
.get(`/api/${testTable._id}/records/${testRecord.body._id}`)
.get(`/api/${testTable._id}/rows/${testRow.body._id}`)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)

View File

@ -27,9 +27,9 @@ describe("/views", () => {
.expect('Content-Type', /json/)
.expect(200)
const createRecord = async record => request
.post(`/api/${table._id}/records`)
.send(record)
const createRow = async row => request
.post(`/api/${table._id}/rows`)
.send(row)
.set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/)
.expect(200)
@ -58,7 +58,7 @@ describe("/views", () => {
expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
})
it("updates the table record with the new view metadata", async () => {
it("updates the table row with the new view metadata", async () => {
const res = await createView()
expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
const updatedTable = await getDocument(instance._id, table._id)
@ -110,15 +110,15 @@ describe("/views", () => {
it("returns data for the created view", async () => {
await createView()
await createRecord({
await createRow({
tableId: table._id,
Price: 1000
})
await createRecord({
await createRow({
tableId: table._id,
Price: 2000
})
await createRecord({
await createRow({
tableId: table._id,
Price: 4000
})
@ -138,17 +138,17 @@ describe("/views", () => {
groupBy: "Category",
tableId: table._id
})
await createRecord({
await createRow({
tableId: table._id,
Price: 1000,
Category: "One"
})
await createRecord({
await createRow({
tableId: table._id,
Price: 2000,
Category: "One"
})
await createRecord({
await createRow({
tableId: table._id,
Price: 4000,
Category: "Two"

View File

@ -1,6 +1,6 @@
const Router = require("@koa/router")
const viewController = require("../controllers/view")
const recordController = require("../controllers/record")
const rowController = require("../controllers/row")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_VIEW } = require("../../utilities/accessLevels")
const usage = require("../../middleware/usageQuota")
@ -11,7 +11,7 @@ router
.get(
"/api/views/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView
rowController.fetchView
)
.get("/api/views", authorized(BUILDER), viewController.fetch)
.delete(

View File

@ -1,7 +1,7 @@
const sendEmail = require("./steps/sendEmail")
const createRecord = require("./steps/createRecord")
const updateRecord = require("./steps/updateRecord")
const deleteRecord = require("./steps/deleteRecord")
const createRow = require("./steps/createRow")
const updateRow = require("./steps/updateRow")
const deleteRow = require("./steps/deleteRow")
const createUser = require("./steps/createUser")
const environment = require("../environment")
const download = require("download")
@ -17,16 +17,16 @@ const DEFAULT_DIRECTORY = ".budibase-automations"
const AUTOMATION_MANIFEST = "manifest.json"
const BUILTIN_ACTIONS = {
SEND_EMAIL: sendEmail.run,
CREATE_RECORD: createRecord.run,
UPDATE_RECORD: updateRecord.run,
DELETE_RECORD: deleteRecord.run,
CREATE_ROW: createRow.run,
UPDATE_ROW: updateRow.run,
DELETE_ROW: deleteRow.run,
CREATE_USER: createUser.run,
}
const BUILTIN_DEFINITIONS = {
SEND_EMAIL: sendEmail.definition,
CREATE_RECORD: createRecord.definition,
UPDATE_RECORD: updateRecord.definition,
DELETE_RECORD: deleteRecord.definition,
CREATE_ROW: createRow.definition,
UPDATE_ROW: updateRow.definition,
DELETE_ROW: deleteRow.definition,
CREATE_USER: createUser.definition,
}

View File

@ -84,33 +84,33 @@ module.exports.cleanInputValues = (inputs, schema) => {
}
/**
* Given a record input like a save or update record we need to clean the inputs against a schema that is not part of
* Given a row input like a save or update row we need to clean the inputs against a schema that is not part of
* the automation but is instead part of the Table/Table. This function will get the table schema and use it to instead
* perform the cleanInputValues function on the input record.
* perform the cleanInputValues function on the input row.
*
* @param {string} instanceId The instance which the Table/Table is contained under.
* @param {string} tableId The ID of the Table/Table which the schema is to be retrieved for.
* @param {object} record The input record structure which requires clean-up after having been through mustache statements.
* @returns {Promise<Object>} The cleaned up records object, will should now have all the required primitive types.
* @param {object} row The input row structure which requires clean-up after having been through mustache statements.
* @returns {Promise<Object>} The cleaned up rows object, will should now have all the required primitive types.
*/
module.exports.cleanUpRecord = async (instanceId, tableId, record) => {
module.exports.cleanUpRow = async (instanceId, tableId, row) => {
const db = new CouchDB(instanceId)
const table = await db.get(tableId)
return module.exports.cleanInputValues(record, { properties: table.schema })
return module.exports.cleanInputValues(row, { properties: table.schema })
}
/**
* A utility function for the cleanUpRecord, which can be used if only the record ID is known (not the table ID) to clean
* up a record after mustache statements have been replaced. This is specifically useful for the update record action.
* A utility function for the cleanUpRow, which can be used if only the row ID is known (not the table ID) to clean
* up a row after mustache statements have been replaced. This is specifically useful for the update row action.
*
* @param {string} instanceId The instance which the Table/Table is contained under.
* @param {string} recordId The ID of the record from which the tableId will be extracted, to get the Table/Table schema.
* @param {object} record The input record structure which requires clean-up after having been through mustache statements.
* @returns {Promise<Object>} The cleaned up records object, which will now have all the required primitive types.
* @param {string} rowId The ID of the row from which the tableId will be extracted, to get the Table/Table schema.
* @param {object} row The input row structure which requires clean-up after having been through mustache statements.
* @returns {Promise<Object>} The cleaned up rows object, which will now have all the required primitive types.
*/
module.exports.cleanUpRecordById = async (instanceId, recordId, record) => {
module.exports.cleanUpRowById = async (instanceId, rowId, row) => {
const db = new CouchDB(instanceId)
const foundRecord = await db.get(recordId)
return module.exports.cleanUpRecord(instanceId, foundRecord.tableId, record)
const foundRow = await db.get(rowId)
return module.exports.cleanUpRow(instanceId, foundRow.tableId, row)
}

View File

@ -1,4 +1,4 @@
const recordController = require("../../api/controllers/record")
const rowController = require("../../api/controllers/row")
const automationUtils = require("../automationUtils")
const environment = require("../../environment")
const usage = require("../../utilities/usageQuota")
@ -9,12 +9,12 @@ module.exports.definition = {
icon: "ri-save-3-fill",
description: "Add a row to your database",
type: "ACTION",
stepId: "CREATE_RECORD",
stepId: "CREATE_ROW",
inputs: {},
schema: {
inputs: {
properties: {
record: {
row: {
type: "object",
properties: {
tableId: {
@ -22,18 +22,18 @@ module.exports.definition = {
customType: "table",
},
},
customType: "record",
customType: "row",
title: "Table",
required: ["tableId"],
},
},
required: ["record"],
required: ["row"],
},
outputs: {
properties: {
record: {
row: {
type: "object",
customType: "record",
customType: "row",
description: "The new row",
},
response: {
@ -60,32 +60,32 @@ module.exports.definition = {
module.exports.run = async function({ inputs, instanceId, apiKey }) {
// TODO: better logging of when actions are missed due to missing parameters
if (inputs.record == null || inputs.record.tableId == null) {
if (inputs.row == null || inputs.row.tableId == null) {
return
}
inputs.record = await automationUtils.cleanUpRecord(
inputs.row = await automationUtils.cleanUpRow(
instanceId,
inputs.record.tableId,
inputs.record
inputs.row.tableId,
inputs.row
)
// have to clean up the record, remove the table from it
// have to clean up the row, remove the table from it
const ctx = {
params: {
tableId: inputs.record.tableId,
tableId: inputs.row.tableId,
},
request: {
body: inputs.record,
body: inputs.row,
},
user: { instanceId },
}
try {
if (environment.CLOUD) {
await usage.update(apiKey, usage.Properties.RECORD, 1)
await usage.update(apiKey, usage.Properties.ROW, 1)
}
await recordController.save(ctx)
await rowController.save(ctx)
return {
record: inputs.record,
row: inputs.row,
response: ctx.body,
id: ctx.body._id,
revision: ctx.body._rev,

View File

@ -1,4 +1,4 @@
const recordController = require("../../api/controllers/record")
const rowController = require("../../api/controllers/row")
const environment = require("../../environment")
const usage = require("../../utilities/usageQuota")
@ -8,7 +8,7 @@ module.exports.definition = {
name: "Delete Row",
tagline: "Delete a {{inputs.enriched.table.name}} row",
type: "ACTION",
stepId: "DELETE_RECORD",
stepId: "DELETE_ROW",
inputs: {},
schema: {
inputs: {
@ -31,9 +31,9 @@ module.exports.definition = {
},
outputs: {
properties: {
record: {
row: {
type: "object",
customType: "record",
customType: "row",
description: "The deleted row",
},
response: {
@ -45,7 +45,7 @@ module.exports.definition = {
description: "Whether the action was successful",
},
},
required: ["record", "success"],
required: ["row", "success"],
},
},
}
@ -58,7 +58,7 @@ module.exports.run = async function({ inputs, instanceId, apiKey }) {
let ctx = {
params: {
tableId: inputs.tableId,
recordId: inputs.id,
rowId: inputs.id,
revId: inputs.revision,
},
user: { instanceId },
@ -66,12 +66,12 @@ module.exports.run = async function({ inputs, instanceId, apiKey }) {
try {
if (environment.CLOUD) {
await usage.update(apiKey, usage.Properties.RECORD, -1)
await usage.update(apiKey, usage.Properties.ROW, -1)
}
await recordController.destroy(ctx)
await rowController.destroy(ctx)
return {
response: ctx.body,
record: ctx.record,
row: ctx.row,
success: ctx.status === 200,
}
} catch (err) {

View File

@ -1,35 +1,35 @@
const recordController = require("../../api/controllers/record")
const rowController = require("../../api/controllers/row")
const automationUtils = require("../automationUtils")
module.exports.definition = {
name: "Update Row",
tagline: "Update a {{inputs.enriched.table.name}} record",
tagline: "Update a {{inputs.enriched.table.name}} row",
icon: "ri-refresh-fill",
description: "Update a row in your database",
type: "ACTION",
stepId: "UPDATE_RECORD",
stepId: "UPDATE_ROW",
inputs: {},
schema: {
inputs: {
properties: {
record: {
row: {
type: "object",
customType: "record",
customType: "row",
title: "Table",
},
recordId: {
rowId: {
type: "string",
title: "Row ID",
},
},
required: ["record", "recordId"],
required: ["row", "rowId"],
},
outputs: {
properties: {
record: {
row: {
type: "object",
customType: "record",
description: "The updated record",
customType: "row",
description: "The updated row",
},
response: {
type: "object",
@ -41,11 +41,11 @@ module.exports.definition = {
},
id: {
type: "string",
description: "The identifier of the updated record",
description: "The identifier of the updated row",
},
revision: {
type: "string",
description: "The revision of the updated record",
description: "The revision of the updated row",
},
},
required: ["success", "id", "revision"],
@ -54,37 +54,37 @@ module.exports.definition = {
}
module.exports.run = async function({ inputs, instanceId }) {
if (inputs.recordId == null || inputs.record == null) {
if (inputs.rowId == null || inputs.row == null) {
return
}
inputs.record = await automationUtils.cleanUpRecordById(
inputs.row = await automationUtils.cleanUpRowById(
instanceId,
inputs.recordId,
inputs.record
inputs.rowId,
inputs.row
)
// clear any falsy properties so that they aren't updated
for (let propKey of Object.keys(inputs.record)) {
if (!inputs.record[propKey] || inputs.record[propKey] === "") {
delete inputs.record[propKey]
for (let propKey of Object.keys(inputs.row)) {
if (!inputs.row[propKey] || inputs.row[propKey] === "") {
delete inputs.row[propKey]
}
}
// have to clean up the record, remove the table from it
// have to clean up the row, remove the table from it
const ctx = {
params: {
id: inputs.recordId,
id: inputs.rowId,
},
request: {
body: inputs.record,
body: inputs.row,
},
user: { instanceId },
}
try {
await recordController.patch(ctx)
await rowController.patch(ctx)
return {
record: ctx.body,
row: ctx.body,
response: ctx.message,
id: ctx.body._id,
revision: ctx.body._rev,

View File

@ -11,13 +11,13 @@ const FAKE_NUMBER = 1
const FAKE_DATETIME = "1970-01-01T00:00:00.000Z"
const BUILTIN_DEFINITIONS = {
RECORD_SAVED: {
ROW_SAVED: {
name: "Row Saved",
event: "record:save",
event: "row:save",
icon: "ri-save-line",
tagline: "Row is added to {{inputs.enriched.table.name}}",
description: "Fired when a row is saved to your database",
stepId: "RECORD_SAVED",
stepId: "ROW_SAVED",
inputs: {},
schema: {
inputs: {
@ -32,9 +32,9 @@ const BUILTIN_DEFINITIONS = {
},
outputs: {
properties: {
record: {
row: {
type: "object",
customType: "record",
customType: "row",
description: "The new row that was saved",
},
id: {
@ -46,18 +46,18 @@ const BUILTIN_DEFINITIONS = {
description: "Revision of row",
},
},
required: ["record", "id"],
required: ["row", "id"],
},
},
type: "TRIGGER",
},
RECORD_DELETED: {
ROW_DELETED: {
name: "Row Deleted",
event: "record:delete",
event: "row:delete",
icon: "ri-delete-bin-line",
tagline: "Row is deleted from {{inputs.enriched.table.name}}",
description: "Fired when a row is deleted from your database",
stepId: "RECORD_DELETED",
stepId: "ROW_DELETED",
inputs: {},
schema: {
inputs: {
@ -72,20 +72,20 @@ const BUILTIN_DEFINITIONS = {
},
outputs: {
properties: {
record: {
row: {
type: "object",
customType: "record",
customType: "row",
description: "The row that was deleted",
},
},
required: ["record", "id"],
required: ["row", "id"],
},
},
type: "TRIGGER",
},
}
async function queueRelevantRecordAutomations(event, eventType) {
async function queueRelevantRowAutomations(event, eventType) {
if (event.instanceId == null) {
throw `No instanceId specified for ${eventType} - check event emitters.`
}
@ -108,7 +108,7 @@ async function queueRelevantRecordAutomations(event, eventType) {
if (
!automation.live ||
!automationTrigger.inputs ||
automationTrigger.inputs.tableId !== event.record.tableId
automationTrigger.inputs.tableId !== event.row.tableId
) {
continue
}
@ -116,27 +116,27 @@ async function queueRelevantRecordAutomations(event, eventType) {
}
}
emitter.on("record:save", async function(event) {
if (!event || !event.record || !event.record.tableId) {
emitter.on("row:save", async function(event) {
if (!event || !event.row || !event.row.tableId) {
return
}
await queueRelevantRecordAutomations(event, "record:save")
await queueRelevantRowAutomations(event, "row:save")
})
emitter.on("record:delete", async function(event) {
if (!event || !event.record || !event.record.tableId) {
emitter.on("row:delete", async function(event) {
if (!event || !event.row || !event.row.tableId) {
return
}
await queueRelevantRecordAutomations(event, "record:delete")
await queueRelevantRowAutomations(event, "row:delete")
})
async function fillRecordOutput(automation, params) {
async function fillRowOutput(automation, params) {
let triggerSchema = automation.definition.trigger
let tableId = triggerSchema.inputs.tableId
const db = new CouchDB(params.instanceId)
try {
let table = await db.get(tableId)
let record = {}
let row = {}
for (let schemaKey of Object.keys(table.schema)) {
if (params[schemaKey] != null) {
continue
@ -144,20 +144,20 @@ async function fillRecordOutput(automation, params) {
let propSchema = table.schema[schemaKey]
switch (propSchema.constraints.type) {
case "string":
record[schemaKey] = FAKE_STRING
row[schemaKey] = FAKE_STRING
break
case "boolean":
record[schemaKey] = FAKE_BOOL
row[schemaKey] = FAKE_BOOL
break
case "number":
record[schemaKey] = FAKE_NUMBER
row[schemaKey] = FAKE_NUMBER
break
case "datetime":
record[schemaKey] = FAKE_DATETIME
row[schemaKey] = FAKE_DATETIME
break
}
}
params.record = record
params.row = row
} catch (err) {
throw "Failed to find table for trigger"
}
@ -171,7 +171,7 @@ module.exports.externalTrigger = async function(automation, params) {
automation.definition.trigger != null &&
automation.definition.trigger.inputs.tableId != null
) {
params = await fillRecordOutput(automation, params)
params = await fillRowOutput(automation, params)
}
automationQueue.add({ automation, event: params })

View File

@ -10,40 +10,40 @@ const { generateLinkID } = require("../utils")
* @param {string} tableId2 The ID of the second table (the linked).
* @param {string} fieldName1 The name of the field in the linker table.
* @param {string} fieldName2 The name of the field in the linked table.
* @param {string} recordId1 The ID of the record which is acting as the linker.
* @param {string} recordId2 The ID of the record which is acting as the linked.
* @param {string} rowId1 The ID of the row which is acting as the linker.
* @param {string} rowId2 The ID of the row which is acting as the linked.
* @constructor
*/
function LinkDocument(
tableId1,
fieldName1,
recordId1,
rowId1,
tableId2,
fieldName2,
recordId2
rowId2
) {
// build the ID out of unique references to this link document
this._id = generateLinkID(tableId1, tableId2, recordId1, recordId2)
this._id = generateLinkID(tableId1, tableId2, rowId1, rowId2)
// required for referencing in view
this.type = "link"
this.doc1 = {
tableId: tableId1,
fieldName: fieldName1,
recordId: recordId1,
rowId: rowId1,
}
this.doc2 = {
tableId: tableId2,
fieldName: fieldName2,
recordId: recordId2,
rowId: rowId2,
}
}
class LinkController {
constructor({ instanceId, tableId, record, table, oldTable }) {
constructor({ instanceId, tableId, row, table, oldTable }) {
this._instanceId = instanceId
this._db = new CouchDB(instanceId)
this._tableId = tableId
this._record = record
this._row = row
this._table = table
this._oldTable = oldTable
}
@ -84,11 +84,11 @@ class LinkController {
/**
* Utility function for main getLinkDocuments function - refer to it for functionality.
*/
getRecordLinkDocs(recordId) {
getRowLinkDocs(rowId) {
return getLinkDocuments({
instanceId: this._instanceId,
tableId: this._tableId,
recordId,
rowId,
includeDocs: IncludeDocs.INCLUDE,
})
}
@ -105,43 +105,43 @@ class LinkController {
}
// all operations here will assume that the table
// this operation is related to has linked records
// this operation is related to has linked rows
/**
* When a record is saved this will carry out the necessary operations to make sure
* When a row is saved this will carry out the necessary operations to make sure
* the link has been created/updated.
* @returns {Promise<object>} returns the record that has been cleaned and prepared to be written to the DB - links
* @returns {Promise<object>} returns the row that has been cleaned and prepared to be written to the DB - links
* have also been created.
*/
async recordSaved() {
async rowSaved() {
const table = await this.table()
const record = this._record
const row = this._row
const operations = []
// get link docs to compare against
const linkDocs = await this.getRecordLinkDocs(record._id)
const linkDocs = await this.getRowLinkDocs(row._id)
for (let fieldName of Object.keys(table.schema)) {
// get the links this record wants to make
const recordField = record[fieldName]
// get the links this row wants to make
const rowField = row[fieldName]
const field = table.schema[fieldName]
if (field.type === "link" && recordField != null) {
// check which links actual pertain to the update in this record
if (field.type === "link" && rowField != null) {
// check which links actual pertain to the update in this row
const thisFieldLinkDocs = linkDocs.filter(
linkDoc =>
linkDoc.doc1.fieldName === fieldName ||
linkDoc.doc2.fieldName === fieldName
)
const linkDocIds = thisFieldLinkDocs.map(linkDoc => {
return linkDoc.doc1.recordId === record._id
? linkDoc.doc2.recordId
: linkDoc.doc1.recordId
return linkDoc.doc1.rowId === row._id
? linkDoc.doc2.rowId
: linkDoc.doc1.rowId
})
// iterate through the link IDs in the record field, see if any don't exist already
for (let linkId of recordField) {
// iterate through the link IDs in the row field, see if any don't exist already
for (let linkId of rowField) {
if (linkId && linkId !== "" && linkDocIds.indexOf(linkId) === -1) {
operations.push(
new LinkDocument(
table._id,
fieldName,
record._id,
row._id,
field.tableId,
field.fieldName,
linkId
@ -154,7 +154,7 @@ class LinkController {
.filter(doc => {
let correctDoc =
doc.doc1.fieldName === fieldName ? doc.doc2 : doc.doc1
return recordField.indexOf(correctDoc.recordId) === -1
return rowField.indexOf(correctDoc.rowId) === -1
})
.map(doc => {
return { ...doc, _deleted: true }
@ -162,23 +162,23 @@ class LinkController {
// now add the docs to be deleted to the bulk operation
operations.push(...toDeleteDocs)
// replace this field with a simple entry to denote there are links
delete record[fieldName]
delete row[fieldName]
}
}
await this._db.bulkDocs(operations)
return record
return row
}
/**
* When a record is deleted this will carry out the necessary operations to make sure
* When a row is deleted this will carry out the necessary operations to make sure
* any links that existed have been removed.
* @returns {Promise<object>} The operation has been completed and the link documents should now
* be accurate. This also returns the record that was deleted.
* be accurate. This also returns the row that was deleted.
*/
async recordDeleted() {
const record = this._record
async rowDeleted() {
const row = this._row
// need to get the full link docs to be be able to delete it
const linkDocs = await this.getRecordLinkDocs(record._id)
const linkDocs = await this.getRowLinkDocs(row._id)
if (linkDocs.length === 0) {
return null
}
@ -189,11 +189,11 @@ class LinkController {
}
})
await this._db.bulkDocs(toDelete)
return record
return row
}
/**
* Remove a field from a table as well as any linked records that pertained to it.
* Remove a field from a table as well as any linked rows that pertained to it.
* @param {string} fieldName The field to be removed from the table.
* @returns {Promise<void>} The table has now been updated.
*/

View File

@ -2,14 +2,14 @@ const LinkController = require("./LinkController")
const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils")
/**
* This functionality makes sure that when records with links are created, updated or deleted they are processed
* This functionality makes sure that when rows with links are created, updated or deleted they are processed
* correctly - making sure that no stale links are left around and that all links have been made successfully.
*/
const EventType = {
RECORD_SAVE: "record:save",
RECORD_UPDATE: "record:update",
RECORD_DELETE: "record:delete",
ROW_SAVE: "row:save",
ROW_UPDATE: "row:update",
ROW_DELETE: "row:delete",
TABLE_SAVE: "table:save",
TABLE_UPDATED: "table:updated",
TABLE_DELETE: "table:delete",
@ -22,21 +22,21 @@ exports.getLinkDocuments = getLinkDocuments
exports.createLinkView = createLinkView
/**
* Update link documents for a record or table - this is to be called by the API controller when a change is occurring.
* Update link documents for a row or table - this is to be called by the API controller when a change is occurring.
* @param {string} eventType states what type of change which is occurring, means this can be expanded upon in the
* future quite easily (all updates go through one function).
* @param {string} instanceId The ID of the instance in which the change is occurring.
* @param {string} tableId The ID of the of the table which is being changed.
* @param {object|null} record The record which is changing, e.g. created, updated or deleted.
* @param {object|null} row The row which is changing, e.g. created, updated or deleted.
* @param {object|null} table If the table has already been retrieved this can be used to reduce database gets.
* @param {object|null} oldTable If the table is being updated then the old table can be provided for differencing.
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the record for
* record operations and the table for table operations.
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the row for
* row operations and the table for table operations.
*/
exports.updateLinks = async function({
eventType,
instanceId,
record,
row,
tableId,
table,
oldTable,
@ -54,14 +54,14 @@ exports.updateLinks = async function({
(oldTable == null ||
!(await linkController.doesTableHaveLinkedFields(oldTable)))
) {
return record
return row
}
switch (eventType) {
case EventType.RECORD_SAVE:
case EventType.RECORD_UPDATE:
return await linkController.recordSaved()
case EventType.RECORD_DELETE:
return await linkController.recordDeleted()
case EventType.ROW_SAVE:
case EventType.ROW_UPDATE:
return await linkController.rowSaved()
case EventType.ROW_DELETE:
return await linkController.rowDeleted()
case EventType.TABLE_SAVE:
return await linkController.tableSaved()
case EventType.TABLE_UPDATED:
@ -69,52 +69,52 @@ exports.updateLinks = async function({
case EventType.TABLE_DELETE:
return await linkController.tableDeleted()
default:
throw "Type of event is not known, linked record handler requires update."
throw "Type of event is not known, linked row handler requires update."
}
}
/**
* Update a record with information about the links that pertain to it.
* @param {string} instanceId The instance in which this record has been created.
* @param {object} records The record(s) themselves which is to be updated with info (if applicable). This can be
* a single record object or an array of records - both will be handled.
* @returns {Promise<object>} The updated record (this may be the same if no links were found). If an array was input
* Update a row with information about the links that pertain to it.
* @param {string} instanceId The instance in which this row has been created.
* @param {object} rows The row(s) themselves which is to be updated with info (if applicable). This can be
* a single row object or an array of rows - both will be handled.
* @returns {Promise<object>} The updated row (this may be the same if no links were found). If an array was input
* then an array will be output, object input -> object output.
*/
exports.attachLinkInfo = async (instanceId, records) => {
// handle a single record as well as multiple
exports.attachLinkInfo = async (instanceId, rows) => {
// handle a single row as well as multiple
let wasArray = true
if (!(records instanceof Array)) {
records = [records]
if (!(rows instanceof Array)) {
rows = [rows]
wasArray = false
}
// start by getting all the link values for performance reasons
let responses = await Promise.all(
records.map(record =>
rows.map(row =>
getLinkDocuments({
instanceId,
tableId: record.tableId,
recordId: record._id,
tableId: row.tableId,
rowId: row._id,
includeDocs: IncludeDocs.EXCLUDE,
})
)
)
// can just use an index to access responses, order maintained
let index = 0
// now iterate through the records and all field information
for (let record of records) {
// get all links for record, ignore fieldName for now
// now iterate through the rows and all field information
for (let row of rows) {
// get all links for row, ignore fieldName for now
const linkVals = responses[index++]
for (let linkVal of linkVals) {
// work out which link pertains to this record
if (!(record[linkVal.fieldName] instanceof Array)) {
record[linkVal.fieldName] = [linkVal.id]
// work out which link pertains to this row
if (!(row[linkVal.fieldName] instanceof Array)) {
row[linkVal.fieldName] = [linkVal.id]
} else {
record[linkVal.fieldName].push(linkVal.id)
row[linkVal.fieldName].push(linkVal.id)
}
}
}
// if it was an array when it came in then handle it as an array in response
// otherwise return the first element as there was only one input
return wasArray ? records : records[0]
return wasArray ? rows : rows[0]
}

View File

@ -25,12 +25,12 @@ exports.createLinkView = async instanceId => {
if (doc.type === "link") {
let doc1 = doc.doc1
let doc2 = doc.doc2
emit([doc1.tableId, doc1.recordId], {
id: doc2.recordId,
emit([doc1.tableId, doc1.rowId], {
id: doc2.rowId,
fieldName: doc1.fieldName,
})
emit([doc2.tableId, doc2.recordId], {
id: doc1.recordId,
emit([doc2.tableId, doc2.rowId], {
id: doc1.rowId,
fieldName: doc2.fieldName,
})
}
@ -45,11 +45,11 @@ exports.createLinkView = async instanceId => {
/**
* Gets the linking documents, not the linked documents themselves.
* @param {string} instanceId The instance in which we are searching for linked records.
* @param {string} tableId The table which we are searching for linked records against.
* @param {string} instanceId The instance in which we are searching for linked rows.
* @param {string} tableId The table which we are searching for linked rows against.
* @param {string|null} fieldName The name of column/field which is being altered, only looking for
* linking documents that are related to it. If this is not specified then the table level will be assumed.
* @param {string|null} recordId The ID of the record which we want to find linking documents for -
* @param {string|null} rowId The ID of the row which we want to find linking documents for -
* if this is not specified then it will assume table or field level depending on whether the
* field name has been specified.
* @param {boolean|null} includeDocs whether to include docs in the response call, this is considerably slower so only
@ -60,13 +60,13 @@ exports.createLinkView = async instanceId => {
exports.getLinkDocuments = async function({
instanceId,
tableId,
recordId,
rowId,
includeDocs,
}) {
const db = new CouchDB(instanceId)
let params
if (recordId != null) {
params = { key: [tableId, recordId] }
if (rowId != null) {
params = { key: [tableId, rowId] }
}
// only table is known
else {

View File

@ -5,7 +5,7 @@ const SEPARATOR = "_"
const DocumentTypes = {
TABLE: "ta",
RECORD: "re",
ROW: "re",
USER: "us",
AUTOMATION: "au",
LINK: "li",
@ -19,7 +19,7 @@ exports.SEPARATOR = SEPARATOR
/**
* If creating DB allDocs/query params with only a single top level ID this can be used, this
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
* More complex cases such as link docs and records which have multiple levels of IDs that their
* More complex cases such as link docs and rows which have multiple levels of IDs that their
* ID consists of need their own functions to build the allDocs parameters.
* @param {string} docType The type of document which input params are being built for, e.g. user,
* link, app, table and so on.
@ -55,31 +55,31 @@ exports.generateTableID = () => {
}
/**
* Gets the DB allDocs/query params for retrieving a record.
* @param {string} tableId The table in which the records have been stored.
* @param {string|null} recordId The ID of the record which is being specifically queried for. This can be
* left null to get all the records in the table.
* Gets the DB allDocs/query params for retrieving a row.
* @param {string} tableId The table in which the rows have been stored.
* @param {string|null} rowId The ID of the row which is being specifically queried for. This can be
* left null to get all the rows in the table.
* @param {object} otherProps Any other properties to add to the request.
* @returns {object} Parameters which can then be used with an allDocs request.
*/
exports.getRecordParams = (tableId, recordId = null, otherProps = {}) => {
exports.getRowParams = (tableId, rowId = null, otherProps = {}) => {
if (tableId == null) {
throw "Cannot build params for records without a table ID"
throw "Cannot build params for rows without a table ID"
}
const endOfKey =
recordId == null
rowId == null
? `${tableId}${SEPARATOR}`
: `${tableId}${SEPARATOR}${recordId}`
return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps)
: `${tableId}${SEPARATOR}${rowId}`
return getDocParams(DocumentTypes.ROW, endOfKey, otherProps)
}
/**
* Gets a new record ID for the specified table.
* @param {string} tableId The table which the record is being created for.
* @returns {string} The new ID which a record doc can be stored under.
* Gets a new row ID for the specified table.
* @param {string} tableId The table which the row is being created for.
* @returns {string} The new ID which a row doc can be stored under.
*/
exports.generateRecordID = tableId => {
return `${DocumentTypes.RECORD}${SEPARATOR}${tableId}${SEPARATOR}${newid()}`
exports.generateRowID = tableId => {
return `${DocumentTypes.ROW}${SEPARATOR}${tableId}${SEPARATOR}${newid()}`
}
/**
@ -118,12 +118,12 @@ exports.generateAutomationID = () => {
* instead a view is built to make walking to tree easier.
* @param {string} tableId1 The ID of the linker table.
* @param {string} tableId2 The ID of the linked table.
* @param {string} recordId1 The ID of the linker record.
* @param {string} recordId2 The ID of the linked record.
* @param {string} rowId1 The ID of the linker row.
* @param {string} rowId2 The ID of the linked row.
* @returns {string} The new link doc ID which the automation doc can be stored under.
*/
exports.generateLinkID = (tableId1, tableId2, recordId1, recordId2) => {
return `${DocumentTypes.AUTOMATION}${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}${SEPARATOR}${recordId1}${SEPARATOR}${recordId2}`
exports.generateLinkID = (tableId1, tableId2, rowId1, rowId2) => {
return `${DocumentTypes.AUTOMATION}${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}`
}
/**

View File

@ -11,18 +11,18 @@ const EventEmitter = require("events").EventEmitter
* This is specifically quite important for mustache used in automations.
*/
class BudibaseEmitter extends EventEmitter {
emitRecord(eventName, instanceId, record, table = null) {
emitRow(eventName, instanceId, row, table = null) {
let event = {
record,
row,
instanceId,
tableId: record.tableId,
tableId: row.tableId,
}
if (table) {
event.table = table
}
event.id = record._id
if (record._rev) {
event.revision = record._rev
event.id = row._id
if (row._rev) {
event.revision = row._rev
}
this.emit(eventName, event)
}

View File

@ -9,7 +9,7 @@ const METHOD_MAP = {
}
const DOMAIN_MAP = {
records: usageQuota.Properties.RECORD,
rows: usageQuota.Properties.ROW,
upload: usageQuota.Properties.UPLOAD,
views: usageQuota.Properties.VIEW,
users: usageQuota.Properties.USER,

View File

@ -2,7 +2,7 @@ const environment = require("../environment")
const { apiKeyTable } = require("../db/dynamoClient")
const DEFAULT_USAGE = {
records: 0,
rows: 0,
storage: 0,
views: 0,
automationRuns: 0,
@ -10,7 +10,7 @@ const DEFAULT_USAGE = {
}
const DEFAULT_PLAN = {
records: 1000,
rows: 1000,
// 1 GB
storage: 8589934592,
views: 10,
@ -42,7 +42,7 @@ function getNewQuotaReset() {
}
exports.Properties = {
RECORD: "records",
ROW: "rows",
UPLOAD: "storage",
VIEW: "views",
USER: "users",

View File

@ -1,8 +1,8 @@
{
"_lib": "./dist/index.js",
"_templates": {
"saveRecordButton": {
"description": "Save record button",
"saveRowButton": {
"description": "Save row button",
"component": "button"
}
},
@ -269,8 +269,8 @@
"destinationUrl": "string"
}
},
"recorddetail": {
"description": "Loads a record, using an ID in the url",
"rowdetail": {
"description": "Loads a row, using an ID in the url",
"context": "table",
"children": true,
"data": true,

View File

@ -19392,8 +19392,8 @@ var app = (function (crypto$1) {
const common = () => commonPlus([]);
const _events = {
recordApi: {
save: commonPlus(["onInvalid", "onRecordUpdated", "onRecordCreated"]),
rowApi: {
save: commonPlus(["onInvalid", "onRowUpdated", "onRowCreated"]),
delete: common(),
getContext: common(),
getNew: common(),
@ -19409,7 +19409,7 @@ var app = (function (crypto$1) {
aggregates: common(),
},
collectionApi: {
getAllowedRecordTypes: common(),
getAllowedRowTypes: common(),
initialise: common(),
delete: common(),
},
@ -21240,7 +21240,7 @@ var app = (function (crypto$1) {
),
makerule(
"indexType",
"reference index may only exist on a record node",
"reference index may only exist on a row node",
index =>
isTable(index.parent()) || index.indexType !== indexTypes.reference
),
@ -21298,7 +21298,7 @@ var app = (function (crypto$1) {
getFlattenedHierarchy,
fp_1(
n =>
isCollectionRecord(n) &&
isCollectionRow(n) &&
new RegExp(`${n.collectionPathRegx()}$`).test(collectionKey)
),
]);
@ -21309,7 +21309,7 @@ var app = (function (crypto$1) {
fp_1(
n =>
n.nodeKey() === nodeKey ||
(isCollectionRecord(n) && n.collectionNodeKey() === nodeKey)
(isCollectionRow(n) && n.collectionNodeKey() === nodeKey)
),
]);
@ -21321,20 +21321,20 @@ var app = (function (crypto$1) {
const isNode = (appHierarchy, key) =>
isSomething(getExactNodeForKey(appHierarchy)(key));
const isTable = node => isSomething(node) && node.type === "record";
const isSingleRecord = node => isTable(node) && node.isSingle;
const isCollectionRecord = node => isTable(node) && !node.isSingle;
const isTable = node => isSomething(node) && node.type === "row";
const isSingleRow = node => isTable(node) && node.isSingle;
const isCollectionRow = node => isTable(node) && !node.isSingle;
const isRoot = node => isSomething(node) && node.isRoot();
const getSafeFieldParser = (tryParse, defaultValueFunctions) => (
field,
record
row
) => {
if (fp_25(field.name)(record)) {
if (fp_25(field.name)(row)) {
return getSafeValueParser(
tryParse,
defaultValueFunctions
)(record[field.name])
)(row[field.name])
}
return defaultValueFunctions[field.getUndefinedValue]()
};
@ -21372,10 +21372,10 @@ var app = (function (crypto$1) {
const validateTypeConstraints = validationRules => async (
field,
record,
row,
context
) => {
const fieldValue = record[field.name];
const fieldValue = row[field.name];
const validateRule = async r =>
!(await r.isValid(fieldValue, field.typeOptions, context))
? r.getMessage(fieldValue, field.typeOptions)
@ -21880,10 +21880,10 @@ var app = (function (crypto$1) {
);
const permissionTypes = {
CREATE_RECORD: "create record",
UPDATE_RECORD: "update record",
READ_RECORD: "read record",
DELETE_RECORD: "delete record",
CREATE_ROW: "create row",
UPDATE_ROW: "update row",
READ_ROW: "read row",
DELETE_ROW: "delete row",
READ_INDEX: "read index",
MANAGE_INDEX: "manage index",
MANAGE_COLLECTION: "manage collection",
@ -21953,13 +21953,13 @@ var app = (function (crypto$1) {
get: () => ({ type }),
});
const createRecord = nodePermission(permissionTypes.CREATE_RECORD);
const createRow = nodePermission(permissionTypes.CREATE_ROW);
const updateRecord = nodePermission(permissionTypes.UPDATE_RECORD);
const updateRow = nodePermission(permissionTypes.UPDATE_ROW);
const deleteRecord = nodePermission(permissionTypes.DELETE_RECORD);
const deleteRow = nodePermission(permissionTypes.DELETE_ROW);
const readRecord = nodePermission(permissionTypes.READ_RECORD);
const readRow = nodePermission(permissionTypes.READ_ROW);
const writeTemplates = staticPermission(permissionTypes.WRITE_TEMPLATES);
@ -21994,10 +21994,10 @@ var app = (function (crypto$1) {
const alwaysAuthorized = () => true;
const permission = {
createRecord,
updateRecord,
deleteRecord,
readRecord,
createRow,
updateRow,
deleteRow,
readRow,
writeTemplates,
createUser,
setPassword,
@ -22013,41 +22013,41 @@ var app = (function (crypto$1) {
setUserAccessLevels,
};
const getNew = app => (collectionKey, recordTypeName) => {
const recordNode = getRecordNode(app, collectionKey);
const getNew = app => (collectionKey, rowTypeName) => {
const rowNode = getRowNode(app, collectionKey);
collectionKey = safeKey(collectionKey);
return apiWrapperSync(
app,
events.recordApi.getNew,
permission.createRecord.isAuthorized(recordNode.nodeKey()),
{ collectionKey, recordTypeName },
events.rowApi.getNew,
permission.createRow.isAuthorized(rowNode.nodeKey()),
{ collectionKey, rowTypeName },
_getNew,
recordNode,
rowNode,
collectionKey
)
};
const _getNew = (recordNode, collectionKey) =>
constructRecord(recordNode, getNewFieldValue, collectionKey);
const _getNew = (rowNode, collectionKey) =>
constructRow(rowNode, getNewFieldValue, collectionKey);
const getRecordNode = (app, collectionKey) => {
const getRowNode = (app, collectionKey) => {
collectionKey = safeKey(collectionKey);
return getNodeForCollectionPath(app.hierarchy)(collectionKey)
};
const getNewChild = app => (recordKey, collectionName, recordTypeName) =>
getNew(app)(joinKey(recordKey, collectionName), recordTypeName);
const getNewChild = app => (rowKey, collectionName, rowTypeName) =>
getNew(app)(joinKey(rowKey, collectionName), rowTypeName);
const constructRecord = (recordNode, getFieldValue, collectionKey) => {
const record = $(recordNode.fields, [fp_35("name"), fp_26(getFieldValue)]);
const constructRow = (rowNode, getFieldValue, collectionKey) => {
const row = $(rowNode.fields, [fp_35("name"), fp_26(getFieldValue)]);
record.id = `${recordNode.nodeId}-${shortid_1()}`;
record.key = isSingleRecord(recordNode)
? joinKey(collectionKey, recordNode.name)
: joinKey(collectionKey, record.id);
record.isNew = true;
record.type = recordNode.name;
return record
row.id = `${rowNode.nodeId}-${shortid_1()}`;
row.key = isSingleRow(rowNode)
? joinKey(collectionKey, rowNode.name)
: joinKey(collectionKey, row.id);
row.isNew = true;
row.type = rowNode.name;
return row
};
const pathRegxMaker = node => () =>
@ -22056,7 +22056,7 @@ var app = (function (crypto$1) {
const nodeKeyMaker = node => () =>
switchCase(
[
n => isTable(n) && !isSingleRecord(n),
n => isTable(n) && !isSingleRow(n),
n =>
joinKey(
node.parent().nodeKey(),
@ -22076,7 +22076,7 @@ var app = (function (crypto$1) {
node.parent = fp_21(parent);
node.isRoot = () =>
isNothing(parent) && node.name === "root" && node.type === "root";
if (isCollectionRecord(node)) {
if (isCollectionRow(node)) {
node.collectionNodeKey = () =>
joinKey(parent.nodeKey(), node.collectionName);
node.collectionPathRegx = () =>
@ -22116,7 +22116,7 @@ var app = (function (crypto$1) {
const app = createCoreApp(backendDefinition, user);
return {
recordApi: {
rowApi: {
getNew: getNew(app),
getNewChild: getNewChild(app),
},
@ -22232,40 +22232,40 @@ var app = (function (crypto$1) {
const ERROR = "##error_message";
const loadRecord = api => async ({ recordKey, statePath }) => {
if (!recordKey) {
api.error("Load Record: record key not set");
const loadRow = api => async ({ rowKey, statePath }) => {
if (!rowKey) {
api.error("Load Row: row key not set");
return
}
if (!statePath) {
api.error("Load Record: state path not set");
api.error("Load Row: state path not set");
return
}
const record = await api.get({
url: `/api/record/${trimSlash(recordKey)}`,
const row = await api.get({
url: `/api/row/${trimSlash(rowKey)}`,
});
if (api.isSuccess(record)) api.setState(statePath, record);
if (api.isSuccess(row)) api.setState(statePath, row);
};
const listRecords = api => async ({ indexKey, statePath }) => {
const listRows = api => async ({ indexKey, statePath }) => {
if (!indexKey) {
api.error("Load Record: record key not set");
api.error("Load Row: row key not set");
return
}
if (!statePath) {
api.error("Load Record: state path not set");
api.error("Load Row: state path not set");
return
}
const records = await api.get({
url: `/api/listRecords/${trimSlash(indexKey)}`,
const rows = await api.get({
url: `/api/listRows/${trimSlash(indexKey)}`,
});
if (api.isSuccess(records)) api.setState(statePath, records);
if (api.isSuccess(rows)) api.setState(statePath, rows);
};
const USER_STATE_PATH = "_bbuser";
@ -22291,32 +22291,32 @@ var app = (function (crypto$1) {
localStorage.setItem("budibase:user", JSON.stringify(user));
};
const saveRecord = api => async ({ statePath }) => {
const saveRow = api => async ({ statePath }) => {
if (!statePath) {
api.error("Load Record: state path not set");
api.error("Load Row: state path not set");
return
}
const recordtoSave = api.getState(statePath);
const rowtoSave = api.getState(statePath);
if (!recordtoSave) {
api.error(`there is no record in state: ${statePath}`);
if (!rowtoSave) {
api.error(`there is no row in state: ${statePath}`);
return
}
if (!recordtoSave.key) {
if (!rowtoSave.key) {
api.error(
`item in state does not appear to be a record - it has no key (${statePath})`
`item in state does not appear to be a row - it has no key (${statePath})`
);
return
}
const savedRecord = await api.post({
url: `/api/record/${trimSlash(recordtoSave.key)}`,
body: recordtoSave,
const savedRow = await api.post({
url: `/api/row/${trimSlash(rowtoSave.key)}`,
body: rowtoSave,
});
if (api.isSuccess(savedRecord)) api.setState(statePath, savedRecord);
if (api.isSuccess(savedRow)) api.setState(statePath, savedRow);
};
const createApi = ({ rootPath, setState, getState }) => {
@ -22383,23 +22383,23 @@ var app = (function (crypto$1) {
};
return {
loadRecord: loadRecord(apiOpts),
listRecords: listRecords(apiOpts),
loadRow: loadRow(apiOpts),
listRows: listRows(apiOpts),
authenticate: authenticate(apiOpts),
saveRecord: saveRecord(apiOpts),
saveRow: saveRow(apiOpts),
}
};
const getNewChildRecordToState = (coreApi, setState) => ({
recordKey,
const getNewChildRowToState = (coreApi, setState) => ({
rowKey,
collectionName,
childRecordType,
childRowType,
statePath,
}) => {
const error = errorHandler(setState);
try {
if (!recordKey) {
error("getNewChild > recordKey not set");
if (!rowKey) {
error("getNewChild > rowKey not set");
return
}
@ -22408,8 +22408,8 @@ var app = (function (crypto$1) {
return
}
if (!childRecordType) {
error("getNewChild > childRecordType not set");
if (!childRowType) {
error("getNewChild > childRowType not set");
return
}
@ -22418,10 +22418,10 @@ var app = (function (crypto$1) {
return
}
const rec = coreApi.recordApi.getNewChild(
recordKey,
const rec = coreApi.rowApi.getNewChild(
rowKey,
collectionName,
childRecordType
childRowType
);
setState(statePath, rec);
} catch (e) {
@ -22429,9 +22429,9 @@ var app = (function (crypto$1) {
}
};
const getNewRecordToState = (coreApi, setState) => ({
const getNewRowToState = (coreApi, setState) => ({
collectionKey,
childRecordType,
childRowType,
statePath,
}) => {
const error = errorHandler(setState);
@ -22441,8 +22441,8 @@ var app = (function (crypto$1) {
return
}
if (!childRecordType) {
error("getNewChild > childRecordType not set");
if (!childRowType) {
error("getNewChild > childRowType not set");
return
}
@ -22451,7 +22451,7 @@ var app = (function (crypto$1) {
return
}
const rec = coreApi.recordApi.getNew(collectionKey, childRecordType);
const rec = coreApi.rowApi.getNew(collectionKey, childRowType);
setState(statePath, rec);
} catch (e) {
error(e.message);
@ -22485,18 +22485,18 @@ var app = (function (crypto$1) {
return {
"Set State": handler(["path", "value"], setStateHandler),
"Load Record": handler(["recordKey", "statePath"], api.loadRecord),
"List Records": handler(["indexKey", "statePath"], api.listRecords),
"Save Record": handler(["statePath"], api.saveRecord),
"Load Row": handler(["rowKey", "statePath"], api.loadRow),
"List Rows": handler(["indexKey", "statePath"], api.listRows),
"Save Row": handler(["statePath"], api.saveRow),
"Get New Child Record": handler(
["recordKey", "collectionName", "childRecordType", "statePath"],
getNewChildRecordToState(coreApi, setStateWithStore)
"Get New Child Row": handler(
["rowKey", "collectionName", "childRowType", "statePath"],
getNewChildRowToState(coreApi, setStateWithStore)
),
"Get New Record": handler(
["collectionKey", "childRecordType", "statePath"],
getNewRecordToState(coreApi, setStateWithStore)
"Get New Row": handler(
["collectionKey", "childRowType", "statePath"],
getNewRowToState(coreApi, setStateWithStore)
),
Authenticate: handler(["username", "password"], api.authenticate),

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@ window["##BUDIBASE_APPDEFINITION##"] = {
children: [
{
name: "customer",
type: "record",
type: "row",
fields: [
{
name: "name",
@ -23,7 +23,7 @@ window["##BUDIBASE_APPDEFINITION##"] = {
children: [
{
name: "invoiceyooo",
type: "record",
type: "row",
fields: [
{
name: "amount",
@ -53,11 +53,11 @@ window["##BUDIBASE_APPDEFINITION##"] = {
{
name: "customer_invoices",
type: "index",
map: "return {...record};",
map: "return {...row};",
filter: "",
indexType: "ancestor",
getShardName: "",
getSortKey: "record.id",
getSortKey: "row.id",
aggregateGroups: [],
allowedTableNodeIds: [2],
nodeId: 5,
@ -73,11 +73,11 @@ window["##BUDIBASE_APPDEFINITION##"] = {
{
name: "Yeo index",
type: "index",
map: "return {...record};",
map: "return {...row};",
filter: "",
indexType: "ancestor",
getShardName: "",
getSortKey: "record.id",
getSortKey: "row.id",
aggregateGroups: [],
allowedTableNodeIds: [1],
nodeId: 4,
@ -85,11 +85,11 @@ window["##BUDIBASE_APPDEFINITION##"] = {
{
name: "everyones_invoices",
type: "index",
map: "return {...record};",
map: "return {...row};",
filter: "",
indexType: "ancestor",
getShardName: "",
getSortKey: "record.id",
getSortKey: "row.id",
aggregateGroups: [],
allowedTableNodeIds: [2],
nodeId: 6,

View File

@ -54,8 +54,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -63,7 +63,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -46,8 +46,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -55,7 +55,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -99,16 +99,16 @@
let ignoreList = ["_id", "_rev", "id"]
if (dataKey && data.every(d => d[dataKey])) {
return data.map(d => {
let clonedRecord = { ...d }
if (clonedRecord[formatKey]) {
delete clonedRecord[formatKey]
let clonedRow = { ...d }
if (clonedRow[formatKey]) {
delete clonedRow[formatKey]
}
let value = clonedRecord[dataKey]
let value = clonedRow[dataKey]
if (!ignoreList.includes(dataKey)) {
delete clonedRecord[dataKey]
delete clonedRow[dataKey]
}
clonedRecord[formatKey] = value
return clonedRecord
clonedRow[formatKey] = value
return clonedRow
})
} else {
return data

View File

@ -41,8 +41,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -50,7 +50,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -70,8 +70,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -79,7 +79,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -45,8 +45,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -54,7 +54,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -70,8 +70,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -79,7 +79,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -65,8 +65,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -74,7 +74,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -60,8 +60,8 @@
})
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -69,7 +69,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -32,7 +32,7 @@ audited: new Date("2020-01-01T16:00:00-08:00"),
city: "Belfast",
name: 1,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "ceb87054790f480e80512368545755bb",
_rev: "2-56e401ebaf59e6310b85fb0c6c2fece5",
},
@ -42,7 +42,7 @@ audited: new Date("2020-01-03T16:00:00-08:00"),
city: "Belfast",
name: 1,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "0a36103b55124f348a23d10b2f3ed0e3",
_rev: "2-50d62530b2edfc63d5fd0b3719dbb286",
},
@ -52,7 +52,7 @@ audited: new Date("2020-01-04T16:00:00-08:00"),
city: "Belfast",
name: 1,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "68ade2bb94754caa8fc62c7084e3cef7",
_rev: "2-a03fe02f3595920adfbcd9c70564fe9d",
},
@ -62,7 +62,7 @@ audited: new Date("2020-01-01T16:00:00-08:00"),
city: "Dublin",
name: 2,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "2ab6dabf833f4d99b3438fa4353ba429",
_rev: "2-45b190489e76842981902cc9f04369ec",
},
@ -72,7 +72,7 @@ audited: new Date("2020-01-02T16:00:00-08:00"),
city: "Dublin",
name: 2,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "1b2ca36db1724427a98ba95547f946e0",
_rev: "2-c43def17ada959948b9af5484ad5b6b7",
},
@ -82,7 +82,7 @@ audited: new Date("2020-01-03T16:00:00-08:00"),
city: "Dublin",
name: 2,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "d9235d884a224ca68ac30cefdbb8ae53",
_rev: "2-695e426a261a25474cbf6b1f069dccb4",
},
@ -92,7 +92,7 @@ audited: new Date("2020-01-04T16:00:00-08:00"),
city: "Dublin",
name: 2,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "9f8bc39a9cfb4f779da8c998d7622927",
_rev: "2-8ae1aff82e1ffc6ffa75f6b9d074e003",
},
@ -102,7 +102,7 @@ audited: new Date("2020-01-02T16:00:00-08:00"),
city: "London",
name: 3,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "75274e906073493bbf75cda8656e8db0",
_rev: "2-6cfc6bb2fccb83c92b50aa5507f2a092"
},
@ -112,7 +112,7 @@ audited: new Date("2020-01-06T16:00:00-08:00"),
city: "London",
name: 3,
tableId: "2334751ac0764c1a931bff5b6b6767eb",
type: "record",
type: "row",
_id: "da3d4b151bc641f4ace487a2314d2550",
_rev: "2-ac18490eaa016be0e71bd4c4ea332981",
}

View File

@ -24,16 +24,16 @@ export function reformatDataKey(data = [], dataKey = null, formatKey = null) {
let ignoreList = ["_id", "_rev", "id"]
if (dataKey && data.every(d => d[dataKey])) {
return data.map(d => {
let clonedRecord = { ...d }
if (clonedRecord[formatKey]) {
delete clonedRecord[formatKey]
let clonedRow = { ...d }
if (clonedRow[formatKey]) {
delete clonedRow[formatKey]
}
let value = clonedRecord[dataKey]
let value = clonedRow[dataKey]
if (!ignoreList.includes(dataKey)) {
delete clonedRecord[dataKey]
delete clonedRow[dataKey]
}
clonedRecord[formatKey] = value
return clonedRecord
clonedRow[formatKey] = value
return clonedRow
})
} else {
return data

View File

@ -26,8 +26,8 @@
$: console.log("CHART CONFIGS", chartConfigs)
async function fetchData() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
store.update(state => {
@ -35,7 +35,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -11,8 +11,8 @@
async function fetchData() {
if (!table || !table.length) return
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const json = await response.json()
@ -21,7 +21,7 @@
return state
})
} else {
throw new Error("Failed to fetch records.", response)
throw new Error("Failed to fetch rows.", response)
}
}

View File

@ -49,7 +49,7 @@
const shouldDisplayField = name => {
if (name.startsWith("_")) return false
// always 'record'
// always 'row'
if (name === "type") return false
// tables are always tied to a single tableId, this is irrelevant
if (name === "tableId") return false

View File

@ -10,7 +10,7 @@
Toggle,
} from "@budibase/bbui"
import Dropzone from "./attachments/Dropzone.svelte"
import LinkedRecordSelector from "./LinkedRecordSelector.svelte"
import LinkedRowSelector from "./LinkedRowSelector.svelte"
import debounce from "lodash.debounce"
import ErrorsBox from "./ErrorsBox.svelte"
import { capitalise } from "./helpers"
@ -34,12 +34,12 @@
link: [],
}
let record
let row
let store = _bb.store
let schema = {}
let tableDef = {}
let saved = false
let recordId
let rowId
let isNew = true
let errors = {}
@ -53,7 +53,7 @@
const response = await _bb.api.get(FETCH_TABLE_URL)
tableDef = await response.json()
schema = tableDef.schema
record = {
row = {
tableId: table,
}
}
@ -61,13 +61,13 @@
const save = debounce(async () => {
for (let field of fields) {
// Assign defaults to empty fields to prevent validation issues
if (!(field in record)) {
record[field] = DEFAULTS_FOR_TYPE[schema[field].type]
if (!(field in row)) {
row[field] = DEFAULTS_FOR_TYPE[schema[field].type]
}
}
const SAVE_RECORD_URL = `/api/${table}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, record)
const SAVE_ROW_URL = `/api/${table}/rows`
const response = await _bb.api.post(SAVE_ROW_URL, row)
const json = await response.json()
@ -79,9 +79,9 @@
errors = {}
// wipe form, if new record, otherwise update
// wipe form, if new row, otherwise update
// table to get new _rev
record = isNew ? { tableId: table } : json
row = isNew ? { tableId: table } : json
// set saved, and unset after 1 second
// i.e. make the success notifier appear, then disappear again after time
@ -100,18 +100,18 @@
onMount(async () => {
const routeParams = _bb.routeParams()
recordId =
rowId =
Object.keys(routeParams).length > 0 && (routeParams.id || routeParams[0])
isNew = !recordId || recordId === "new"
isNew = !rowId || rowId === "new"
if (isNew) {
record = { tableId: table }
row = { tableId: table }
return
}
const GET_RECORD_URL = `/api/${table}/records/${recordId}`
const response = await _bb.api.get(GET_RECORD_URL)
record = await response.json()
const GET_ROW_URL = `/api/${table}/rows/${rowId}`
const response = await _bb.api.get(GET_ROW_URL)
row = await response.json()
})
</script>
@ -129,29 +129,29 @@
</Label>
{/if}
{#if schema[field].type === 'options'}
<Select secondary bind:value={record[field]}>
<Select secondary bind:value={row[field]}>
<option value="">Choose an option</option>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</Select>
{:else if schema[field].type === 'datetime'}
<DatePicker bind:value={record[field]} />
<DatePicker bind:value={row[field]} />
{:else if schema[field].type === 'boolean'}
<Toggle
text={wide ? null : capitalise(schema[field].name)}
bind:checked={record[field]} />
bind:checked={row[field]} />
{:else if schema[field].type === 'number'}
<Input type="number" bind:value={record[field]} />
<Input type="number" bind:value={row[field]} />
{:else if schema[field].type === 'string'}
<Input bind:value={record[field]} />
<Input bind:value={row[field]} />
{:else if schema[field].type === 'attachment'}
<Dropzone bind:files={record[field]} />
<Dropzone bind:files={row[field]} />
{:else if schema[field].type === 'link'}
<LinkedRecordSelector
<LinkedRowSelector
secondary
showLabel={false}
bind:linkedRecords={record[field]}
bind:linkedRows={row[field]}
schema={schema[field]} />
{/if}
</div>

View File

@ -5,7 +5,7 @@
import { capitalise } from "./helpers"
export let schema = {}
export let linkedRecords = []
export let linkedRows = []
export let showLabel = true
export let secondary
@ -13,7 +13,7 @@
$: label = capitalise(schema.name)
$: linkedTableId = schema.tableId
$: recordsPromise = fetchRecords(linkedTableId)
$: rowsPromise = fetchRows(linkedTableId)
$: fetchTable(linkedTableId)
async function fetchTable() {
@ -25,17 +25,17 @@
linkedTable = await response.json()
}
async function fetchRecords(linkedTableId) {
async function fetchRows(linkedTableId) {
if (linkedTableId == null) {
return
}
const FETCH_RECORDS_URL = `/api/${linkedTableId}/records`
const response = await api.get(FETCH_RECORDS_URL)
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
const response = await api.get(FETCH_ROWS_URL)
return await response.json()
}
function getPrettyName(record) {
return record[(linkedTable && linkedTable.primaryDisplay) || "_id"]
function getPrettyName(row) {
return row[(linkedTable && linkedTable.primaryDisplay) || "_id"]
}
</script>
@ -50,14 +50,14 @@
table.
</Label>
{:else}
{#await recordsPromise then records}
{#await rowsPromise then rows}
<Multiselect
{secondary}
bind:value={linkedRecords}
bind:value={linkedRows}
label={showLabel ? label : null}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{#each rows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/await}

View File

@ -14,46 +14,46 @@
return await response.json()
}
async function fetchFirstRecord() {
const FETCH_RECORDS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
async function fetchFirstRow() {
const FETCH_ROWS_URL = `/api/views/all_${table}`
const response = await _bb.api.get(FETCH_ROWS_URL)
if (response.status === 200) {
const allRecords = await response.json()
if (allRecords.length > 0) return allRecords[0]
const allRows = await response.json()
if (allRows.length > 0) return allRows[0]
}
}
async function fetchData() {
const pathParts = window.location.pathname.split("/")
let record
let row
// if srcdoc, then we assume this is the builder preview
if (pathParts.length === 0 || pathParts[0] === "srcdoc") {
record = await fetchFirstRecord()
row = await fetchFirstRow()
} else {
const id = pathParts[pathParts.length - 1]
const GET_RECORD_URL = `/api/${table}/records/${id}`
const response = await _bb.api.get(GET_RECORD_URL)
const GET_ROW_URL = `/api/${table}/rows/${id}`
const response = await _bb.api.get(GET_ROW_URL)
if (response.status === 200) {
record = await response.json()
row = await response.json()
}
}
if (record) {
// Fetch table schema so we can check for linked records
const table = await fetchTable(record.tableId)
if (row) {
// Fetch table schema so we can check for linked rows
const table = await fetchTable(row.tableId)
for (let key of Object.keys(table.schema)) {
if (table.schema[key].type === "link") {
record[key] = Array.isArray(record[key]) ? record[key].length : 0
row[key] = Array.isArray(row[key]) ? row[key].length : 0
}
}
_bb.attachChildren(target, {
hydrate: false,
context: record,
context: row,
})
} else {
throw new Error("Failed to fetch record.", response)
throw new Error("Failed to fetch row.", response)
}
}

View File

@ -1,22 +1,22 @@
export default ({ records }) =>
records.map(r => ({
export default ({ rows }) =>
rows.map(r => ({
name: `Save ${r.name} Button`,
props: buttonProps(r),
}))
const buttonProps = record => ({
const buttonProps = row => ({
_component: "@budibase/standard-components/button",
_children: [
{
_component: "@budibase/standard-components/text",
text: `Save ${record.name}`,
text: `Save ${row.name}`,
},
],
onClick: [
{
"##eventHandlerType": "Save Record",
"##eventHandlerType": "Save Row",
parameters: {
statePath: `${record.name}`,
statePath: `${row.name}`,
},
},
],

View File

@ -4,22 +4,22 @@ export default async function fetchData(datasource) {
const { isTable, name } = datasource
if (name) {
const records = isTable ? await fetchTableData() : await fetchViewData()
const rows = isTable ? await fetchTableData() : await fetchViewData()
// Fetch table schema so we can check for linked records
if (records && records.length) {
const table = await fetchTable(records[0].tableId)
// Fetch table schema so we can check for linked rows
if (rows && rows.length) {
const table = await fetchTable(rows[0].tableId)
const keys = Object.keys(table.schema)
records.forEach(record => {
rows.forEach(row => {
for (let key of keys) {
if (table.schema[key].type === "link") {
record[key] = Array.isArray(record[key]) ? record[key].length : 0
row[key] = Array.isArray(row[key]) ? row[key].length : 0
}
}
})
}
return records
return rows
}
async function fetchTable(id) {

View File

@ -11,7 +11,7 @@ export { default as radiobutton } from "./Radiobutton.svelte"
export { default as option } from "./Option.svelte"
export { default as button } from "./Button.svelte"
export { default as login } from "./Login.svelte"
export { default as saveRecordButton } from "./Templates/saveRecordButton"
export { default as saveRowButton } from "./Templates/saveRowButton"
export { default as link } from "./Link.svelte"
export { default as image } from "./Image.svelte"
export { default as Navigation } from "./Navigation.svelte"
@ -26,7 +26,7 @@ export { default as embed } from "./Embed.svelte"
export { default as stackedlist } from "./StackedList.svelte"
export { default as card } from "./Card.svelte"
export { default as cardhorizontal } from "./CardHorizontal.svelte"
export { default as recorddetail } from "./RecordDetail.svelte"
export { default as rowdetail } from "./RowDetail.svelte"
export { default as datepicker } from "./DatePicker.svelte"
export * from "./Chart"
export { default as icon } from "./Icon.svelte"