Merge branch 'master' of github.com:Budibase/budibase into bugz

This commit is contained in:
Martin McKeaveney 2020-10-13 12:52:55 +01:00
commit 55527784ed
154 changed files with 3971 additions and 3202 deletions

View File

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

View File

@ -226,7 +226,7 @@
"company": { "company": {
"name": "Hoeger LLC", "name": "Hoeger LLC",
"catchPhrase": "Centralized empowering task-force", "catchPhrase": "Centralized empowering task-force",
"bs": "target end-to-end models" "bs": "target end-to-end tables"
} }
} }
] ]

View File

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

View File

@ -4,9 +4,9 @@ xcontext('Create Components', () => {
cy.server() cy.server()
cy.visit('localhost:4001/_builder') cy.visit('localhost:4001/_builder')
// https://on.cypress.io/type // https://on.cypress.io/type
cy.createApp('Model App', 'Model App Description') cy.createApp('Table App', 'Table App Description')
cy.createTable('dog', 'name', 'age') cy.createTable('dog', 'name', 'age')
cy.addRecord('bob', '15') cy.addRow('bob', '15')
}) })
// https://on.cypress.io/interacting-with-elements // https://on.cypress.io/interacting-with-elements

View File

@ -16,8 +16,8 @@ context("Create a Table", () => {
cy.contains("name").should("be.visible") cy.contains("name").should("be.visible")
}) })
it("creates a record in the table", () => { it("creates a row in the table", () => {
cy.addRecord(["Rover"]) cy.addRow(["Rover"])
cy.contains("Rover").should("be.visible") cy.contains("Rover").should("be.visible")
}) })
@ -32,7 +32,7 @@ context("Create a Table", () => {
cy.contains("nameupdated").should("have.text", "nameupdated") cy.contains("nameupdated").should("have.text", "nameupdated")
}) })
it("edits a record", () => { it("edits a row", () => {
cy.get("tbody .ri-more-line").click() cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=edit-row]").click() cy.get("[data-cy=edit-row]").click()
cy.get(".modal input").type("Updated") cy.get(".modal input").type("Updated")
@ -40,7 +40,7 @@ context("Create a Table", () => {
cy.contains("RoverUpdated").should("have.text", "RoverUpdated") cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
}) })
it("deletes a record", () => { it("deletes a row", () => {
cy.get("tbody .ri-more-line").click() cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=delete-row]").click() cy.get("[data-cy=delete-row]").click()
cy.contains("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", "age", "Number")
cy.addColumn("data", "rating", "Number") cy.addColumn("data", "rating", "Number")
// 6 Records // 6 Rows
cy.addRecord(["Students", 25, 1]) cy.addRow(["Students", 25, 1])
cy.addRecord(["Students", 20, 3]) cy.addRow(["Students", 20, 3])
cy.addRecord(["Students", 18, 6]) cy.addRow(["Students", 18, 6])
cy.addRecord(["Students", 25, 2]) cy.addRow(["Students", 25, 2])
cy.addRecord(["Teachers", 49, 5]) cy.addRow(["Teachers", 49, 5])
cy.addRecord(["Teachers", 36, 3]) cy.addRow(["Teachers", 36, 3])
}) })
it("creates a view", () => { it("creates a view", () => {
@ -109,7 +109,7 @@ context("Create a View", () => {
}) })
it("renames a view", () => { it("renames a view", () => {
cy.contains("[data-cy=model-nav-item]", "Test View") cy.contains("[data-cy=table-nav-item]", "Test View")
.find(".ri-more-line") .find(".ri-more-line")
.click() .click()
cy.contains("Edit").click() cy.contains("Edit").click()
@ -121,8 +121,8 @@ context("Create a View", () => {
}) })
it("deletes a view", () => { it("deletes a view", () => {
cy.contains("[data-cy=model-nav-item]", "Test View Updated").click() cy.contains("[data-cy=table-nav-item]", "Test View Updated").click()
cy.contains("[data-cy=model-nav-item]", "Test View Updated") cy.contains("[data-cy=table-nav-item]", "Test View Updated")
.find(".ri-more-line") .find(".ri-more-line")
.click() .click()
cy.contains("Delete").click() cy.contains("Delete").click()

View File

@ -3,7 +3,7 @@ context('Screen Tests', () => {
before(() => { before(() => {
cy.server() cy.server()
cy.visit('localhost:4001/_builder') cy.visit('localhost:4001/_builder')
cy.createApp('Conor Cy App', 'Model App Description') cy.createApp('Conor Cy App', 'Table App Description')
cy.navigateToFrontend() cy.navigateToFrontend()
}) })

View File

@ -64,7 +64,7 @@ Cypress.Commands.add("createTestTableWithData", () => {
}) })
Cypress.Commands.add("createTable", tableName => { Cypress.Commands.add("createTable", tableName => {
// Enter model name // Enter table name
cy.contains("Create New Table").click() cy.contains("Create New Table").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {
cy.get("input") cy.get("input")
@ -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.contains("Create New Row").click()
cy.get(".modal").within(() => { cy.get(".modal").within(() => {

View File

@ -6,7 +6,7 @@ import { cloneDeep, difference } from "lodash/fp"
* @property {string} componentInstanceId - an _id of a component that has been added to a screen, which you want to fetch bindable props for * @property {string} componentInstanceId - an _id of a component that has been added to a screen, which you want to fetch bindable props for
* @propperty {Object} screen - current screen - where componentInstanceId lives * @propperty {Object} screen - current screen - where componentInstanceId lives
* @property {Object} components - dictionary of component definitions * @property {Object} components - dictionary of component definitions
* @property {Array} models - array of all models * @property {Array} tables - array of all tables
*/ */
/** /**
@ -23,13 +23,13 @@ import { cloneDeep, difference } from "lodash/fp"
* @param {fetchBindablePropertiesParameter} param * @param {fetchBindablePropertiesParameter} param
* @returns {Array.<BindableProperty>} * @returns {Array.<BindableProperty>}
*/ */
export default function({ componentInstanceId, screen, components, models }) { export default function({ componentInstanceId, screen, components, tables }) {
const walkResult = walk({ const walkResult = walk({
// cloning so we are free to mutate props (e.g. by adding _contexts) // cloning so we are free to mutate props (e.g. by adding _contexts)
instance: cloneDeep(screen.props), instance: cloneDeep(screen.props),
targetId: componentInstanceId, targetId: componentInstanceId,
components, components,
models, tables,
}) })
return [ return [
@ -38,7 +38,7 @@ export default function({ componentInstanceId, screen, components, models }) {
.map(componentInstanceToBindable(walkResult)), .map(componentInstanceToBindable(walkResult)),
...(walkResult.target?._contexts ...(walkResult.target?._contexts
.map(contextToBindables(models, walkResult)) .map(contextToBindables(tables, walkResult))
.flat() ?? []), .flat() ?? []),
] ]
} }
@ -71,14 +71,14 @@ const componentInstanceToBindable = walkResult => i => {
} }
} }
const contextToBindables = (models, walkResult) => context => { const contextToBindables = (tables, walkResult) => context => {
const contextParentPath = getParentPath(walkResult, context) const contextParentPath = getParentPath(walkResult, context)
const modelId = context.model?.modelId ?? context.model const tableId = context.table?.tableId ?? context.table
const model = models.find(model => model._id === modelId) const table = tables.find(table => table._id === tableId)
let schema = let schema =
context.model?.type === "view" context.table?.type === "view"
? model?.views?.[context.model.name]?.schema ? table?.views?.[context.table.name]?.schema
: model?.schema : table?.schema
// Avoid crashing whenever no data source has been selected // Avoid crashing whenever no data source has been selected
if (!schema) { if (!schema) {
@ -98,9 +98,9 @@ const contextToBindables = (models, walkResult) => context => {
// how the binding expression persists, and is used in the app at runtime // how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${contextParentPath}data.${runtimeBoundKey}`, runtimeBinding: `${contextParentPath}data.${runtimeBoundKey}`,
// how the binding expressions looks to the user of the builder // how the binding expressions looks to the user of the builder
readableBinding: `${context.instance._instanceName}.${model.name}.${key}`, readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
// model / view info // table / view info
model: context.model, table: context.table,
} }
} }
@ -126,7 +126,7 @@ const getParentPath = (walkResult, context) => {
) )
} }
const walk = ({ instance, targetId, components, models, result }) => { const walk = ({ instance, targetId, components, tables, result }) => {
if (!result) { if (!result) {
result = { result = {
target: null, target: null,
@ -165,8 +165,8 @@ const walk = ({ instance, targetId, components, models, result }) => {
if (contextualInstance) { if (contextualInstance) {
// add to currentContexts (ancestory of context) // add to currentContexts (ancestory of context)
// before walking children // before walking children
const model = instance[component.context] const table = instance[component.context]
result.currentContexts.push({ instance, model }) result.currentContexts.push({ instance, table })
} }
const currentContexts = [...result.currentContexts] const currentContexts = [...result.currentContexts]
@ -175,7 +175,7 @@ const walk = ({ instance, targetId, components, models, result }) => {
// these have been deep cloned above, so shouln't modify the // these have been deep cloned above, so shouln't modify the
// original component instances // original component instances
child._contexts = currentContexts child._contexts = currentContexts
walk({ instance: child, targetId, components, models, result }) walk({ instance: child, targetId, components, tables, result })
} }
if (contextualInstance) { if (contextualInstance) {

View File

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

View File

@ -3,12 +3,12 @@ import { cloneDeep } from "lodash/fp"
import api from "../api" import api from "../api"
const INITIAL_BACKEND_UI_STATE = { const INITIAL_BACKEND_UI_STATE = {
models: [], tables: [],
views: [], views: [],
users: [], users: [],
selectedDatabase: {}, selectedDatabase: {},
selectedModel: {}, selectedTable: {},
draftModel: {}, draftTable: {},
} }
export const getBackendUiStore = () => { export const getBackendUiStore = () => {
@ -18,16 +18,16 @@ export const getBackendUiStore = () => {
reset: () => store.set({ ...INITIAL_BACKEND_UI_STATE }), reset: () => store.set({ ...INITIAL_BACKEND_UI_STATE }),
database: { database: {
select: async db => { select: async db => {
const modelsResponse = await api.get(`/api/models`) const tablesResponse = await api.get(`/api/tables`)
const models = await modelsResponse.json() const tables = await tablesResponse.json()
store.update(state => { store.update(state => {
state.selectedDatabase = db state.selectedDatabase = db
state.models = models state.tables = tables
return state return state
}) })
}, },
}, },
records: { rows: {
save: () => save: () =>
store.update(state => { store.update(state => {
state.selectedView = state.selectedView state.selectedView = state.selectedView
@ -38,56 +38,56 @@ export const getBackendUiStore = () => {
state.selectedView = state.selectedView state.selectedView = state.selectedView
return state return state
}), }),
select: record => select: row =>
store.update(state => { store.update(state => {
state.selectedRecord = record state.selectedRow = row
return state return state
}), }),
}, },
models: { tables: {
fetch: async () => { fetch: async () => {
const modelsResponse = await api.get(`/api/models`) const tablesResponse = await api.get(`/api/tables`)
const models = await modelsResponse.json() const tables = await tablesResponse.json()
store.update(state => { store.update(state => {
state.models = models state.tables = tables
return state return state
}) })
}, },
select: model => select: table =>
store.update(state => { store.update(state => {
state.selectedModel = model state.selectedTable = table
state.draftModel = cloneDeep(model) state.draftTable = cloneDeep(table)
state.selectedView = { name: `all_${model._id}` } state.selectedView = { name: `all_${table._id}` }
return state return state
}), }),
save: async model => { save: async table => {
const updatedModel = cloneDeep(model) const updatedTable = cloneDeep(table)
// update any renamed schema keys to reflect their names // update any renamed schema keys to reflect their names
for (let key in updatedModel.schema) { for (let key in updatedTable.schema) {
const field = updatedModel.schema[key] const field = updatedTable.schema[key]
// field has been renamed // field has been renamed
if (field.name && field.name !== key) { if (field.name && field.name !== key) {
updatedModel.schema[field.name] = field updatedTable.schema[field.name] = field
updatedModel._rename = { old: key, updated: field.name } updatedTable._rename = { old: key, updated: field.name }
delete updatedModel.schema[key] delete updatedTable.schema[key]
} }
} }
const SAVE_MODEL_URL = `/api/models` const SAVE_TABLE_URL = `/api/tables`
const response = await api.post(SAVE_MODEL_URL, updatedModel) const response = await api.post(SAVE_TABLE_URL, updatedTable)
const savedModel = await response.json() const savedTable = await response.json()
await store.actions.models.fetch() await store.actions.tables.fetch()
store.actions.models.select(savedModel) store.actions.tables.select(savedTable)
return savedModel return savedTable
}, },
delete: async model => { delete: async table => {
await api.delete(`/api/models/${model._id}/${model._rev}`) await api.delete(`/api/tables/${table._id}/${table._rev}`)
store.update(state => { store.update(state => {
state.models = state.models.filter( state.tables = state.tables.filter(
existing => existing._id !== model._id existing => existing._id !== table._id
) )
state.selectedModel = {} state.selectedTable = {}
return state return state
}) })
}, },
@ -95,23 +95,23 @@ export const getBackendUiStore = () => {
store.update(state => { store.update(state => {
// delete the original if renaming // delete the original if renaming
if (originalName) { if (originalName) {
delete state.draftModel.schema[originalName] delete state.draftTable.schema[originalName]
state.draftModel._rename = { state.draftTable._rename = {
old: originalName, old: originalName,
updated: field.name, updated: field.name,
} }
} }
state.draftModel.schema[field.name] = cloneDeep(field) state.draftTable.schema[field.name] = cloneDeep(field)
store.actions.models.save(state.draftModel) store.actions.tables.save(state.draftTable)
return state return state
}) })
}, },
deleteField: field => { deleteField: field => {
store.update(state => { store.update(state => {
delete state.draftModel.schema[field.name] delete state.draftTable.schema[field.name]
store.actions.models.save(state.draftModel) store.actions.tables.save(state.draftTable)
return state return state
}) })
}, },
@ -120,12 +120,12 @@ export const getBackendUiStore = () => {
select: view => select: view =>
store.update(state => { store.update(state => {
state.selectedView = view state.selectedView = view
state.selectedModel = {} state.selectedTable = {}
return state return state
}), }),
delete: async view => { delete: async view => {
await api.delete(`/api/views/${view}`) await api.delete(`/api/views/${view}`)
await store.actions.models.fetch() await store.actions.tables.fetch()
}, },
save: async view => { save: async view => {
const response = await api.post(`/api/views`, view) const response = await api.post(`/api/views`, view)
@ -137,14 +137,14 @@ export const getBackendUiStore = () => {
} }
store.update(state => { store.update(state => {
const viewModel = state.models.find( const viewTable = state.tables.find(
model => model._id === view.modelId table => table._id === view.tableId
) )
if (view.originalName) delete viewModel.views[view.originalName] if (view.originalName) delete viewTable.views[view.originalName]
viewModel.views[view.name] = viewMeta viewTable.views[view.name] = viewMeta
state.models = state.models state.tables = state.tables
state.selectedView = viewMeta state.selectedView = viewMeta
return state return state
}) })

View File

@ -15,7 +15,7 @@ const createScreen = () => ({
}, },
_children: [], _children: [],
_instanceName: "", _instanceName: "",
model: "", table: "",
}, },
route: "", route: "",
name: "screen-id", name: "screen-id",

View File

@ -15,7 +15,7 @@ const createScreen = () => ({
}, },
_children: [], _children: [],
_instanceName: "", _instanceName: "",
model: "", table: "",
}, },
route: "", route: "",
name: "screen-id", name: "screen-id",

View File

@ -1,19 +1,19 @@
import newRecordScreen from "./newRecordScreen" import newRowScreen from "./newRowScreen"
import recordDetailScreen from "./recordDetailScreen" import rowDetailScreen from "./rowDetailScreen"
import recordListScreen from "./recordListScreen" import rowListScreen from "./rowListScreen"
import emptyNewRecordScreen from "./emptyNewRecordScreen" import emptyNewRowScreen from "./emptyNewRowScreen"
import createFromScratchScreen from "./createFromScratchScreen" import createFromScratchScreen from "./createFromScratchScreen"
import emptyRecordDetailScreen from "./emptyRecordDetailScreen" import emptyRowDetailScreen from "./emptyRowDetailScreen"
import { generateNewIdsForComponent } from "../../storeUtils" import { generateNewIdsForComponent } from "../../storeUtils"
import { uuid } from "builderStore/uuid" import { uuid } from "builderStore/uuid"
const allTemplates = models => [ const allTemplates = tables => [
createFromScratchScreen, createFromScratchScreen,
...newRecordScreen(models), ...newRowScreen(tables),
...recordDetailScreen(models), ...rowDetailScreen(tables),
...recordListScreen(models), ...rowListScreen(tables),
emptyNewRecordScreen, emptyNewRowScreen,
emptyRecordDetailScreen, emptyRowDetailScreen,
] ]
// allows us to apply common behaviour to all create() functions // allows us to apply common behaviour to all create() functions
@ -28,8 +28,8 @@ const createTemplateOverride = (frontendState, create) => () => {
return screen return screen
} }
export default (frontendState, models) => export default (frontendState, tables) =>
allTemplates(models).map(template => ({ allTemplates(tables).map(template => ({
...template, ...template,
create: createTemplateOverride(frontendState, template.create), create: createTemplateOverride(frontendState, template.create),
})) }))

View File

@ -1,18 +1,18 @@
export default function(models) { export default function(tables) {
return models.map(model => { return tables.map(table => {
const fields = Object.keys(model.schema) const fields = Object.keys(table.schema)
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row" const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row"
return { return {
name: `${model.name} - New`, name: `${table.name} - New`,
create: () => createScreen(model, heading), create: () => createScreen(table, heading),
id: NEW_RECORD_TEMPLATE, id: NEW_ROW_TEMPLATE,
} }
}) })
} }
export const NEW_RECORD_TEMPLATE = "NEW_RECORD_TEMPLATE" export const NEW_ROW_TEMPLATE = "NEW_ROW_TEMPLATE"
const createScreen = (model, heading) => ({ const createScreen = (table, heading) => ({
props: { props: {
_id: "", _id: "",
_component: "@budibase/standard-components/newrow", _component: "@budibase/standard-components/newrow",
@ -22,7 +22,7 @@ const createScreen = (model, heading) => ({
active: {}, active: {},
selected: {}, selected: {},
}, },
model: model._id, table: table._id,
_children: [ _children: [
{ {
_id: "", _id: "",
@ -50,7 +50,7 @@ const createScreen = (model, heading) => ({
selected: {}, selected: {},
}, },
_code: "", _code: "",
_instanceName: `${model.name} Form`, _instanceName: `${table.name} Form`,
_children: [], _children: [],
}, },
{ {
@ -91,7 +91,7 @@ const createScreen = (model, heading) => ({
onClick: [ onClick: [
{ {
parameters: { parameters: {
url: `/${model.name.toLowerCase()}`, url: `/${table.name.toLowerCase()}`,
}, },
"##eventHandlerType": "Navigate To", "##eventHandlerType": "Navigate To",
}, },
@ -116,9 +116,9 @@ const createScreen = (model, heading) => ({
{ {
parameters: { parameters: {
contextPath: "data", contextPath: "data",
modelId: model._id, tableId: table._id,
}, },
"##eventHandlerType": "Save Record", "##eventHandlerType": "Save Row",
}, },
], ],
_instanceName: "Save Button", _instanceName: "Save Button",
@ -127,9 +127,9 @@ const createScreen = (model, heading) => ({
], ],
}, },
], ],
_instanceName: `${model.name} - New`, _instanceName: `${table.name} - New`,
_code: "", _code: "",
}, },
route: `/${model.name.toLowerCase()}/new`, route: `/${table.name.toLowerCase()}/new`,
name: "", name: "",
}) })

View File

@ -1,18 +1,18 @@
export default function(models) { export default function(tables) {
return models.map(model => { return tables.map(table => {
const fields = Object.keys(model.schema) const fields = Object.keys(table.schema)
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail" const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail"
return { return {
name: `${model.name} - Detail`, name: `${table.name} - Detail`,
create: () => createScreen(model, heading), create: () => createScreen(table, heading),
id: RECORD_DETAIL_TEMPLATE, id: ROW_DETAIL_TEMPLATE,
} }
}) })
} }
export const RECORD_DETAIL_TEMPLATE = "RECORD_DETAIL_TEMPLATE" export const ROW_DETAIL_TEMPLATE = "ROW_DETAIL_TEMPLATE"
const createScreen = (model, heading) => ({ const createScreen = (table, heading) => ({
props: { props: {
_id: "", _id: "",
_component: "@budibase/standard-components/rowdetail", _component: "@budibase/standard-components/rowdetail",
@ -22,7 +22,7 @@ const createScreen = (model, heading) => ({
active: {}, active: {},
selected: {}, selected: {},
}, },
model: model._id, table: table._id,
_children: [ _children: [
{ {
_id: "", _id: "",
@ -50,7 +50,7 @@ const createScreen = (model, heading) => ({
selected: {}, selected: {},
}, },
_code: "", _code: "",
_instanceName: `${model.name} Form`, _instanceName: `${table.name} Form`,
_children: [], _children: [],
}, },
{ {
@ -91,7 +91,7 @@ const createScreen = (model, heading) => ({
onClick: [ onClick: [
{ {
parameters: { parameters: {
url: `/${model.name.toLowerCase()}`, url: `/${table.name.toLowerCase()}`,
}, },
"##eventHandlerType": "Navigate To", "##eventHandlerType": "Navigate To",
}, },
@ -116,9 +116,9 @@ const createScreen = (model, heading) => ({
{ {
parameters: { parameters: {
contextPath: "data", contextPath: "data",
modelId: model._id, tableId: table._id,
}, },
"##eventHandlerType": "Save Record", "##eventHandlerType": "Save Row",
}, },
], ],
_instanceName: "Save Button", _instanceName: "Save Button",
@ -127,9 +127,9 @@ const createScreen = (model, heading) => ({
], ],
}, },
], ],
_instanceName: `${model.name} - Detail`, _instanceName: `${table.name} - Detail`,
_code: "", _code: "",
}, },
route: `/${model.name.toLowerCase()}/:id`, route: `/${table.name.toLowerCase()}/:id`,
name: "", name: "",
}) })

View File

@ -1,16 +1,16 @@
export default function(models) { export default function(tables) {
return models.map(model => { return tables.map(table => {
return { return {
name: `${model.name} - List`, name: `${table.name} - List`,
create: () => createScreen(model), create: () => createScreen(table),
id: RECORD_LIST_TEMPLATE, id: ROW_LIST_TEMPLATE,
} }
}) })
} }
export const RECORD_LIST_TEMPLATE = "RECORD_LIST_TEMPLATE" export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
const createScreen = model => ({ const createScreen = table => ({
props: { props: {
_id: "", _id: "",
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
@ -53,7 +53,7 @@ const createScreen = model => ({
}, },
_code: "", _code: "",
className: "", className: "",
text: `${model.name} List`, text: `${table.name} List`,
type: "h1", type: "h1",
_instanceName: "Heading 1", _instanceName: "Heading 1",
_children: [], _children: [],
@ -74,7 +74,7 @@ const createScreen = model => ({
onClick: [ onClick: [
{ {
parameters: { parameters: {
url: `/${model.name}/new`, url: `/${table.name}/new`,
}, },
"##eventHandlerType": "Navigate To", "##eventHandlerType": "Navigate To",
}, },
@ -96,19 +96,19 @@ const createScreen = model => ({
_code: "", _code: "",
datasource: { datasource: {
label: "Deals", label: "Deals",
name: `all_${model._id}`, name: `all_${table._id}`,
modelId: model._id, tableId: table._id,
isModel: true, isTable: true,
}, },
_instanceName: `${model.name} Table`, _instanceName: `${table.name} Table`,
_children: [], _children: [],
}, },
], ],
_instanceName: `${model.name} - List`, _instanceName: `${table.name} - List`,
_code: "", _code: "",
className: "", className: "",
onLoad: [], onLoad: [],
}, },
route: `/${model.name.toLowerCase()}`, route: `/${table.name.toLowerCase()}`,
name: "", name: "",
}) })

View File

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

View File

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

View File

@ -6,12 +6,12 @@
export let value export let value
export let bindings export let bindings
$: model = $backendUiStore.models.find(model => model._id === value?.modelId) $: table = $backendUiStore.tables.find(table => table._id === value?.tableId)
$: schemaFields = Object.entries(model?.schema ?? {}) $: schemaFields = Object.entries(table?.schema ?? {})
// Ensure any nullish modelId values get set to empty string so // Ensure any nullish tableId values get set to empty string so
// that the select works // that the select works
$: if (value?.modelId == null) value = { modelId: "" } $: if (value?.tableId == null) value = { tableId: "" }
function schemaHasOptions(schema) { function schemaHasOptions(schema) {
return !!schema.constraints?.inclusion?.length return !!schema.constraints?.inclusion?.length
@ -19,10 +19,10 @@
</script> </script>
<div class="block-field"> <div class="block-field">
<Select bind:value={value.modelId} thin secondary> <Select bind:value={value.tableId} thin secondary>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.models as model} {#each $backendUiStore.tables as table}
<option value={model._id}>{model.name}</option> <option value={table._id}>{table.name}</option>
{/each} {/each}
</Select> </Select>
</div> </div>

View File

@ -8,8 +8,8 @@
<div class="block-field"> <div class="block-field">
<Select bind:value secondary thin> <Select bind:value secondary thin>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each $backendUiStore.models as model} {#each $backendUiStore.tables as table}
<option value={model._id}>{model.name}</option> <option value={table._id}>{table.name}</option>
{/each} {/each}
</Select> </Select>
</div> </div>

View File

@ -10,19 +10,19 @@
let data = [] let data = []
let loading = false let loading = false
$: title = $backendUiStore.selectedModel.name $: title = $backendUiStore.selectedTable.name
$: schema = $backendUiStore.selectedModel.schema $: schema = $backendUiStore.selectedTable.schema
$: modelView = { $: tableView = {
schema, schema,
name: $backendUiStore.selectedView.name, name: $backendUiStore.selectedView.name,
} }
// Fetch records for specified model // Fetch rows for specified table
$: { $: {
if ($backendUiStore.selectedView?.name?.startsWith("all_")) { if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
loading = true loading = true
api.fetchDataForView($backendUiStore.selectedView).then(records => { api.fetchDataForView($backendUiStore.selectedView).then(rows => {
data = records || [] data = rows || []
loading = false loading = false
}) })
} }
@ -34,6 +34,6 @@
{#if Object.keys(schema).length > 0} {#if Object.keys(schema).length > 0}
<CreateRowButton /> <CreateRowButton />
<CreateViewButton /> <CreateViewButton />
<ExportButton view={modelView} /> <ExportButton view={tableView} />
{/if} {/if}
</Table> </Table>

View File

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

View File

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

View File

@ -10,7 +10,7 @@
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import AttachmentList from "./AttachmentList.svelte" import AttachmentList from "./AttachmentList.svelte"
import TablePagination from "./TablePagination.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 RowPopover from "./buttons/CreateRowButton.svelte"
import ColumnPopover from "./buttons/CreateColumnButton.svelte" import ColumnPopover from "./buttons/CreateColumnButton.svelte"
import ViewPopover from "./buttons/CreateViewButton.svelte" import ViewPopover from "./buttons/CreateViewButton.svelte"
@ -39,14 +39,14 @@
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
) )
: [] : []
$: modelId = data?.length ? data[0].modelId : null $: tableId = data?.length ? data[0].tableId : null
function selectRelationship(record, fieldName) { function selectRelationship(row, fieldName) {
if (!record?.[fieldName]?.length) { if (!row?.[fieldName]?.length) {
return return
} }
$goto( $goto(
`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}` `/${$params.application}/backend/table/${tableId}/relationship/${row._id}/${fieldName}`
) )
} }
</script> </script>

View File

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

View File

@ -6,22 +6,22 @@ export async function createUser(user) {
return await response.json() return await response.json()
} }
export async function saveRecord(record, modelId) { export async function saveRow(row, tableId) {
const SAVE_RECORDS_URL = `/api/${modelId}/records` const SAVE_ROWS_URL = `/api/${tableId}/rows`
const response = await api.post(SAVE_RECORDS_URL, record) const response = await api.post(SAVE_ROWS_URL, row)
return await response.json() return await response.json()
} }
export async function deleteRecord(record) { export async function deleteRow(row) {
const DELETE_RECORDS_URL = `/api/${record.modelId}/records/${record._id}/${record._rev}` const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}`
const response = await api.delete(DELETE_RECORDS_URL) const response = await api.delete(DELETE_ROWS_URL)
return response return response
} }
export async function fetchDataForView(view) { 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() return await response.json()
} }

View File

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

View File

@ -1,46 +0,0 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import RecordFieldControl from "../RecordFieldControl.svelte"
import * as api from "../api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let record = {}
let errors = []
$: creating = record?._id == null
$: model = record.modelId
? $backendUiStore.models.find(model => model._id === record?.modelId)
: $backendUiStore.selectedModel
$: modelSchema = Object.entries(model?.schema ?? {})
async function saveRecord() {
const recordResponse = await api.saveRecord(
{ ...record, modelId: model._id },
model._id
)
if (recordResponse.errors) {
errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
// Prevent modal closing if there were errors
return false
}
notifier.success("Record saved successfully.")
backendUiStore.actions.records.save(recordResponse)
}
</script>
<ModalContent
title={creating ? 'Create Row' : 'Edit Row'}
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRecord}>
<ErrorsBox {errors} />
{#each modelSchema as [key, meta]}
<div>
<RecordFieldControl {meta} bind:value={record[key]} />
</div>
{/each}
</ModalContent>

View File

@ -0,0 +1,46 @@
<script>
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import RowFieldControl from "../RowFieldControl.svelte"
import * as api from "../api"
import { ModalContent } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte"
export let row = {}
let errors = []
$: creating = row?._id == null
$: table = row.tableId
? $backendUiStore.tables.find(table => table._id === row?.tableId)
: $backendUiStore.selectedTable
$: tableSchema = Object.entries(table?.schema ?? {})
async function saveRow() {
const rowResponse = await api.saveRow(
{ ...row, tableId: table._id },
table._id
)
if (rowResponse.errors) {
errors = Object.keys(rowResponse.errors)
.map(k => ({ dataPath: k, message: rowResponse.errors[k] }))
.flat()
// Prevent modal closing if there were errors
return false
}
notifier.success("Row saved successfully.")
backendUiStore.actions.rows.save(rowResponse)
}
</script>
<ModalContent
title={creating ? 'Create Row' : 'Edit Row'}
confirmText={creating ? 'Create Row' : 'Save Row'}
onConfirm={saveRow}>
<ErrorsBox {errors} />
{#each tableSchema as [key, meta]}
<div>
<RowFieldControl {meta} bind:value={row[key]} />
</div>
{/each}
</ModalContent>

View File

@ -14,13 +14,13 @@
export let view = {} export let view = {}
export let onClosed export let onClosed
$: viewModel = $backendUiStore.models.find( $: viewTable = $backendUiStore.tables.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.tableId
) )
$: fields = $: fields =
viewModel && viewTable &&
Object.keys(viewModel.schema).filter( Object.keys(viewTable.schema).filter(
field => viewModel.schema[field].type === "number" field => viewTable.schema[field].type === "number"
) )
function saveView() { function saveView() {

View File

@ -32,10 +32,10 @@
} }
function deleteColumn() { function deleteColumn() {
if (field.name === $backendUiStore.selectedModel.primaryDisplay) { if (field.name === $backendUiStore.selectedTable.primaryDisplay) {
notifier.danger("You cannot delete the primary display column") notifier.danger("You cannot delete the primary display column")
} else { } else {
backendUiStore.actions.models.deleteField(field) backendUiStore.actions.tables.deleteField(field)
notifier.success("Column deleted") notifier.success("Column deleted")
} }
hideEditor() hideEditor()

View File

@ -19,7 +19,7 @@
import Checkbox from "components/common/Checkbox.svelte" import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.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" import * as api from "../api"
let fieldDefinitions = cloneDeep(FIELDS) let fieldDefinitions = cloneDeep(FIELDS)
@ -31,14 +31,14 @@
} }
let originalName = field.name let originalName = field.name
$: modelOptions = $backendUiStore.models.filter( $: tableOptions = $backendUiStore.tables.filter(
model => model._id !== $backendUiStore.draftModel._id table => table._id !== $backendUiStore.draftTable._id
) )
$: required = !!field?.constraints?.presence $: required = !!field?.constraints?.presence
async function saveColumn() { async function saveColumn() {
backendUiStore.update(state => { backendUiStore.update(state => {
backendUiStore.actions.models.saveField({ backendUiStore.actions.tables.saveField({
originalName, originalName,
field, field,
}) })
@ -113,10 +113,10 @@
label="Max Value" label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} /> bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'} {:else if field.type === 'link'}
<Select label="Table" thin secondary bind:value={field.modelId}> <Select label="Table" thin secondary bind:value={field.tableId}>
<option value="">Choose an option</option> <option value="">Choose an option</option>
{#each modelOptions as model} {#each tableOptions as table}
<option value={model._id}>{model.name}</option> <option value={table._id}>{table.name}</option>
{/each} {/each}
</Select> </Select>
<Input <Input

View File

@ -10,11 +10,11 @@
let name let name
let field let field
$: fields = Object.keys($backendUiStore.selectedModel.schema).filter(key => { $: fields = Object.keys($backendUiStore.selectedTable.schema).filter(key => {
return $backendUiStore.selectedModel.schema[key].type === "number" return $backendUiStore.selectedTable.schema[key].type === "number"
}) })
$: views = $backendUiStore.models.flatMap(model => $: views = $backendUiStore.tables.flatMap(table =>
Object.keys(model.views || {}) Object.keys(table.views || {})
) )
function saveView() { function saveView() {
@ -24,7 +24,7 @@
} }
backendUiStore.actions.views.save({ backendUiStore.actions.views.save({
name, name,
modelId: $backendUiStore.selectedModel._id, tableId: $backendUiStore.selectedTable._id,
field, field,
}) })
notifier.success(`View ${name} created`) notifier.success(`View ${name} created`)

View File

@ -45,10 +45,10 @@
export let view = {} export let view = {}
export let onClosed export let onClosed
$: viewModel = $backendUiStore.models.find( $: viewTable = $backendUiStore.tables.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.tableId
) )
$: fields = viewModel && Object.keys(viewModel.schema) $: fields = viewTable && Object.keys(viewTable.schema)
function saveView() { function saveView() {
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)
@ -71,25 +71,25 @@
function isMultipleChoice(field) { function isMultipleChoice(field) {
return ( return (
(viewModel.schema[field].constraints && (viewTable.schema[field].constraints &&
viewModel.schema[field].constraints.inclusion && viewTable.schema[field].constraints.inclusion &&
viewModel.schema[field].constraints.inclusion.length) || viewTable.schema[field].constraints.inclusion.length) ||
viewModel.schema[field].type === "boolean" viewTable.schema[field].type === "boolean"
) )
} }
function fieldOptions(field) { function fieldOptions(field) {
return viewModel.schema[field].type === "options" return viewTable.schema[field].type === "options"
? viewModel.schema[field].constraints.inclusion ? viewTable.schema[field].constraints.inclusion
: [true, false] : [true, false]
} }
function isDate(field) { function isDate(field) {
return viewModel.schema[field].type === "datetime" return viewTable.schema[field].type === "datetime"
} }
function isNumber(field) { function isNumber(field) {
return viewModel.schema[field].type === "number" return viewTable.schema[field].type === "number"
} }
const fieldChanged = filter => ev => { const fieldChanged = filter => ev => {
@ -97,8 +97,8 @@
if ( if (
filter.key && filter.key &&
ev.target.value && ev.target.value &&
viewModel.schema[filter.key].type !== viewTable.schema[filter.key].type !==
viewModel.schema[ev.target.value].type viewTable.schema[ev.target.value].type
) { ) {
filter.value = "" filter.value = ""
} }

View File

@ -6,10 +6,10 @@
export let view = {} export let view = {}
export let onClosed export let onClosed
$: viewModel = $backendUiStore.models.find( $: viewTable = $backendUiStore.tables.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId ({ _id }) => _id === $backendUiStore.selectedView.tableId
) )
$: fields = viewModel && Object.keys(viewModel.schema) $: fields = viewTable && Object.keys(viewTable.schema)
function saveView() { function saveView() {
backendUiStore.actions.views.save(view) backendUiStore.actions.views.save(view)

View File

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

View File

@ -7,7 +7,7 @@
</script> </script>
<div <div
data-cy="model-nav-item" data-cy="table-nav-item"
class:indented class:indented
class:selected class:selected
on:click on:click

View File

@ -21,28 +21,28 @@
!schema || Object.keys(schema).every(column => schema[column].success) !schema || Object.keys(schema).every(column => schema[column].success)
$: dataImport = { $: dataImport = {
valid, valid,
schema: buildModelSchema(schema), schema: buildTableSchema(schema),
path: files[0] && files[0].path, path: files[0] && files[0].path,
} }
function buildModelSchema(schema) { function buildTableSchema(schema) {
const modelSchema = {} const tableSchema = {}
for (let key in schema) { for (let key in schema) {
const type = schema[key].type const type = schema[key].type
if (type === "omit") continue if (type === "omit") continue
modelSchema[key] = { tableSchema[key] = {
name: key, name: key,
type, type,
constraints: FIELDS[type.toUpperCase()].constraints, constraints: FIELDS[type.toUpperCase()].constraints,
} }
} }
return modelSchema return tableSchema
} }
async function validateCSV() { async function validateCSV() {
const response = await api.post("/api/models/csv/validate", { const response = await api.post("/api/tables/csv/validate", {
file: files[0], file: files[0],
schema: schema || {}, schema: schema || {},
}) })

View File

@ -11,9 +11,9 @@
$: selectedView = $: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name $backendUiStore.selectedView && $backendUiStore.selectedView.name
function selectModel(model) { function selectTable(table) {
backendUiStore.actions.models.select(model) backendUiStore.actions.tables.select(table)
$goto(`./model/${model._id}`) $goto(`./table/${table._id}`)
} }
function selectView(view) { function selectView(view) {
@ -30,15 +30,15 @@
<Spacer medium /> <Spacer medium />
<CreateTableModal /> <CreateTableModal />
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each $backendUiStore.models as model} {#each $backendUiStore.tables as table}
<ListItem <ListItem
selected={selectedView === `all_${model._id}`} selected={selectedView === `all_${table._id}`}
title={model.name} title={table.name}
icon="ri-table-fill" icon="ri-table-fill"
on:click={() => selectModel(model)}> on:click={() => selectTable(table)}>
<EditTablePopover table={model} /> <EditTablePopover {table} />
</ListItem> </ListItem>
{#each Object.keys(model.views || {}) as viewName} {#each Object.keys(table.views || {}) as viewName}
<ListItem <ListItem
indented indented
selected={selectedView === viewName} selected={selectedView === viewName}
@ -46,10 +46,10 @@
icon="ri-eye-line" icon="ri-eye-line"
on:click={() => (selectedView === viewName ? {} : selectView({ on:click={() => (selectedView === viewName ? {} : selectView({
name: viewName, name: viewName,
...model.views[viewName], ...table.views[viewName],
}))}> }))}>
<EditViewPopover <EditViewPopover
view={{ name: viewName, ...model.views[viewName] }} /> view={{ name: viewName, ...table.views[viewName] }} />
</ListItem> </ListItem>
{/each} {/each}
{/each} {/each}

View File

@ -7,14 +7,14 @@
import TableDataImport from "../TableDataImport.svelte" import TableDataImport from "../TableDataImport.svelte"
import analytics from "analytics" import analytics from "analytics"
import screenTemplates from "builderStore/store/screenTemplates" import screenTemplates from "builderStore/store/screenTemplates"
import { NEW_RECORD_TEMPLATE } from "builderStore/store/screenTemplates/newRecordScreen" import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
import { RECORD_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/recordDetailScreen" import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
import { RECORD_LIST_TEMPLATE } from "builderStore/store/screenTemplates/recordListScreen" import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
const defaultScreens = [ const defaultScreens = [
NEW_RECORD_TEMPLATE, NEW_ROW_TEMPLATE,
RECORD_DETAIL_TEMPLATE, ROW_DETAIL_TEMPLATE,
RECORD_LIST_TEMPLATE, ROW_LIST_TEMPLATE,
] ]
let modal let modal
@ -38,16 +38,16 @@
} }
async function saveTable() { async function saveTable() {
const model = await backendUiStore.actions.models.save({ const table = await backendUiStore.actions.tables.save({
name, name,
schema: dataImport.schema || {}, schema: dataImport.schema || {},
dataImport, dataImport,
}) })
notifier.success(`Table ${name} created successfully.`) notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`) $goto(`./table/${table._id}`)
analytics.captureEvent("Table Created", { name }) analytics.captureEvent("Table Created", { name })
const screens = screenTemplates($store, [model]) const screens = screenTemplates($store, [table])
.filter(template => defaultScreens.includes(template.id)) .filter(template => defaultScreens.includes(template.id))
.map(template => template.create()) .map(template => template.create())
@ -57,9 +57,9 @@
} catch (_) { } catch (_) {
// TODO: this is temporary // TODO: this is temporary
// a cypress test is failing, because I added the // a cypress test is failing, because I added the
// NewRecord component. So - this throws an exception // NewRow component. So - this throws an exception
// because the currently released standard-components (on NPM) // because the currently released standard-components (on NPM)
// does not have NewRecord // does not have NewRow
// we should remove this after this has been released // we should remove this after this has been released
} }
} }

View File

@ -31,13 +31,13 @@
} }
async function deleteTable() { async function deleteTable() {
await backendUiStore.actions.models.delete(table) await backendUiStore.actions.tables.delete(table)
notifier.success("Table deleted") notifier.success("Table deleted")
hideEditor() hideEditor()
} }
async function save() { async function save() {
await backendUiStore.actions.models.save(table) await backendUiStore.actions.tables.save(table)
notifier.success("Table renamed successfully") notifier.success("Table renamed successfully")
hideEditor() hideEditor()
} }

View File

@ -39,10 +39,10 @@
async function deleteView() { async function deleteView() {
const name = view.name const name = view.name
const id = view.modelId const id = view.tableId
await backendUiStore.actions.views.delete(name) await backendUiStore.actions.views.delete(name)
notifier.success("View deleted") notifier.success("View deleted")
$goto(`./model/${id}`) $goto(`./table/${id}`)
} }
</script> </script>

View File

@ -1,53 +0,0 @@
<script>
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers"
export let schema
export let linkedRecords = []
let records = []
$: label = capitalise(schema.name)
$: linkedModelId = schema.modelId
$: linkedModel = $backendUiStore.models.find(
model => model._id === linkedModelId
)
$: fetchRecords(linkedModelId)
async function fetchRecords(linkedModelId) {
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
try {
const response = await api.get(FETCH_RECORDS_URL)
records = await response.json()
} catch (error) {
console.log(error)
records = []
}
}
function getPrettyName(record) {
return record[linkedModel.primaryDisplay || "_id"]
}
</script>
{#if linkedModel.primaryDisplay == null}
<Label extraSmall grey>{label}</Label>
<Label small black>
Please choose a primary display column for the
<b>{linkedModel.name}</b>
table.
</Label>
{:else}
<Multiselect
secondary
bind:value={linkedRecords}
{label}
placeholder="Choose some options">
{#each records as record}
<option value={record._id}>{getPrettyName(record)}</option>
{/each}
</Multiselect>
{/if}

View File

@ -0,0 +1,53 @@
<script>
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Select, Label, Multiselect } from "@budibase/bbui"
import { capitalise } from "../../helpers"
export let schema
export let linkedRows = []
let rows = []
$: label = capitalise(schema.name)
$: linkedTableId = schema.tableId
$: linkedTable = $backendUiStore.tables.find(
table => table._id === linkedTableId
)
$: fetchRows(linkedTableId)
async function fetchRows(linkedTableId) {
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
try {
const response = await api.get(FETCH_ROWS_URL)
rows = await response.json()
} catch (error) {
console.log(error)
rows = []
}
}
function getPrettyName(row) {
return row[linkedTable.primaryDisplay || "_id"]
}
</script>
{#if linkedTable.primaryDisplay == null}
<Label extraSmall grey>{label}</Label>
<Label small black>
Please choose a primary display column for the
<b>{linkedTable.name}</b>
table.
</Label>
{:else}
<Multiselect
secondary
bind:value={linkedRows}
{label}
placeholder="Choose some options">
{#each rows as row}
<option value={row._id}>{getPrettyName(row)}</option>
{/each}
</Multiselect>
{/if}

View File

@ -10,23 +10,23 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
// just wraps binding in {{ ... }} // just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}` const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
const modelFields = modelId => { const tableFields = tableId => {
const model = $backendUiStore.models.find(m => m._id === modelId) const table = $backendUiStore.tables.find(m => m._id === tableId)
return Object.keys(model.schema).map(k => ({ return Object.keys(table.schema).map(k => ({
name: k, name: k,
type: model.schema[k].type, type: table.schema[k].type,
})) }))
} }
$: schemaFields = $: schemaFields =
parameters && parameters.modelId ? modelFields(parameters.modelId) : [] parameters && parameters.tableId ? tableFields(parameters.tableId) : []
const onFieldsChanged = e => { const onFieldsChanged = e => {
parameters.fields = e.detail parameters.fields = e.detail
@ -35,14 +35,14 @@
<div class="root"> <div class="root">
<Label size="m" color="dark">Table</Label> <Label size="m" color="dark">Table</Label>
<Select secondary bind:value={parameters.modelId}> <Select secondary bind:value={parameters.tableId}>
<option value="" /> <option value="" />
{#each $backendUiStore.models as model} {#each $backendUiStore.tables as table}
<option value={model._id}>{model.name}</option> <option value={table._id}>{table.name}</option>
{/each} {/each}
</Select> </Select>
{#if parameters.modelId} {#if parameters.tableId}
<SaveFields <SaveFields
parameterFields={parameters.fields} parameterFields={parameters.fields}
{schemaFields} {schemaFields}

View File

@ -35,7 +35,7 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
const addField = () => { const addField = () => {

View File

@ -8,7 +8,7 @@
runtimeToReadableBinding, runtimeToReadableBinding,
} from "builderStore/replaceBindings" } from "builderStore/replaceBindings"
// parameters.contextPath used in the client handler to determine which record to save // parameters.contextPath used in the client handler to determine which row to save
// this could be "data" or "data.parent", "data.parent.parent" etc // this could be "data" or "data.parent", "data.parent.parent" etc
export let parameters export let parameters
@ -19,7 +19,7 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
$: { $: {
@ -65,18 +65,18 @@
const component = $store.components[instance._component] const component = $store.components[instance._component]
// component.context is the name of the prop that holds the modelId // component.context is the name of the prop that holds the tableId
const modelInfo = instance[component.context] const tableInfo = instance[component.context]
const modelId = const tableId =
typeof modelInfo === "string" ? modelInfo : modelInfo.modelId typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
if (!modelInfo) return [] if (!tableInfo) return []
const model = $backendUiStore.models.find(m => m._id === modelId) const table = $backendUiStore.tables.find(m => m._id === tableId)
parameters.modelId = modelId parameters.tableId = tableId
return Object.keys(model.schema).map(k => ({ return Object.keys(table.schema).map(k => ({
name: k, name: k,
type: model.schema[k].type, type: table.schema[k].type,
})) }))
} }
@ -88,8 +88,8 @@
<div class="root"> <div class="root">
{#if idFields.length === 0} {#if idFields.length === 0}
<div class="cannot-use"> <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
as a List a List
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Datasource</Label> <Label size="m" color="dark">Datasource</Label>

View File

@ -14,29 +14,29 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
let idFields let idFields
let recordId let rowId
$: { $: {
idFields = bindableProperties.filter( idFields = bindableProperties.filter(
bindable => bindable =>
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id") 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) { if (idFields.length > 0 && !parameters._id) {
recordId = idFields[0].runtimeBinding rowId = idFields[0].runtimeBinding
parameters = parameters parameters = parameters
} else if (!recordId && parameters._id) { } else if (!rowId && parameters._id) {
recordId = parameters._id rowId = parameters._id
.replace("{{", "") .replace("{{", "")
.replace("}}", "") .replace("}}", "")
.trim() .trim()
} }
} }
$: parameters._id = `{{ ${recordId} }}` $: parameters._id = `{{ ${rowId} }}`
// just wraps binding in {{ ... }} // just wraps binding in {{ ... }}
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}` const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
@ -44,11 +44,11 @@
// finds the selected idBinding, then reads the table/view // finds the selected idBinding, then reads the table/view
// from the component instance that it belongs to. // from the component instance that it belongs to.
// then returns the field names for that schema // then returns the field names for that schema
const schemaFromIdBinding = recordId => { const schemaFromIdBinding = rowId => {
if (!recordId) return [] if (!rowId) return []
const idBinding = bindableProperties.find( const idBinding = bindableProperties.find(
prop => prop.runtimeBinding === recordId prop => prop.runtimeBinding === rowId
) )
if (!idBinding) return [] if (!idBinding) return []
@ -56,23 +56,23 @@
const component = $store.components[instance._component] const component = $store.components[instance._component]
// component.context is the name of the prop that holds the modelId // component.context is the name of the prop that holds the tableId
const modelInfo = instance[component.context] const tableInfo = instance[component.context]
if (!modelInfo) return [] if (!tableInfo) return []
const model = $backendUiStore.models.find(m => m._id === modelInfo.modelId) const table = $backendUiStore.tables.find(m => m._id === tableInfo.tableId)
parameters.modelId = modelInfo.modelId parameters.tableId = tableInfo.tableId
return Object.keys(model.schema).map(k => ({ return Object.keys(table.schema).map(k => ({
name: k, name: k,
type: model.schema[k].type, type: table.schema[k].type,
})) }))
} }
let schemaFields let schemaFields
$: { $: {
if (parameters && recordId) { if (parameters && rowId) {
schemaFields = schemaFromIdBinding(recordId) schemaFields = schemaFromIdBinding(rowId)
} else { } else {
schemaFields = [] schemaFields = []
} }
@ -86,12 +86,12 @@
<div class="root"> <div class="root">
{#if idFields.length === 0} {#if idFields.length === 0}
<div class="cannot-use"> <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
as a List a List
</div> </div>
{:else} {:else}
<Label size="m" color="dark">Record Id</Label> <Label size="m" color="dark">Row Id</Label>
<Select secondary bind:value={recordId}> <Select secondary bind:value={rowId}>
<option value="" /> <option value="" />
{#each idFields as idField} {#each idFields as idField}
<option value={idField.runtimeBinding}> <option value={idField.runtimeBinding}>
@ -101,7 +101,7 @@
</Select> </Select>
{/if} {/if}
{#if recordId} {#if rowId}
<SaveFields <SaveFields
parameterFields={parameters.fields} parameterFields={parameters.fields}
{schemaFields} {schemaFields}

View File

@ -1,5 +1,5 @@
import NavigateTo from "./NavigateTo.svelte" import NavigateTo from "./NavigateTo.svelte"
import SaveRecord from "./SaveRecord.svelte" import SaveRow from "./SaveRow.svelte"
// defines what actions are available, when adding a new one // defines what actions are available, when adding a new one
// the component is the setup panel for the action // the component is the setup panel for the action
@ -8,8 +8,8 @@ import SaveRecord from "./SaveRecord.svelte"
export default [ export default [
{ {
name: "Save Record", name: "Save Row",
component: SaveRecord, component: SaveRow,
}, },
{ {
name: "Navigate To", name: "Navigate To",

View File

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

View File

@ -14,7 +14,7 @@
let templateIndex let templateIndex
let draftScreen let draftScreen
$: templates = getTemplates($store, $backendUiStore.models) $: templates = getTemplates($store, $backendUiStore.tables)
$: route = !route && $store.screens.length === 0 ? "*" : route $: route = !route && $store.screens.length === 0 ? "*" : route

View File

@ -37,7 +37,7 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
} }

View File

@ -30,7 +30,7 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
const detailScreens = $store.screens.filter(screen => const detailScreens = $store.screens.filter(screen =>
@ -42,11 +42,11 @@
if ( if (
p.type === "context" && p.type === "context" &&
p.runtimeBinding.endsWith("._id") && p.runtimeBinding.endsWith("._id") &&
p.model p.table
) { ) {
const modelId = const tableId =
typeof p.model === "string" ? p.model : p.model.modelId typeof p.table === "string" ? p.table : p.table.tableId
return modelId === detailScreen.props.model return tableId === detailScreen.props.table
} }
return false return false
}) })

View File

@ -8,8 +8,8 @@
<div> <div>
<Select thin secondary wide on:change {value}> <Select thin secondary wide on:change {value}>
<option value="">Choose a table</option> <option value="">Choose a table</option>
{#each $backendUiStore.models as model} {#each $backendUiStore.tables as table}
<option value={model._id}>{model.name}</option> <option value={table._id}>{table.name}</option>
{/each} {/each}
</Select> </Select>
</div> </div>

View File

@ -7,20 +7,20 @@
export let value = "" export let value = ""
export let onChange = (val = {}) export let onChange = (val = {})
const models = $backendUiStore.models const tables = $backendUiStore.tables
let options = [] let options = []
$: model = componentInstance.datasource $: table = componentInstance.datasource
? models.find(m => m._id === componentInstance.datasource.modelId) ? tables.find(m => m._id === componentInstance.datasource.tableId)
: null : null
$: type = componentInstance.datasource.type $: type = componentInstance.datasource.type
$: if (model) { $: if (table) {
options = options =
type === "model" || type === "link" type === "table" || type === "link"
? Object.keys(model.schema) ? Object.keys(table.schema)
: Object.keys(model.views[componentInstance.datasource.name].schema) : Object.keys(table.views[componentInstance.datasource.name].schema)
} }
</script> </script>

View File

@ -14,14 +14,14 @@
dropdownRight.hide() dropdownRight.hide()
} }
$: models = $backendUiStore.models.map(m => ({ $: tables = $backendUiStore.tables.map(m => ({
label: m.name, label: m.name,
name: `all_${m._id}`, name: `all_${m._id}`,
modelId: m._id, tableId: m._id,
type: "model", type: "table",
})) }))
$: views = $backendUiStore.models.reduce((acc, cur) => { $: views = $backendUiStore.tables.reduce((acc, cur) => {
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({ let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
label: key, label: key,
name: key, name: key,
@ -35,7 +35,7 @@
componentInstanceId: $store.currentComponentInfo._id, componentInstanceId: $store.currentComponentInfo._id,
components: $store.components, components: $store.components,
screen: $store.currentPreviewItem, screen: $store.currentPreviewItem,
models: $backendUiStore.models, tables: $backendUiStore.tables,
}) })
$: links = bindableProperties $: links = bindableProperties
@ -43,8 +43,8 @@
.map(property => ({ .map(property => ({
label: property.readableBinding, label: property.readableBinding,
fieldName: property.fieldSchema.name, fieldName: property.fieldSchema.name,
name: `all_${property.fieldSchema.modelId}`, name: `all_${property.fieldSchema.tableId}`,
modelId: property.fieldSchema.modelId, tableId: property.fieldSchema.tableId,
type: "link", type: "link",
})) }))
</script> </script>
@ -53,7 +53,7 @@
class="dropdownbutton" class="dropdownbutton"
bind:this={anchorRight} bind:this={anchorRight}
on:click={dropdownRight.show}> on:click={dropdownRight.show}>
<span>{value.label ? value.label : 'Model / View'}</span> <span>{value.label ? value.label : 'Table / View'}</span>
<Icon name="arrowdown" /> <Icon name="arrowdown" />
</div> </div>
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}> <DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
@ -62,11 +62,11 @@
<Heading extraSmall>Tables</Heading> <Heading extraSmall>Tables</Heading>
</div> </div>
<ul> <ul>
{#each models as model} {#each tables as table}
<li <li
class:selected={value === model} class:selected={value === table}
on:click={() => handleSelected(model)}> on:click={() => handleSelected(table)}>
{model.label} {table.label}
</li> </li>
{/each} {/each}
</ul> </ul>

View File

@ -19,7 +19,7 @@ export const TYPE_MAP = {
"##bbstate": "", "##bbstate": "",
}, },
}, },
models: { tables: {
default: {}, default: {},
}, },
} }

View File

@ -1,9 +1,9 @@
import Input from "./PropertyPanelControls/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import OptionSelect from "./OptionSelect.svelte" import OptionSelect from "./OptionSelect.svelte"
import Checkbox from "../common/Checkbox.svelte" import Checkbox from "../common/Checkbox.svelte"
import ModelSelect from "components/userInterface/ModelSelect.svelte" import TableSelect from "components/userInterface/TableSelect.svelte"
import ModelViewSelect from "components/userInterface/ModelViewSelect.svelte" import TableViewSelect from "components/userInterface/TableViewSelect.svelte"
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte" import TableViewFieldSelect from "components/userInterface/TableViewFieldSelect.svelte"
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte" import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
import ScreenSelect from "components/userInterface/ScreenSelect.svelte" import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
import { IconSelect } from "components/userInterface/IconSelect" import { IconSelect } from "components/userInterface/IconSelect"
@ -299,7 +299,7 @@ export default {
{ {
name: "List", name: "List",
_component: "@budibase/standard-components/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", icon: "ri-file-list-line",
properties: { properties: {
design: { ...all }, design: { ...all },
@ -307,7 +307,7 @@ export default {
{ {
label: "Data", label: "Data",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
], ],
}, },
@ -325,7 +325,7 @@ export default {
{ {
label: "Source", label: "Source",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
{ {
label: "Editable", label: "Editable",
@ -589,7 +589,7 @@ export default {
{ {
label: "Data", label: "Data",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
{ {
label: "Stripe Color", label: "Stripe Color",
@ -615,7 +615,7 @@ export default {
control: Colorpicker, control: Colorpicker,
defaultValue: "#FFFFFF", defaultValue: "#FFFFFF",
}, },
{ label: "Table", key: "model", control: ModelSelect }, { label: "Table", key: "table", control: TableSelect },
], ],
}, },
children: [], children: [],
@ -661,19 +661,19 @@ export default {
{ {
label: "Data", label: "Data",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
{ {
label: "Name Field", label: "Name Field",
key: "nameKey", key: "nameKey",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Value Field", label: "Value Field",
key: "valueKey", key: "valueKey",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Animate Chart", label: "Animate Chart",
@ -755,19 +755,19 @@ export default {
{ {
label: "Data", label: "Data",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
{ {
label: "Name Label", label: "Name Label",
key: "nameLabel", key: "nameLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Value Label", label: "Value Label",
key: "valueLabel", key: "valueLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Y Axis Label", label: "Y Axis Label",
@ -869,25 +869,25 @@ export default {
{ {
label: "Data", label: "Data",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
{ {
label: "Name Label", label: "Name Label",
key: "nameLabel", key: "nameLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Group Label", label: "Group Label",
key: "groupLabel", key: "groupLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Value Label", label: "Value Label",
key: "valueLabel", key: "valueLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Color", label: "Color",
@ -972,25 +972,25 @@ export default {
{ {
label: "Data", label: "Data",
key: "datasource", key: "datasource",
control: ModelViewSelect, control: TableViewSelect,
}, },
{ {
label: "Value Label", label: "Value Label",
key: "valueLabel", key: "valueLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Topic Label", label: "Topic Label",
key: "topicLabel", key: "topicLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Date Label", label: "Date Label",
key: "dateLabel", key: "dateLabel",
dependsOn: "datasource", dependsOn: "datasource",
control: ModelViewFieldSelect, control: TableViewFieldSelect,
}, },
{ {
label: "Colors", label: "Colors",
@ -1137,7 +1137,7 @@ export default {
// icon: "ri-file-list-line", // icon: "ri-file-list-line",
// properties: { // properties: {
// design: { ...all }, // design: { ...all },
// settings: [{ label: "Table", key: "model", control: ModelSelect }], // settings: [{ label: "Table", key: "table", control: TableSelect }],
// }, // },
// children: [], // children: [],
// }, // },
@ -1145,11 +1145,11 @@ export default {
name: "Row Detail", name: "Row Detail",
_component: "@budibase/standard-components/rowdetail", _component: "@budibase/standard-components/rowdetail",
description: 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", icon: "ri-profile-line",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }], settings: [{ label: "Table", key: "table", control: TableSelect }],
}, },
children: [], children: [],
}, },
@ -1157,11 +1157,11 @@ export default {
name: "New Row", name: "New Row",
_component: "@budibase/standard-components/newrow", _component: "@budibase/standard-components/newrow",
description: description:
"Sets up a new record for creation, which can be used with {{ context }}, in children", "Sets up a new row for creation, which can be used with {{ context }}, in children",
icon: "ri-profile-line", icon: "ri-profile-line",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }], settings: [{ label: "Table", key: "table", control: TableSelect }],
}, },
children: [], children: [],
}, },

View File

@ -1,13 +1,13 @@
<script> <script>
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import * as api from "components/backend/DataTable/api" import * as api from "components/backend/DataTable/api"
import ModelNavigator from "components/backend/ModelNavigator/ModelNavigator.svelte" import TableNavigator from "components/backend/TableNavigator/TableNavigator.svelte"
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=1 -->
<div class="root"> <div class="root">
<div class="nav"> <div class="nav">
<ModelNavigator /> <TableNavigator />
</div> </div>
<div class="content"> <div class="content">
<slot /> <slot />

View File

@ -1,6 +1,6 @@
<script> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
$goto("../model") $goto("../table")
</script> </script>
<!-- routify:options index=false --> <!-- routify:options index=false -->

View File

@ -1,15 +0,0 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
if ($params.selectedModel) {
const model = $backendUiStore.models.find(
m => m._id === $params.selectedModel
)
if (model) {
backendUiStore.actions.models.select(model)
}
}
</script>
<slot />

View File

@ -1,32 +0,0 @@
<script>
import { backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
async function selectModel(model) {
backendUiStore.actions.models.select(model)
}
onMount(async () => {
// navigate to first model in list, if not already selected
// and this is the final url (i.e. no selectedModel)
if (
!$leftover &&
$backendUiStore.models.length > 0 &&
(!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)
) {
$goto(`./${$backendUiStore.models[0]._id}`)
}
})
</script>
<div class="root">
<slot />
</div>
<style>
.root {
height: 100%;
position: relative;
}
</style>

View File

@ -1,35 +0,0 @@
<script>
import { store, backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
async function selectModel(model) {
backendUiStore.actions.models.select(model)
}
onMount(async () => {
// navigate to first model in list, if not already selected
// and this is the final url (i.e. no selectedModel)
if (
!$leftover &&
$backendUiStore.models.length > 0 &&
(!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)
) {
// this file routes as .../models/index, so, go up one.
$goto(`../${$backendUiStore.models[0]._id}`)
}
})
</script>
{#if $backendUiStore.models.length === 0}
<i>Create your first table to start building</i>
{:else}
<i>Select a table to edit</i>
{/if}
<style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
}
</style>

View File

@ -0,0 +1,15 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
if ($params.selectedTable) {
const table = $backendUiStore.tables.find(
m => m._id === $params.selectedTable
)
if (table) {
backendUiStore.actions.tables.select(table)
}
}
</script>
<slot />

View File

@ -1,12 +1,12 @@
<script> <script>
import ModelDataTable from "components/backend/DataTable/ModelDataTable.svelte" import TableDataTable from "components/backend/DataTable/DataTable.svelte"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
$: selectedModel = $backendUiStore.selectedModel $: selectedTable = $backendUiStore.selectedTable
</script> </script>
{#if $backendUiStore.selectedDatabase._id && selectedModel.name} {#if $backendUiStore.selectedDatabase._id && selectedTable.name}
<ModelDataTable /> <TableDataTable />
{:else} {:else}
<i>Create your first table to start building</i> <i>Create your first table to start building</i>
{/if} {/if}

View File

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

View File

@ -0,0 +1,32 @@
<script>
import { backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
async function selectTable(table) {
backendUiStore.actions.tables.select(table)
}
onMount(async () => {
// navigate to first table in list, if not already selected
// and this is the final url (i.e. no selectedTable)
if (
!$leftover &&
$backendUiStore.tables.length > 0 &&
(!$backendUiStore.selectedTable || !$backendUiStore.selectedTable._id)
) {
$goto(`./${$backendUiStore.tables[0]._id}`)
}
})
</script>
<div class="root">
<slot />
</div>
<style>
.root {
height: 100%;
position: relative;
}
</style>

View File

@ -0,0 +1,35 @@
<script>
import { store, backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
async function selectTable(table) {
backendUiStore.actions.tables.select(table)
}
onMount(async () => {
// navigate to first table in list, if not already selected
// and this is the final url (i.e. no selectedTable)
if (
!$leftover &&
$backendUiStore.tables.length > 0 &&
(!$backendUiStore.selectedTable || !$backendUiStore.selectedTable._id)
) {
// this file routes as .../tables/index, so, go up one.
$goto(`../${$backendUiStore.tables[0]._id}`)
}
})
</script>
{#if $backendUiStore.tables.length === 0}
<i>Create your first table to start building</i>
{:else}
<i>Select a table to edit</i>
{/if}
<style>
i {
font-size: var(--font-size-xl);
color: var(--grey-4);
}
</style>

View File

@ -5,9 +5,9 @@
if ($params.selectedView) { if ($params.selectedView) {
let view let view
const viewName = decodeURI($params.selectedView) const viewName = decodeURI($params.selectedView)
for (let model of $backendUiStore.models) { for (let table of $backendUiStore.tables) {
if (model.views && model.views[viewName]) { if (table.views && table.views[viewName]) {
view = model.views[viewName] view = table.views[viewName]
} }
} }
if (view) { if (view) {

View File

@ -22,7 +22,7 @@ describe("fetch bindable properties", () => {
expect(componentBinding).not.toBeDefined() expect(componentBinding).not.toBeDefined()
}) })
it("should return model schema, when inside a context", () => { it("should return table schema, when inside a context", () => {
const result = fetchBindableProperties({ const result = fetchBindableProperties({
componentInstanceId: "list-item-input-id", componentInstanceId: "list-item-input-id",
...testData() ...testData()
@ -33,41 +33,41 @@ describe("fetch bindable properties", () => {
const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name") const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name")
expect(namebinding).toBeDefined() expect(namebinding).toBeDefined()
expect(namebinding.readableBinding).toBe("list-name.Test Model.name") expect(namebinding.readableBinding).toBe("list-name.Test Table.name")
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description") const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "data.description")
expect(descriptionbinding).toBeDefined() expect(descriptionbinding).toBeDefined()
expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description") expect(descriptionbinding.readableBinding).toBe("list-name.Test Table.description")
const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id") const idbinding = contextBindings.find(b => b.runtimeBinding === "data._id")
expect(idbinding).toBeDefined() expect(idbinding).toBeDefined()
expect(idbinding.readableBinding).toBe("list-name.Test Model._id") expect(idbinding.readableBinding).toBe("list-name.Test Table._id")
}) })
it("should return model schema, for grantparent context", () => { it("should return table schema, for grantparent context", () => {
const result = fetchBindableProperties({ const result = fetchBindableProperties({
componentInstanceId: "child-list-item-input-id", componentInstanceId: "child-list-item-input-id",
...testData() ...testData()
}) })
const contextBindings = result.filter(r => r.type==="context") const contextBindings = result.filter(r => r.type==="context")
// 2 fields + _id + _rev ... x 2 models // 2 fields + _id + _rev ... x 2 tables
expect(contextBindings.length).toBe(8) expect(contextBindings.length).toBe(8)
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name") const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name")
expect(namebinding_parent).toBeDefined() expect(namebinding_parent).toBeDefined()
expect(namebinding_parent.readableBinding).toBe("list-name.Test Model.name") expect(namebinding_parent.readableBinding).toBe("list-name.Test Table.name")
const descriptionbinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.description") const descriptionbinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.description")
expect(descriptionbinding_parent).toBeDefined() expect(descriptionbinding_parent).toBeDefined()
expect(descriptionbinding_parent.readableBinding).toBe("list-name.Test Model.description") expect(descriptionbinding_parent.readableBinding).toBe("list-name.Test Table.description")
const namebinding_own = contextBindings.find(b => b.runtimeBinding === "data.name") const namebinding_own = contextBindings.find(b => b.runtimeBinding === "data.name")
expect(namebinding_own).toBeDefined() expect(namebinding_own).toBeDefined()
expect(namebinding_own.readableBinding).toBe("child-list-name.Test Model.name") expect(namebinding_own.readableBinding).toBe("child-list-name.Test Table.name")
const descriptionbinding_own = contextBindings.find(b => b.runtimeBinding === "data.description") const descriptionbinding_own = contextBindings.find(b => b.runtimeBinding === "data.description")
expect(descriptionbinding_own).toBeDefined() expect(descriptionbinding_own).toBeDefined()
expect(descriptionbinding_own.readableBinding).toBe("child-list-name.Test Model.description") expect(descriptionbinding_own.readableBinding).toBe("child-list-name.Test Table.description")
}) })
it("should return bindable component props, from components in same context", () => { it("should return bindable component props, from components in same context", () => {
@ -126,7 +126,7 @@ const testData = () => {
_id: "list-id", _id: "list-id",
_component: "@budibase/standard-components/list", _component: "@budibase/standard-components/list",
_instanceName: "list-name", _instanceName: "list-name",
model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id" }, table: { isTable: true, tableId: "test-table-id", label: "Test Table", name: "all_test-table-id" },
_children: [ _children: [
{ {
_id: "list-item-heading-id", _id: "list-item-heading-id",
@ -144,7 +144,7 @@ const testData = () => {
_id: "child-list-id", _id: "child-list-id",
_component: "@budibase/standard-components/list", _component: "@budibase/standard-components/list",
_instanceName: "child-list-name", _instanceName: "child-list-name",
model: { isModel: true, modelId: "test-model-id", label: "Test Model", name: "all_test-model-id"}, table: { isTable: true, tableId: "test-table-id", label: "Test Table", name: "all_test-table-id"},
_children: [ _children: [
{ {
_id: "child-list-item-heading-id", _id: "child-list-item-heading-id",
@ -166,9 +166,9 @@ const testData = () => {
} }
} }
const models = [{ const tables = [{
_id: "test-model-id", _id: "test-table-id",
name: "Test Model", name: "Test Table",
schema: { schema: {
name: { name: {
type: "string" type: "string"
@ -184,9 +184,9 @@ const testData = () => {
props: {}, props: {},
}, },
"@budibase/standard-components/list" : { "@budibase/standard-components/list" : {
context: "model", context: "table",
props: { props: {
model: "string" table: "string"
}, },
}, },
"@budibase/standard-components/input" : { "@budibase/standard-components/input" : {
@ -202,6 +202,6 @@ const testData = () => {
}, },
} }
return { screen, models, components } return { screen, tables, components }
} }

View File

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

View File

@ -52,27 +52,27 @@ const apiOpts = {
delete: del, delete: del,
} }
const saveRecord = async (params, state) => const saveRow = async (params, state) =>
await post({ await post({
url: `/api/${params.modelId}/records`, url: `/api/${params.tableId}/rows`,
body: makeRecordRequestBody(params, state), body: makeRowRequestBody(params, state),
}) })
const updateRecord = async (params, state) => { const updateRow = async (params, state) => {
const record = makeRecordRequestBody(params, state) const row = makeRowRequestBody(params, state)
record._id = params._id row._id = params._id
await patch({ await patch({
url: `/api/${params.modelId}/records/${params._id}`, url: `/api/${params.tableId}/rows/${params._id}`,
body: record, body: row,
}) })
} }
const makeRecordRequestBody = (parameters, state) => { const makeRowRequestBody = (parameters, state) => {
// start with the record thats currently in context // start with the row thats currently in context
const body = { ...(state.data || {}) } const body = { ...(state.data || {}) }
// dont send the model // dont send the table
if (body._model) delete body._model if (body._table) delete body._table
// then override with supplied parameters // then override with supplied parameters
for (let fieldName in parameters.fields) { for (let fieldName in parameters.fields) {
@ -101,6 +101,6 @@ const makeRecordRequestBody = (parameters, state) => {
export default { export default {
authenticate: authenticate(apiOpts), authenticate: authenticate(apiOpts),
saveRecord, saveRow,
updateRecord, updateRow,
} }

View File

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

View File

@ -80,7 +80,7 @@
"request": "launch", "request": "launch",
"name": "Jest - Models", "name": "Jest - Models",
"program": "${workspaceFolder}/node_modules/.bin/jest", "program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["model.spec", "--runInBand"], "args": ["table.spec", "--runInBand"],
"console": "integratedTerminal", "console": "integratedTerminal",
"internalConsoleOptions": "neverOpen", "internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true, "disableOptimisticBPs": true,

View File

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

View File

@ -39,9 +39,9 @@ async function replicateCouch({ instanceId, clientId, credentials }) {
async function getCurrentInstanceQuota(instanceId) { async function getCurrentInstanceQuota(instanceId) {
const db = new PouchDB(instanceId) const db = new PouchDB(instanceId)
const records = await db.allDocs({ const rows = await db.allDocs({
startkey: DocumentTypes.RECORD + SEPARATOR, startkey: DocumentTypes.ROW + SEPARATOR,
endkey: DocumentTypes.RECORD + SEPARATOR + UNICODE_MAX, endkey: DocumentTypes.ROW + SEPARATOR + UNICODE_MAX,
}) })
const users = await db.allDocs({ const users = await db.allDocs({
@ -49,13 +49,13 @@ async function getCurrentInstanceQuota(instanceId) {
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX, endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX,
}) })
const existingRecords = records.rows.length const existingRows = rows.rows.length
const existingUsers = users.rows.length const existingUsers = users.rows.length
const designDoc = await db.get("_design/database") const designDoc = await db.get("_design/database")
return { return {
records: existingRecords, rows: existingRows,
users: existingUsers, users: existingUsers,
views: Object.keys(designDoc.views).length, views: Object.keys(designDoc.views).length,
} }

View File

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

View File

@ -1,146 +0,0 @@
const CouchDB = require("../../db")
const linkRecords = require("../../db/linkedRecords")
const csvParser = require("../../utilities/csvParser")
const {
getRecordParams,
getModelParams,
generateModelID,
generateRecordID,
} = require("../../db/utils")
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
const body = await db.allDocs(
getModelParams(null, {
include_docs: true,
})
)
ctx.body = body.rows.map(row => row.doc)
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.get(ctx.params.id)
}
exports.save = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const { dataImport, ...rest } = ctx.request.body
const modelToSave = {
type: "model",
_id: generateModelID(),
views: {},
...rest,
}
let renameDocs = []
// if the model obj had an _id then it will have been retrieved
const oldModel = ctx.preExisting
// rename record fields when table column is renamed
const { _rename } = modelToSave
if (_rename && modelToSave.schema[_rename.updated].type === "link") {
throw "Cannot rename a linked field."
} else if (_rename && modelToSave.primaryDisplay === _rename.old) {
throw "Cannot rename the primary display field."
} else if (_rename) {
const records = await db.allDocs(
getRecordParams(modelToSave._id, null, {
include_docs: true,
})
)
renameDocs = records.rows.map(({ doc }) => {
doc[_rename.updated] = doc[_rename.old]
delete doc[_rename.old]
return doc
})
delete modelToSave._rename
}
// update schema of non-statistics views when new columns are added
for (let view in modelToSave.views) {
const modelView = modelToSave.views[view]
if (!modelView) continue
if (modelView.schema.group || modelView.schema.field) continue
modelView.schema = modelToSave.schema
}
// update linked records
await linkRecords.updateLinks({
instanceId,
eventType: oldModel
? linkRecords.EventType.MODEL_UPDATED
: linkRecords.EventType.MODEL_SAVE,
model: modelToSave,
oldModel: oldModel,
})
// don't perform any updates until relationships have been
// checked by the updateLinks function
if (renameDocs.length !== 0) {
await db.bulkDocs(renameDocs)
}
const result = await db.post(modelToSave)
modelToSave._rev = result.rev
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
if (dataImport && dataImport.path) {
// Populate the table with records imported from CSV in a bulk update
const data = await csvParser.transform(dataImport)
for (let row of data) {
row._id = generateRecordID(modelToSave._id)
row.modelId = modelToSave._id
}
await db.bulkDocs(data)
}
ctx.status = 200
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
ctx.body = modelToSave
}
exports.destroy = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const modelToDelete = await db.get(ctx.params.modelId)
// Delete all records for that model
const records = await db.allDocs(
getRecordParams(ctx.params.modelId, null, {
include_docs: true,
})
)
await db.bulkDocs(
records.rows.map(record => ({ ...record.doc, _deleted: true }))
)
// update linked records
await linkRecords.updateLinks({
instanceId,
eventType: linkRecords.EventType.MODEL_DELETE,
model: modelToDelete,
})
// don't remove the table itself until very end
await db.remove(modelToDelete)
ctx.eventEmitter &&
ctx.eventEmitter.emitModel(`model:delete`, instanceId, modelToDelete)
ctx.status = 200
ctx.message = `Model ${ctx.params.modelId} deleted.`
}
exports.validateCSVSchema = async function(ctx) {
const { file, schema = {} } = ctx.request.body
const result = await csvParser.parse(file.path, schema)
ctx.body = {
schema: result,
path: file.path,
}
}

View File

@ -1,382 +0,0 @@
const CouchDB = require("../../db")
const validateJs = require("validate.js")
const linkRecords = require("../../db/linkedRecords")
const {
getRecordParams,
generateRecordID,
DocumentTypes,
SEPARATOR,
} = require("../../db/utils")
const { cloneDeep } = require("lodash")
const MODEL_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.MODEL}${SEPARATOR}`
validateJs.extend(validateJs.validators.datetime, {
parse: function(value) {
return new Date(value).getTime()
},
// Input is a unix timestamp
format: function(value) {
return new Date(value).toISOString()
},
})
exports.patch = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
let record = await db.get(ctx.params.id)
const model = await db.get(record.modelId)
const patchfields = ctx.request.body
record = coerceRecordValues(record, model)
for (let key of Object.keys(patchfields)) {
if (!model.schema[key]) continue
record[key] = patchfields[key]
}
const validateResult = await validate({
record,
model,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
// returned record is cleaned and prepared for writing to DB
record = await linkRecords.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_UPDATE,
record,
modelId: record.modelId,
model,
})
const response = await db.put(record)
record._rev = response.rev
record.type = "record"
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:update`, instanceId, record, model)
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} updated successfully.`
}
exports.save = async function(ctx) {
if (ctx.request.body.type === "delete") {
await bulkDelete(ctx)
} else {
await saveRecord(ctx)
}
}
exports.fetchView = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const { stats, group, field } = ctx.query
const viewName = ctx.params.viewName
// if this is a model view being looked for just transfer to that
if (viewName.indexOf(MODEL_VIEW_BEGINS_WITH) === 0) {
ctx.params.modelId = viewName.substring(4)
await exports.fetchModelRecords(ctx)
return
}
const response = await db.query(`database/${viewName}`, {
include_docs: !stats,
group,
})
if (stats) {
response.rows = response.rows.map(row => ({
group: row.key,
field,
...row.value,
avg: row.value.sum / row.value.count,
}))
} else {
response.rows = response.rows.map(row => row.doc)
}
ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows)
}
exports.fetchModelRecords = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const response = await db.allDocs(
getRecordParams(ctx.params.modelId, null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
ctx.body = await linkRecords.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
}
exports.search = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const response = await db.allDocs({
include_docs: true,
...ctx.request.body,
})
ctx.body = await linkRecords.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
}
exports.find = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const record = await db.get(ctx.params.recordId)
if (record.modelId !== ctx.params.modelId) {
ctx.throw(400, "Supplied modelId does not match the records modelId")
return
}
ctx.body = await linkRecords.attachLinkInfo(instanceId, record)
}
exports.destroy = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const record = await db.get(ctx.params.recordId)
if (record.modelId !== ctx.params.modelId) {
ctx.throw(400, "Supplied modelId doesn't match the record's modelId")
return
}
await linkRecords.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_DELETE,
record,
modelId: record.modelId,
})
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.status = 200
// for automations include the record that was deleted
ctx.record = record
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record)
}
exports.validate = async function(ctx) {
const errors = await validate({
instanceId: ctx.user.instanceId,
modelId: ctx.params.modelId,
record: ctx.request.body,
})
ctx.status = 200
ctx.body = errors
}
async function validate({ instanceId, modelId, record, model }) {
if (!model) {
const db = new CouchDB(instanceId)
model = await db.get(modelId)
}
const errors = {}
for (let fieldName of Object.keys(model.schema)) {
const res = validateJs.single(
record[fieldName],
model.schema[fieldName].constraints
)
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}
exports.fetchEnrichedRecord = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const modelId = ctx.params.modelId
const recordId = ctx.params.recordId
if (instanceId == null || modelId == null || recordId == null) {
ctx.status = 400
ctx.body = {
status: 400,
error:
"Cannot handle request, URI params have not been successfully prepared.",
}
return
}
// need model to work out where links go in record
const [model, record] = await Promise.all([db.get(modelId), db.get(recordId)])
// get the link docs
const linkVals = await linkRecords.getLinkDocuments({
instanceId,
modelId,
recordId,
})
// look up the actual records based on the ids
const response = await db.allDocs({
include_docs: true,
keys: linkVals.map(linkVal => linkVal.id),
})
// need to include the IDs in these records for any links they may have
let linkedRecords = await linkRecords.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
// insert the link records in the correct place throughout the main record
for (let fieldName of Object.keys(model.schema)) {
let field = model.schema[fieldName]
if (field.type === "link") {
record[fieldName] = linkedRecords.filter(
linkRecord => linkRecord.modelId === field.modelId
)
}
}
ctx.body = record
ctx.status = 200
}
function coerceRecordValues(rec, model) {
const record = cloneDeep(rec)
for (let [key, value] of Object.entries(record)) {
const field = model.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return record
}
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}
async function bulkDelete(ctx) {
const instanceId = ctx.user.instanceId
const { records } = ctx.request.body
const db = new CouchDB(ctx.user.instanceId)
await db.bulkDocs(
records.map(
record => ({ ...record, _deleted: true }),
err => {
if (err) {
ctx.status = 500
} else {
records.forEach(record => {
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record)
})
ctx.status = 200
}
}
)
)
}
async function saveRecord(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
let record = ctx.request.body
record.modelId = ctx.params.modelId
if (!record._rev && !record._id) {
record._id = generateRecordID(record.modelId)
}
// if the record obj had an _id then it will have been retrieved
const existingRecord = ctx.preExisting
const model = await db.get(record.modelId)
record = coerceRecordValues(record, model)
const validateResult = await validate({
record,
model,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
// make sure link records are up to date
record = await linkRecords.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_SAVE,
record,
modelId: record.modelId,
model,
})
if (existingRecord) {
const response = await db.put(record)
record._rev = response.rev
record.type = "record"
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} updated successfully.`
return
}
record.type = "record"
const response = await db.post(record)
record._rev = response.rev
ctx.eventEmitter &&
ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model)
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} created successfully`
}

View File

@ -0,0 +1,350 @@
const CouchDB = require("../../db")
const validateJs = require("validate.js")
const linkRows = require("../../db/linkedRows")
const {
getRowParams,
generateRowID,
DocumentTypes,
SEPARATOR,
} = require("../../db/utils")
const { cloneDeep } = require("lodash")
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
validateJs.extend(validateJs.validators.datetime, {
parse: function(value) {
return new Date(value).getTime()
},
// Input is a unix timestamp
format: function(value) {
return new Date(value).toISOString()
},
})
exports.patch = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
let row = await db.get(ctx.params.id)
const table = await db.get(row.tableId)
const patchfields = ctx.request.body
row = coerceRowValues(row, table)
for (let key of Object.keys(patchfields)) {
if (!table.schema[key]) continue
row[key] = patchfields[key]
}
const validateResult = await validate({
row,
table,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
// returned row is cleaned and prepared for writing to DB
row = await linkRows.updateLinks({
instanceId,
eventType: linkRows.EventType.ROW_UPDATE,
row,
tableId: row.tableId,
table,
})
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
ctx.eventEmitter &&
ctx.eventEmitter.emitRow(`row:update`, instanceId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
}
exports.save = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
let row = ctx.request.body
row.tableId = ctx.params.tableId
if (!row._rev && !row._id) {
row._id = generateRowID(row.tableId)
}
// if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting
const table = await db.get(row.tableId)
row = coerceRowValues(row, table)
const validateResult = await validate({
row,
table,
})
if (!validateResult.valid) {
ctx.status = 400
ctx.body = {
status: 400,
errors: validateResult.errors,
}
return
}
// make sure link rows are up to date
row = await linkRows.updateLinks({
instanceId,
eventType: linkRows.EventType.ROW_SAVE,
row,
tableId: row.tableId,
table,
})
if (existingRow) {
const response = await db.put(row)
row._rev = response.rev
row.type = "row"
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} updated successfully.`
return
}
row.type = "row"
const response = await db.post(row)
row._rev = response.rev
ctx.eventEmitter &&
ctx.eventEmitter.emitRow(`row:save`, instanceId, row, table)
ctx.body = row
ctx.status = 200
ctx.message = `${table.name} created successfully`
}
exports.fetchView = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const { stats, group, field } = ctx.query
const viewName = ctx.params.viewName
// if this is a table view being looked for just transfer to that
if (viewName.indexOf(TABLE_VIEW_BEGINS_WITH) === 0) {
ctx.params.tableId = viewName.substring(4)
await exports.fetchTableRows(ctx)
return
}
const response = await db.query(`database/${viewName}`, {
include_docs: !stats,
group,
})
if (stats) {
response.rows = response.rows.map(row => ({
group: row.key,
field,
...row.value,
avg: row.value.sum / row.value.count,
}))
} else {
response.rows = response.rows.map(row => row.doc)
}
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
}
exports.fetchTableRows = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const response = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
ctx.body = response.rows.map(row => row.doc)
ctx.body = await linkRows.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
}
exports.search = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const response = await db.allDocs({
include_docs: true,
...ctx.request.body,
})
ctx.body = await linkRows.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
}
exports.find = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId does not match the rows tableId")
return
}
ctx.body = await linkRows.attachLinkInfo(instanceId, row)
}
exports.destroy = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const row = await db.get(ctx.params.rowId)
if (row.tableId !== ctx.params.tableId) {
ctx.throw(400, "Supplied tableId doesn't match the row's tableId")
return
}
await linkRows.updateLinks({
instanceId,
eventType: linkRows.EventType.ROW_DELETE,
row,
tableId: row.tableId,
})
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
ctx.status = 200
// for automations include the row that was deleted
ctx.row = row
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, instanceId, row)
}
exports.validate = async function(ctx) {
const errors = await validate({
instanceId: ctx.user.instanceId,
tableId: ctx.params.tableId,
row: ctx.request.body,
})
ctx.status = 200
ctx.body = errors
}
async function validate({ instanceId, tableId, row, table }) {
if (!table) {
const db = new CouchDB(instanceId)
table = await db.get(tableId)
}
const errors = {}
for (let fieldName of Object.keys(table.schema)) {
const res = validateJs.single(
row[fieldName],
table.schema[fieldName].constraints
)
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}
exports.fetchEnrichedRow = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const tableId = ctx.params.tableId
const rowId = ctx.params.rowId
if (instanceId == null || tableId == null || rowId == null) {
ctx.status = 400
ctx.body = {
status: 400,
error:
"Cannot handle request, URI params have not been successfully prepared.",
}
return
}
// need table to work out where links go in row
const [table, row] = await Promise.all([db.get(tableId), db.get(rowId)])
// get the link docs
const linkVals = await linkRows.getLinkDocuments({
instanceId,
tableId,
rowId,
})
// look up the actual rows based on the ids
const response = await db.allDocs({
include_docs: true,
keys: linkVals.map(linkVal => linkVal.id),
})
// need to include the IDs in these rows for any links they may have
let linkedRows = await linkRows.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
// insert the link rows in the correct place throughout the main row
for (let fieldName of Object.keys(table.schema)) {
let field = table.schema[fieldName]
if (field.type === "link") {
row[fieldName] = linkedRows.filter(
linkRow => linkRow.tableId === field.tableId
)
}
}
ctx.body = row
ctx.status = 200
}
function coerceRowValues(rec, table) {
const row = cloneDeep(rec)
for (let [key, value] of Object.entries(row)) {
const field = table.schema[key]
if (!field) continue
// eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
row[key] = TYPE_TRANSFORM_MAP[field.type][value]
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
}
}
return row
}
const TYPE_TRANSFORM_MAP = {
link: {
"": [],
[null]: [],
[undefined]: undefined,
},
options: {
"": "",
[null]: "",
[undefined]: undefined,
},
string: {
"": "",
[null]: "",
[undefined]: undefined,
},
number: {
"": null,
[null]: null,
[undefined]: undefined,
parse: n => parseFloat(n),
},
datetime: {
"": null,
[undefined]: undefined,
[null]: null,
},
attachment: {
"": [],
[null]: [],
[undefined]: undefined,
},
boolean: {
"": null,
[null]: null,
[undefined]: undefined,
true: true,
false: false,
},
}

View File

@ -0,0 +1,144 @@
const CouchDB = require("../../db")
const linkRows = require("../../db/linkedRows")
const csvParser = require("../../utilities/csvParser")
const {
getRowParams,
getTableParams,
generateTableID,
generateRowID,
} = require("../../db/utils")
exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
const body = await db.allDocs(
getTableParams(null, {
include_docs: true,
})
)
ctx.body = body.rows.map(row => row.doc)
}
exports.find = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
ctx.body = await db.get(ctx.params.id)
}
exports.save = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const { dataImport, ...rest } = ctx.request.body
const tableToSave = {
type: "table",
_id: generateTableID(),
views: {},
...rest,
}
let renameDocs = []
// if the table obj had an _id then it will have been retrieved
const oldTable = ctx.preExisting
// rename row fields when table column is renamed
const { _rename } = tableToSave
if (_rename && tableToSave.schema[_rename.updated].type === "link") {
throw "Cannot rename a linked field."
} else if (_rename && tableToSave.primaryDisplay === _rename.old) {
throw "Cannot rename the primary display field."
} else if (_rename) {
const rows = await db.allDocs(
getRowParams(tableToSave._id, null, {
include_docs: true,
})
)
renameDocs = rows.rows.map(({ doc }) => {
doc[_rename.updated] = doc[_rename.old]
delete doc[_rename.old]
return doc
})
delete tableToSave._rename
}
// update schema of non-statistics views when new columns are added
for (let view in tableToSave.views) {
const tableView = tableToSave.views[view]
if (!tableView) continue
if (tableView.schema.group || tableView.schema.field) continue
tableView.schema = tableToSave.schema
}
// update linked rows
await linkRows.updateLinks({
instanceId,
eventType: oldTable
? linkRows.EventType.TABLE_UPDATED
: linkRows.EventType.TABLE_SAVE,
table: tableToSave,
oldTable: oldTable,
})
// don't perform any updates until relationships have been
// checked by the updateLinks function
if (renameDocs.length !== 0) {
await db.bulkDocs(renameDocs)
}
const result = await db.post(tableToSave)
tableToSave._rev = result.rev
ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:save`, instanceId, tableToSave)
if (dataImport && dataImport.path) {
// Populate the table with rows imported from CSV in a bulk update
const data = await csvParser.transform(dataImport)
for (let row of data) {
row._id = generateRowID(tableToSave._id)
row.tableId = tableToSave._id
}
await db.bulkDocs(data)
}
ctx.status = 200
ctx.message = `Table ${ctx.request.body.name} saved successfully.`
ctx.body = tableToSave
}
exports.destroy = async function(ctx) {
const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId)
const tableToDelete = await db.get(ctx.params.tableId)
// Delete all rows for that table
const rows = await db.allDocs(
getRowParams(ctx.params.tableId, null, {
include_docs: true,
})
)
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true })))
// update linked rows
await linkRows.updateLinks({
instanceId,
eventType: linkRows.EventType.TABLE_DELETE,
table: tableToDelete,
})
// don't remove the table itself until very end
await db.remove(tableToDelete)
ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:delete`, instanceId, tableToDelete)
ctx.status = 200
ctx.message = `Table ${ctx.params.tableId} deleted.`
}
exports.validateCSVSchema = async function(ctx) {
const { file, schema = {} } = ctx.request.body
const result = await csvParser.parse(file.path, schema)
ctx.body = {
schema: result,
path: file.path,
}
}

View File

@ -4,7 +4,7 @@ const fs = require("fs")
const { join } = require("../../../utilities/centralPath") const { join } = require("../../../utilities/centralPath")
const os = require("os") const os = require("os")
const exporters = require("./exporters") const exporters = require("./exporters")
const { fetchView } = require("../record") const { fetchView } = require("../row")
const controller = { const controller = {
fetch: async ctx => { fetch: async ctx => {
@ -45,21 +45,21 @@ const controller = {
await db.put(designDoc) await db.put(designDoc)
// add views to model document // add views to table document
const model = await db.get(ctx.request.body.modelId) const table = await db.get(ctx.request.body.tableId)
if (!model.views) model.views = {} if (!table.views) table.views = {}
if (!view.meta.schema) { if (!view.meta.schema) {
view.meta.schema = model.schema view.meta.schema = table.schema
} }
model.views[viewToSave.name] = view.meta table.views[viewToSave.name] = view.meta
if (originalName) { if (originalName) {
delete model.views[originalName] delete table.views[originalName]
} }
await db.put(model) await db.put(table)
ctx.body = model.views[viewToSave.name] ctx.body = table.views[viewToSave.name]
ctx.message = `View ${viewToSave.name} saved successfully.` ctx.message = `View ${viewToSave.name} saved successfully.`
}, },
destroy: async ctx => { destroy: async ctx => {
@ -74,9 +74,9 @@ const controller = {
await db.put(designDoc) await db.put(designDoc)
const model = await db.get(view.meta.modelId) const table = await db.get(view.meta.tableId)
delete model.views[viewName] delete table.views[viewName]
await db.put(model) await db.put(table)
ctx.body = view ctx.body = view
ctx.message = `View ${ctx.params.viewName} saved successfully.` ctx.message = `View ${ctx.params.viewName} saved successfully.`
@ -85,7 +85,7 @@ const controller = {
const view = ctx.request.body const view = ctx.request.body
const format = ctx.query.format const format = ctx.query.format
// Fetch view records // Fetch view rows
ctx.params.viewName = view.name ctx.params.viewName = view.name
ctx.query.group = view.groupBy ctx.query.group = view.groupBy
if (view.field) { if (view.field) {

View File

@ -3,7 +3,7 @@
exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = ` exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = `
Object { Object {
"map": "function (doc) { "map": "function (doc) {
if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) { if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
emit(doc[\\"_id\\"], doc[\\"myField\\"]); emit(doc[\\"_id\\"], doc[\\"myField\\"]);
} }
}", }",
@ -12,7 +12,6 @@ Object {
"field": "myField", "field": "myField",
"filters": Array [], "filters": Array [],
"groupBy": undefined, "groupBy": undefined,
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"schema": Object { "schema": Object {
"avg": Object { "avg": Object {
"type": "number", "type": "number",
@ -36,6 +35,7 @@ Object {
"type": "number", "type": "number",
}, },
}, },
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
}, },
"reduce": "_stats", "reduce": "_stats",
} }
@ -44,7 +44,7 @@ Object {
exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = ` exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = `
Object { Object {
"map": "function (doc) { "map": "function (doc) {
if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && doc[\\"Name\\"] === \\"Test\\" || doc[\\"Yes\\"] > \\"Value\\") { if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && doc[\\"Name\\"] === \\"Test\\" || doc[\\"Yes\\"] > \\"Value\\") {
emit(doc._id); emit(doc._id);
} }
}", }",
@ -65,8 +65,8 @@ Object {
}, },
], ],
"groupBy": undefined, "groupBy": undefined,
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"schema": null, "schema": null,
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
}, },
} }
`; `;
@ -74,7 +74,7 @@ Object {
exports[`viewBuilder Group By creates a view emitting the group by field 1`] = ` exports[`viewBuilder Group By creates a view emitting the group by field 1`] = `
Object { Object {
"map": "function (doc) { "map": "function (doc) {
if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) { if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
emit(doc[\\"age\\"], doc[\\"score\\"]); emit(doc[\\"age\\"], doc[\\"score\\"]);
} }
}", }",
@ -83,8 +83,8 @@ Object {
"field": "score", "field": "score",
"filters": Array [], "filters": Array [],
"groupBy": "age", "groupBy": "age",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"schema": null, "schema": null,
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
}, },
"reduce": "_stats", "reduce": "_stats",
} }

View File

@ -6,7 +6,7 @@ describe("viewBuilder", () => {
it("creates a view with multiple filters and conjunctions", () => { it("creates a view with multiple filters and conjunctions", () => {
expect(viewTemplate({ expect(viewTemplate({
"name": "Test View", "name": "Test View",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [{ "filters": [{
"value": "Test", "value": "Test",
"condition": "EQUALS", "condition": "EQUALS",
@ -27,7 +27,7 @@ describe("viewBuilder", () => {
"name": "Calculate View", "name": "Calculate View",
"field": "myField", "field": "myField",
"calculation": "stats", "calculation": "stats",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [] "filters": []
})).toMatchSnapshot() })).toMatchSnapshot()
}) })
@ -37,7 +37,7 @@ describe("viewBuilder", () => {
it("creates a view emitting the group by field", () => { it("creates a view emitting the group by field", () => {
expect(viewTemplate({ expect(viewTemplate({
"name": "Test Scores Grouped By Age", "name": "Test Scores Grouped By Age",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"groupBy": "age", "groupBy": "age",
"field": "score", "field": "score",
"filters": [], "filters": [],

View File

@ -90,12 +90,12 @@ function parseEmitExpression(field, groupBy) {
* *
* @param {Object} viewDefinition - the JSON definition for a custom view. * @param {Object} viewDefinition - the JSON definition for a custom view.
* field: field that calculations will be performed on * field: field that calculations will be performed on
* modelId: modelId of the model this view was created from * tableId: tableId of the table this view was created from
* groupBy: field that calculations will be grouped by. Field must be present for this to be useful * groupBy: field that calculations will be grouped by. Field must be present for this to be useful
* filters: Array of filter objects containing predicates that are parsed into a JS expression * filters: Array of filter objects containing predicates that are parsed into a JS expression
* calculation: an optional calculation to be performed over the view data. * calculation: an optional calculation to be performed over the view data.
*/ */
function viewTemplate({ field, modelId, groupBy, filters = [], calculation }) { function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
const parsedFilters = parseFilterExpression(filters) const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& ${parsedFilters}` : "" const filterExpression = parsedFilters ? `&& ${parsedFilters}` : ""
@ -115,14 +115,14 @@ function viewTemplate({ field, modelId, groupBy, filters = [], calculation }) {
return { return {
meta: { meta: {
field, field,
modelId, tableId,
groupBy, groupBy,
filters, filters,
schema, schema,
calculation, calculation,
}, },
map: `function (doc) { map: `function (doc) {
if (doc.modelId === "${modelId}" ${filterExpression}) { if (doc.tableId === "${tableId}" ${filterExpression}) {
${emitExpression} ${emitExpression}
} }
}`, }`,

View File

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

View File

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

View File

@ -1,27 +0,0 @@
const Router = require("@koa/router")
const modelController = require("../controllers/model")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels")
const router = Router()
router
.get("/api/models", authorized(BUILDER), modelController.fetch)
.get(
"/api/models/:id",
authorized(READ_MODEL, ctx => ctx.params.id),
modelController.find
)
.post("/api/models", authorized(BUILDER), modelController.save)
.post(
"/api/models/csv/validate",
authorized(BUILDER),
modelController.validateCSVSchema
)
.delete(
"/api/models/:modelId/:revId",
authorized(BUILDER),
modelController.destroy
)
module.exports = router

View File

@ -1,49 +0,0 @@
const Router = require("@koa/router")
const recordController = require("../controllers/record")
const authorized = require("../../middleware/authorized")
const usage = require("../../middleware/usageQuota")
const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels")
const router = Router()
router
.get(
"/api/:modelId/:recordId/enrich",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchEnrichedRecord
)
.get(
"/api/:modelId/records",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchModelRecords
)
.get(
"/api/:modelId/records/:recordId",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.find
)
.post("/api/records/search", recordController.search)
.post(
"/api/:modelId/records",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
usage,
recordController.save
)
.patch(
"/api/:modelId/records/:id",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.patch
)
.post(
"/api/:modelId/records/validate",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.validate
)
.delete(
"/api/:modelId/records/:recordId/:revId",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
usage,
recordController.destroy
)
module.exports = router

View File

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

View File

@ -0,0 +1,27 @@
const Router = require("@koa/router")
const tableController = require("../controllers/table")
const authorized = require("../../middleware/authorized")
const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels")
const router = Router()
router
.get("/api/tables", authorized(BUILDER), tableController.fetch)
.get(
"/api/tables/:id",
authorized(READ_TABLE, ctx => ctx.params.id),
tableController.find
)
.post("/api/tables", authorized(BUILDER), tableController.save)
.post(
"/api/tables/csv/validate",
authorized(BUILDER),
tableController.validateCSVSchema
)
.delete(
"/api/tables/:tableId/:revId",
authorized(BUILDER),
tableController.destroy
)
module.exports = router

View File

@ -2,7 +2,7 @@ const {
createInstance, createInstance,
createClientDatabase, createClientDatabase,
createApplication, createApplication,
createModel, createTable,
createView, createView,
supertest, supertest,
defaultHeaders defaultHeaders
@ -12,8 +12,8 @@ const {
generatePowerUserPermissions, generatePowerUserPermissions,
POWERUSER_LEVEL_ID, POWERUSER_LEVEL_ID,
ADMIN_LEVEL_ID, ADMIN_LEVEL_ID,
READ_MODEL, READ_TABLE,
WRITE_MODEL, WRITE_TABLE,
} = require("../../../utilities/accessLevels") } = require("../../../utilities/accessLevels")
describe("/accesslevels", () => { describe("/accesslevels", () => {
@ -21,7 +21,7 @@ describe("/accesslevels", () => {
let server let server
let request let request
let instanceId let instanceId
let model let table
let view let view
beforeAll(async () => { beforeAll(async () => {
@ -36,8 +36,8 @@ describe("/accesslevels", () => {
beforeEach(async () => { beforeEach(async () => {
instanceId = (await createInstance(request, appId))._id instanceId = (await createInstance(request, appId))._id
model = await createModel(request, appId, instanceId) table = await createTable(request, appId, instanceId)
view = await createView(request, appId, instanceId, model._id) view = await createView(request, appId, instanceId, table._id)
}) })
describe("create", () => { describe("create", () => {
@ -63,7 +63,7 @@ describe("/accesslevels", () => {
it("should list custom levels, plus 2 default levels", async () => { it("should list custom levels, plus 2 default levels", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] })
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -96,7 +96,7 @@ describe("/accesslevels", () => {
it("should delete custom access level", async () => { it("should delete custom access level", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL } ] }) .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE } ] })
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -119,7 +119,7 @@ describe("/accesslevels", () => {
it("should add given permissions", async () => { it("should add given permissions", async () => {
const createRes = await request const createRes = await request
.post(`/api/accesslevels`) .post(`/api/accesslevels`)
.send({ name: "user", permissions: [ { itemId: model._id, name: READ_MODEL }] }) .send({ name: "user", permissions: [ { itemId: table._id, name: READ_TABLE }] })
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -130,7 +130,7 @@ describe("/accesslevels", () => {
.patch(`/api/accesslevels/${customLevel._id}`) .patch(`/api/accesslevels/${customLevel._id}`)
.send({ .send({
_rev: customLevel._rev, _rev: customLevel._rev,
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ] addedPermissions: [ { itemId: table._id, name: WRITE_TABLE } ]
}) })
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -142,8 +142,8 @@ describe("/accesslevels", () => {
.expect(200) .expect(200)
expect(finalRes.body.permissions.length).toBe(2) expect(finalRes.body.permissions.length).toBe(2)
expect(finalRes.body.permissions.some(p => p.name === WRITE_MODEL)).toBe(true) expect(finalRes.body.permissions.some(p => p.name === WRITE_TABLE)).toBe(true)
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true) expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
}) })
it("should remove given permissions", async () => { it("should remove given permissions", async () => {
@ -152,8 +152,8 @@ describe("/accesslevels", () => {
.send({ .send({
name: "user", name: "user",
permissions: [ permissions: [
{ itemId: model._id, name: READ_MODEL }, { itemId: table._id, name: READ_TABLE },
{ itemId: model._id, name: WRITE_MODEL }, { itemId: table._id, name: WRITE_TABLE },
] ]
}) })
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
@ -166,7 +166,7 @@ describe("/accesslevels", () => {
.patch(`/api/accesslevels/${customLevel._id}`) .patch(`/api/accesslevels/${customLevel._id}`)
.send({ .send({
_rev: customLevel._rev, _rev: customLevel._rev,
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }] removedPermissions: [ { itemId: table._id, name: WRITE_TABLE }]
}) })
.set(defaultHeaders(appId, instanceId)) .set(defaultHeaders(appId, instanceId))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -178,7 +178,7 @@ describe("/accesslevels", () => {
.expect(200) .expect(200)
expect(finalRes.body.permissions.length).toBe(1) expect(finalRes.body.permissions.length).toBe(1)
expect(finalRes.body.permissions.some(p => p.name === READ_MODEL)).toBe(true) expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
}) })
}) })
}); });

View File

@ -2,8 +2,8 @@ const {
createClientDatabase, createClientDatabase,
createApplication, createApplication,
createInstance, createInstance,
createModel, createTable,
getAllFromModel, getAllFromTable,
defaultHeaders, defaultHeaders,
supertest, supertest,
insertDocument, insertDocument,
@ -126,10 +126,10 @@ describe("/automations", () => {
describe("create", () => { describe("create", () => {
it("should setup the automation fully", () => { it("should setup the automation fully", () => {
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"] let trigger = TRIGGER_DEFINITIONS["ROW_SAVED"]
trigger.id = "wadiawdo34" trigger.id = "wadiawdo34"
let createAction = ACTION_DEFINITIONS["CREATE_RECORD"] let createAction = ACTION_DEFINITIONS["CREATE_ROW"]
createAction.inputs.record = { createAction.inputs.row = {
name: "{{trigger.name}}", name: "{{trigger.name}}",
description: "{{trigger.description}}" description: "{{trigger.description}}"
} }
@ -167,9 +167,9 @@ describe("/automations", () => {
describe("trigger", () => { describe("trigger", () => {
it("trigger the automation successfully", async () => { it("trigger the automation successfully", async () => {
let model = await createModel(request, app._id, instance._id) let table = await createTable(request, app._id, instance._id)
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id TEST_AUTOMATION.definition.trigger.inputs.tableId = table._id
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id TEST_AUTOMATION.definition.steps[0].inputs.row.tableId = table._id
await createAutomation() await createAutomation()
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to // 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 // know that it has finished all of its actions - this is currently the best way
@ -180,7 +180,7 @@ describe("/automations", () => {
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`) expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name) expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
await delay(500) await delay(500)
let elements = await getAllFromModel(request, app._id, instance._id, model._id) let elements = await getAllFromTable(request, app._id, instance._id, table._id)
// don't test it unless there are values to test // don't test it unless there are values to test
if (elements.length === 1) { if (elements.length === 1) {
expect(elements.length).toEqual(1) expect(elements.length).toEqual(1)
@ -189,7 +189,7 @@ describe("/automations", () => {
return return
} }
} }
throw "Failed to find the records" throw "Failed to find the rows"
}) })
}) })

View File

@ -39,13 +39,13 @@ exports.defaultHeaders = (appId, instanceId) => {
} }
} }
exports.createModel = async (request, appId, instanceId, model) => { exports.createTable = async (request, appId, instanceId, table) => {
if (model != null && model._id) { if (table != null && table._id) {
delete model._id delete table._id
} }
model = model || { table = table || {
name: "TestModel", name: "TestTable",
type: "model", type: "table",
key: "name", key: "name",
schema: { schema: {
name: { name: {
@ -64,23 +64,23 @@ exports.createModel = async (request, appId, instanceId, model) => {
} }
const res = await request const res = await request
.post(`/api/models`) .post(`/api/tables`)
.set(exports.defaultHeaders(appId, instanceId)) .set(exports.defaultHeaders(appId, instanceId))
.send(model) .send(table)
return res.body return res.body
} }
exports.getAllFromModel = async (request, appId, instanceId, modelId) => { exports.getAllFromTable = async (request, appId, instanceId, tableId) => {
const res = await request const res = await request
.get(`/api/${modelId}/records`) .get(`/api/${tableId}/rows`)
.set(exports.defaultHeaders(appId, instanceId)) .set(exports.defaultHeaders(appId, instanceId))
return res.body return res.body
} }
exports.createView = async (request, appId, instanceId, modelId, view) => { exports.createView = async (request, appId, instanceId, tableId, view) => {
view = view || { view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ", map: "function(doc) { emit(doc[doc.key], doc._id); } ",
modelId: modelId, tableId: tableId,
} }
const res = await request const res = await request

View File

@ -2,17 +2,17 @@ const {
createApplication, createApplication,
createClientDatabase, createClientDatabase,
createInstance, createInstance,
createModel, createTable,
supertest, supertest,
defaultHeaders, defaultHeaders,
} = require("./couchTestUtils"); } = require("./couchTestUtils");
describe("/records", () => { describe("/rows", () => {
let request let request
let server let server
let instance let instance
let model let table
let record let row
let app let app
beforeAll(async () => { beforeAll(async () => {
@ -27,26 +27,26 @@ describe("/records", () => {
beforeEach(async () => { beforeEach(async () => {
instance = await createInstance(request, app._id) instance = await createInstance(request, app._id)
model = await createModel(request, app._id, instance._id) table = await createTable(request, app._id, instance._id)
record = { row = {
name: "Test Contact", name: "Test Contact",
description: "original description", description: "original description",
status: "new", status: "new",
modelId: model._id tableId: table._id
} }
}) })
const createRecord = async r => const createRow = async r =>
await request await request
.post(`/api/${r ? r.modelId : record.modelId}/records`) .post(`/api/${r ? r.tableId : row.tableId}/rows`)
.send(r || record) .send(r || row)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const loadRecord = async id => const loadRow = async id =>
await request await request
.get(`/api/${model._id}/records/${id}`) .get(`/api/${table._id}/rows/${id}`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -55,114 +55,114 @@ describe("/records", () => {
describe("save, load, update, delete", () => { describe("save, load, update, delete", () => {
it("returns a success message when the record is created", async () => { it("returns a success message when the row is created", async () => {
const res = await createRecord() const res = await createRow()
expect(res.res.statusMessage).toEqual(`${model.name} created successfully`) expect(res.res.statusMessage).toEqual(`${table.name} created successfully`)
expect(res.body.name).toEqual("Test Contact") expect(res.body.name).toEqual("Test Contact")
expect(res.body._rev).toBeDefined() expect(res.body._rev).toBeDefined()
}) })
it("updates a record successfully", async () => { it("updates a row successfully", async () => {
const rec = await createRecord() const rec = await createRow()
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.post(`/api/${model._id}/records`) .post(`/api/${table._id}/rows`)
.send({ .send({
_id: existing._id, _id: existing._id,
_rev: existing._rev, _rev: existing._rev,
modelId: model._id, tableId: table._id,
name: "Updated Name", name: "Updated Name",
}) })
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.res.statusMessage).toEqual(`${model.name} updated successfully.`) expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`)
expect(res.body.name).toEqual("Updated Name") expect(res.body.name).toEqual("Updated Name")
}) })
it("should load a record", async () => { it("should load a row", async () => {
const rec = await createRecord() const rec = await createRow()
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.get(`/api/${model._id}/records/${existing._id}`) .get(`/api/${table._id}/rows/${existing._id}`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body).toEqual({ expect(res.body).toEqual({
...record, ...row,
_id: existing._id, _id: existing._id,
_rev: existing._rev, _rev: existing._rev,
type: "record", type: "row",
}) })
}) })
it("should list all records for given modelId", async () => { it("should list all rows for given tableId", async () => {
const newRecord = { const newRow = {
modelId: model._id, tableId: table._id,
name: "Second Contact", name: "Second Contact",
status: "new" status: "new"
} }
await createRecord() await createRow()
await createRecord(newRecord) await createRow(newRow)
const res = await request const res = await request
.get(`/api/${model._id}/records`) .get(`/api/${table._id}/rows`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body.length).toBe(2) expect(res.body.length).toBe(2)
expect(res.body.find(r => r.name === newRecord.name)).toBeDefined() expect(res.body.find(r => r.name === newRow.name)).toBeDefined()
expect(res.body.find(r => r.name === record.name)).toBeDefined() expect(res.body.find(r => r.name === row.name)).toBeDefined()
}) })
it("lists records when queried by their ID", async () => { it("lists rows when queried by their ID", async () => {
const newRecord = { const newRow = {
modelId: model._id, tableId: table._id,
name: "Second Contact", name: "Second Contact",
status: "new" status: "new"
} }
const record = await createRecord() const row = await createRow()
const secondRecord = await createRecord(newRecord) 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 const res = await request
.post(`/api/records/search`) .post(`/api/rows/search`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.send({ .send({
keys: recordIds keys: rowIds
}) })
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.body.length).toBe(2) 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 () => { it("load should return 404 when row does not exist", async () => {
await createRecord() await createRow()
await request await request
.get(`/api/${model._id}/records/not-a-valid-id`) .get(`/api/${table._id}/rows/not-a-valid-id`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(404) .expect(404)
}) })
it("record values are coerced", async () => { it("row values are coerced", async () => {
const str = {type:"string", constraints: { type: "string", presence: false }} const str = {type:"string", constraints: { type: "string", presence: false }}
const attachment = {type:"attachment", constraints: { type: "array", presence: false }} const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }} const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
const number = {type:"number", constraints: { type: "number", presence: false }} const number = {type:"number", constraints: { type: "number", presence: false }}
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }} const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
model = await createModel(request, app._id, instance._id, { table = await createTable(request, app._id, instance._id, {
name: "TestModel2", name: "TestTable2",
type: "model", type: "table",
key: "name", key: "name",
schema: { schema: {
name: str, name: str,
@ -189,8 +189,8 @@ describe("/records", () => {
}, },
}) })
record = { row = {
name: "Test Record", name: "Test Row",
stringUndefined: undefined, stringUndefined: undefined,
stringNull: null, stringNull: null,
stringString: "i am a string", stringString: "i am a string",
@ -209,15 +209,15 @@ describe("/records", () => {
boolUndefined: undefined, boolUndefined: undefined,
boolString: "true", boolString: "true",
boolBool: true, boolBool: true,
modelId: model._id, tableId: table._id,
attachmentNull : null, attachmentNull : null,
attachmentUndefined : undefined, attachmentUndefined : undefined,
attachmentEmpty : "", 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.stringUndefined).toBe(undefined)
expect(saved.stringNull).toBe("") expect(saved.stringNull).toBe("")
@ -230,8 +230,8 @@ describe("/records", () => {
expect(saved.datetimeEmptyString).toBe(null) expect(saved.datetimeEmptyString).toBe(null)
expect(saved.datetimeNull).toBe(null) expect(saved.datetimeNull).toBe(null)
expect(saved.datetimeUndefined).toBe(undefined) expect(saved.datetimeUndefined).toBe(undefined)
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString()) expect(saved.datetimeString).toBe(new Date(row.datetimeString).toISOString())
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString()) expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString())
expect(saved.boolNull).toBe(null) expect(saved.boolNull).toBe(null)
expect(saved.boolEmpty).toBe(null) expect(saved.boolEmpty).toBe(null)
expect(saved.boolUndefined).toBe(undefined) expect(saved.boolUndefined).toBe(undefined)
@ -245,37 +245,37 @@ describe("/records", () => {
describe("patch", () => { describe("patch", () => {
it("should update only the fields that are supplied", async () => { it("should update only the fields that are supplied", async () => {
const rec = await createRecord() const rec = await createRow()
const existing = rec.body const existing = rec.body
const res = await request const res = await request
.patch(`/api/${model._id}/records/${existing._id}`) .patch(`/api/${table._id}/rows/${existing._id}`)
.send({ .send({
_id: existing._id, _id: existing._id,
_rev: existing._rev, _rev: existing._rev,
modelId: model._id, tableId: table._id,
name: "Updated Name", name: "Updated Name",
}) })
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(res.res.statusMessage).toEqual(`${model.name} updated successfully.`) expect(res.res.statusMessage).toEqual(`${table.name} updated successfully.`)
expect(res.body.name).toEqual("Updated Name") expect(res.body.name).toEqual("Updated Name")
expect(res.body.description).toEqual(existing.description) 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(savedRow.body.description).toEqual(existing.description)
expect(savedRecord.body.name).toEqual("Updated Name") expect(savedRow.body.name).toEqual("Updated Name")
}) })
}) })
describe("validate", () => { describe("validate", () => {
it("should return no errors on valid record", async () => { it("should return no errors on valid row", async () => {
const result = await request const result = await request
.post(`/api/${model._id}/records/validate`) .post(`/api/${table._id}/rows/validate`)
.send({ name: "ivan" }) .send({ name: "ivan" })
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -285,9 +285,9 @@ describe("/records", () => {
expect(Object.keys(result.body.errors)).toEqual([]) 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 const result = await request
.post(`/api/${model._id}/records/validate`) .post(`/api/${table._id}/rows/validate`)
.send({ name: 1 }) .send({ name: 1 })
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)

View File

@ -1,6 +1,6 @@
const { const {
createInstance, createInstance,
createModel, createTable,
supertest, supertest,
createClientDatabase, createClientDatabase,
createApplication, createApplication,
@ -9,7 +9,7 @@ const {
getDocument getDocument
} = require("./couchTestUtils") } = require("./couchTestUtils")
describe("/models", () => { describe("/tables", () => {
let request let request
let server let server
let app let app
@ -30,11 +30,11 @@ describe("/models", () => {
instance = await createInstance(request, app._id); instance = await createInstance(request, app._id);
}); });
it("returns a success message when the model is successfully created", done => { it("returns a success message when the table is successfully created", done => {
request request
.post(`/api/models`) .post(`/api/tables`)
.send({ .send({
name: "TestModel", name: "TestTable",
key: "name", key: "name",
schema: { schema: {
name: { type: "string" } name: { type: "string" }
@ -44,17 +44,17 @@ describe("/models", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (err, res) => { .end(async (err, res) => {
expect(res.res.statusMessage).toEqual("Model TestModel saved successfully."); expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.");
expect(res.body.name).toEqual("TestModel"); expect(res.body.name).toEqual("TestTable");
done(); done();
}); });
}) })
it("renames all the record fields for a model when a schema key is renamed", async () => { it("renames all the row fields for a table when a schema key is renamed", async () => {
const testModel = await createModel(request, app._id, instance._id); const testTable = await createTable(request, app._id, instance._id);
const testRecord = await request const testRow = await request
.post(`/api/${testModel._id}/records`) .post(`/api/${testTable._id}/rows`)
.send({ .send({
name: "test" name: "test"
}) })
@ -62,12 +62,12 @@ describe("/models", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const updatedModel = await request const updatedTable = await request
.post(`/api/models`) .post(`/api/tables`)
.send({ .send({
_id: testModel._id, _id: testTable._id,
_rev: testModel._rev, _rev: testTable._rev,
name: "TestModel", name: "TestTable",
key: "name", key: "name",
_rename: { _rename: {
old: "name", old: "name",
@ -81,11 +81,11 @@ describe("/models", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
expect(updatedModel.res.statusMessage).toEqual("Model TestModel saved successfully."); expect(updatedTable.res.statusMessage).toEqual("Table TestTable saved successfully.");
expect(updatedModel.body.name).toEqual("TestModel"); expect(updatedTable.body.name).toEqual("TestTable");
const res = await request const res = await request
.get(`/api/${testModel._id}/records/${testRecord.body._id}`) .get(`/api/${testTable._id}/rows/${testRow.body._id}`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -98,11 +98,11 @@ describe("/models", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "POST", method: "POST",
url: `/api/models`, url: `/api/tables`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id, appId: app._id,
body: { body: {
name: "TestModel", name: "TestTable",
key: "name", key: "name",
schema: { schema: {
name: { type: "string" } name: { type: "string" }
@ -113,27 +113,27 @@ describe("/models", () => {
}); });
describe("fetch", () => { describe("fetch", () => {
let testModel let testTable
beforeEach(async () => { beforeEach(async () => {
instance = await createInstance(request, app._id) instance = await createInstance(request, app._id)
testModel = await createModel(request, app._id, instance._id, testModel) testTable = await createTable(request, app._id, instance._id, testTable)
}); });
afterEach(() => { afterEach(() => {
delete testModel._rev delete testTable._rev
}); });
it("returns all the models for that instance in the response body", done => { it("returns all the tables for that instance in the response body", done => {
request request
.get(`/api/models`) .get(`/api/tables`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
const fetchedModel = res.body[0]; const fetchedTable = res.body[0];
expect(fetchedModel.name).toEqual(testModel.name); expect(fetchedTable.name).toEqual(testTable.name);
expect(fetchedModel.type).toEqual("model"); expect(fetchedTable.type).toEqual("table");
done(); done();
}); });
}) })
@ -142,7 +142,7 @@ describe("/models", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "GET", method: "GET",
url: `/api/models`, url: `/api/tables`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id, appId: app._id,
}) })
@ -150,33 +150,33 @@ describe("/models", () => {
}); });
describe("destroy", () => { describe("destroy", () => {
let testModel; let testTable;
beforeEach(async () => { beforeEach(async () => {
instance = await createInstance(request, app._id) instance = await createInstance(request, app._id)
testModel = await createModel(request, app._id, instance._id, testModel) testTable = await createTable(request, app._id, instance._id, testTable)
}); });
afterEach(() => { afterEach(() => {
delete testModel._rev delete testTable._rev
}); });
it("returns a success response when a model is deleted.", async done => { it("returns a success response when a table is deleted.", async done => {
request request
.delete(`/api/models/${testModel._id}/${testModel._rev}`) .delete(`/api/tables/${testTable._id}/${testTable._rev}`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`); expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`);
done(); done();
}); });
}) })
it("deletes linked references to the model after deletion", async done => { it("deletes linked references to the table after deletion", async done => {
const linkedModel = await createModel(request, app._id, instance._id, { const linkedTable = await createTable(request, app._id, instance._id, {
name: "LinkedModel", name: "LinkedTable",
type: "model", type: "table",
key: "name", key: "name",
schema: { schema: {
name: { name: {
@ -185,9 +185,9 @@ describe("/models", () => {
type: "string", type: "string",
}, },
}, },
TestModel: { TestTable: {
type: "link", type: "link",
modelId: testModel._id, tableId: testTable._id,
constraints: { constraints: {
type: "array" type: "array"
} }
@ -196,14 +196,14 @@ describe("/models", () => {
}) })
request request
.delete(`/api/models/${testModel._id}/${testModel._rev}`) .delete(`/api/tables/${testTable._id}/${testTable._rev}`)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
.end(async (_, res) => { .end(async (_, res) => {
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`); expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`);
const dependentModel = await getDocument(instance._id, linkedModel._id) const dependentTable = await getDocument(instance._id, linkedTable._id)
expect(dependentModel.schema.TestModel).not.toBeDefined(); expect(dependentTable.schema.TestTable).not.toBeDefined();
done(); done();
}); });
}) })
@ -212,7 +212,7 @@ describe("/models", () => {
await builderEndpointShouldBlockNormalUsers({ await builderEndpointShouldBlockNormalUsers({
request, request,
method: "DELETE", method: "DELETE",
url: `/api/models/${testModel._id}/${testModel._rev}`, url: `/api/tables/${testTable._id}/${testTable._rev}`,
instanceId: instance._id, instanceId: instance._id,
appId: app._id, appId: app._id,
}) })

View File

@ -2,7 +2,7 @@ const {
createClientDatabase, createClientDatabase,
createApplication, createApplication,
createInstance, createInstance,
createModel, createTable,
supertest, supertest,
defaultHeaders, defaultHeaders,
getDocument getDocument
@ -13,12 +13,12 @@ describe("/views", () => {
let server let server
let app let app
let instance let instance
let model let table
const createView = async (config = { const createView = async (config = {
name: "TestView", name: "TestView",
field: "Price", field: "Price",
modelId: model._id tableId: table._id
}) => }) =>
await request await request
.post(`/api/views`) .post(`/api/views`)
@ -27,9 +27,9 @@ describe("/views", () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
const createRecord = async record => request const createRow = async row => request
.post(`/api/${model._id}/records`) .post(`/api/${table._id}/rows`)
.send(record) .send(row)
.set(defaultHeaders(app._id, instance._id)) .set(defaultHeaders(app._id, instance._id))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200) .expect(200)
@ -50,7 +50,7 @@ describe("/views", () => {
describe("create", () => { describe("create", () => {
beforeEach(async () => { beforeEach(async () => {
model = await createModel(request, app._id, instance._id); table = await createTable(request, app._id, instance._id);
}) })
it("returns a success message when the view is successfully created", async () => { it("returns a success message when the view is successfully created", async () => {
@ -58,14 +58,14 @@ describe("/views", () => {
expect(res.res.statusMessage).toEqual("View TestView saved successfully."); expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
}) })
it("updates the model record with the new view metadata", async () => { it("updates the table row with the new view metadata", async () => {
const res = await createView() const res = await createView()
expect(res.res.statusMessage).toEqual("View TestView saved successfully."); expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
const updatedModel = await getDocument(instance._id, model._id) const updatedTable = await getDocument(instance._id, table._id)
expect(updatedModel.views).toEqual({ expect(updatedTable.views).toEqual({
TestView: { TestView: {
field: "Price", field: "Price",
modelId: model._id, tableId: table._id,
filters: [], filters: [],
schema: { schema: {
name: { name: {
@ -88,7 +88,7 @@ describe("/views", () => {
describe("fetch", () => { describe("fetch", () => {
beforeEach(async () => { beforeEach(async () => {
model = await createModel(request, app._id, instance._id); table = await createTable(request, app._id, instance._id);
}); });
it("returns only custom views", async () => { it("returns only custom views", async () => {
@ -105,21 +105,21 @@ describe("/views", () => {
describe("query", () => { describe("query", () => {
beforeEach(async () => { beforeEach(async () => {
model = await createModel(request, app._id, instance._id); table = await createTable(request, app._id, instance._id);
}); });
it("returns data for the created view", async () => { it("returns data for the created view", async () => {
await createView() await createView()
await createRecord({ await createRow({
modelId: model._id, tableId: table._id,
Price: 1000 Price: 1000
}) })
await createRecord({ await createRow({
modelId: model._id, tableId: table._id,
Price: 2000 Price: 2000
}) })
await createRecord({ await createRow({
modelId: model._id, tableId: table._id,
Price: 4000 Price: 4000
}) })
const res = await request const res = await request
@ -136,20 +136,20 @@ describe("/views", () => {
name: "TestView", name: "TestView",
field: "Price", field: "Price",
groupBy: "Category", groupBy: "Category",
modelId: model._id tableId: table._id
}) })
await createRecord({ await createRow({
modelId: model._id, tableId: table._id,
Price: 1000, Price: 1000,
Category: "One" Category: "One"
}) })
await createRecord({ await createRow({
modelId: model._id, tableId: table._id,
Price: 2000, Price: 2000,
Category: "One" Category: "One"
}) })
await createRecord({ await createRow({
modelId: model._id, tableId: table._id,
Price: 4000, Price: 4000,
Category: "Two" Category: "Two"
}) })

Some files were not shown because too many files have changed in this diff Show More