Merge pull request #687 from Budibase/endpoint-renaming
Major update - renaming model to table and record to row
This commit is contained in:
commit
b5671e467f
|
@ -145,7 +145,7 @@ The HTML and CSS for your apps runtime pages, as well as the budibase client lib
|
|||
|
||||
#### 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
|
||||
|
||||
|
|
|
@ -226,7 +226,7 @@
|
|||
"company": {
|
||||
"name": "Hoeger LLC",
|
||||
"catchPhrase": "Centralized empowering task-force",
|
||||
"bs": "target end-to-end models"
|
||||
"bs": "target end-to-end tables"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -16,7 +16,7 @@ context("Create a automation", () => {
|
|||
cy.contains("automate").click()
|
||||
cy.contains("Create New Automation").click()
|
||||
cy.get(".modal").within(() => {
|
||||
cy.get("input").type("Add Record")
|
||||
cy.get("input").type("Add Row")
|
||||
cy.get(".buttons")
|
||||
.contains("Create")
|
||||
.click()
|
||||
|
@ -24,7 +24,7 @@ context("Create a automation", () => {
|
|||
|
||||
// Add trigger
|
||||
cy.get("[data-cy=add-automation-component]").click()
|
||||
cy.get("[data-cy=RECORD_SAVED]").click()
|
||||
cy.get("[data-cy=ROW_SAVED]").click()
|
||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
|
@ -32,7 +32,7 @@ context("Create a automation", () => {
|
|||
})
|
||||
|
||||
// Create action
|
||||
cy.get("[data-cy=CREATE_RECORD]").click()
|
||||
cy.get("[data-cy=CREATE_ROW]").click()
|
||||
cy.get("[data-cy=automation-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
|
@ -53,9 +53,9 @@ context("Create a automation", () => {
|
|||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
||||
|
||||
it("should add record when a new record is added", () => {
|
||||
it("should add row when a new row is added", () => {
|
||||
cy.contains("backend").click()
|
||||
cy.addRecord(["Rover", 15])
|
||||
cy.addRow(["Rover", 15])
|
||||
cy.reload()
|
||||
cy.contains("goodboy").should("have.text", "goodboy")
|
||||
})
|
||||
|
|
|
@ -4,9 +4,9 @@ xcontext('Create Components', () => {
|
|||
cy.server()
|
||||
cy.visit('localhost:4001/_builder')
|
||||
// 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.addRecord('bob', '15')
|
||||
cy.addRow('bob', '15')
|
||||
})
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
|
|
|
@ -16,8 +16,8 @@ context("Create a Table", () => {
|
|||
cy.contains("name").should("be.visible")
|
||||
})
|
||||
|
||||
it("creates a record in the table", () => {
|
||||
cy.addRecord(["Rover"])
|
||||
it("creates a row in the table", () => {
|
||||
cy.addRow(["Rover"])
|
||||
cy.contains("Rover").should("be.visible")
|
||||
})
|
||||
|
||||
|
@ -32,7 +32,7 @@ context("Create a Table", () => {
|
|||
cy.contains("nameupdated").should("have.text", "nameupdated")
|
||||
})
|
||||
|
||||
it("edits a record", () => {
|
||||
it("edits a row", () => {
|
||||
cy.get("tbody .ri-more-line").click()
|
||||
cy.get("[data-cy=edit-row]").click()
|
||||
cy.get(".modal input").type("Updated")
|
||||
|
@ -40,7 +40,7 @@ context("Create a Table", () => {
|
|||
cy.contains("RoverUpdated").should("have.text", "RoverUpdated")
|
||||
})
|
||||
|
||||
it("deletes a record", () => {
|
||||
it("deletes a row", () => {
|
||||
cy.get("tbody .ri-more-line").click()
|
||||
cy.get("[data-cy=delete-row]").click()
|
||||
cy.contains("Delete Row").click()
|
||||
|
|
|
@ -7,13 +7,13 @@ context("Create a View", () => {
|
|||
cy.addColumn("data", "age", "Number")
|
||||
cy.addColumn("data", "rating", "Number")
|
||||
|
||||
// 6 Records
|
||||
cy.addRecord(["Students", 25, 1])
|
||||
cy.addRecord(["Students", 20, 3])
|
||||
cy.addRecord(["Students", 18, 6])
|
||||
cy.addRecord(["Students", 25, 2])
|
||||
cy.addRecord(["Teachers", 49, 5])
|
||||
cy.addRecord(["Teachers", 36, 3])
|
||||
// 6 Rows
|
||||
cy.addRow(["Students", 25, 1])
|
||||
cy.addRow(["Students", 20, 3])
|
||||
cy.addRow(["Students", 18, 6])
|
||||
cy.addRow(["Students", 25, 2])
|
||||
cy.addRow(["Teachers", 49, 5])
|
||||
cy.addRow(["Teachers", 36, 3])
|
||||
})
|
||||
|
||||
it("creates a view", () => {
|
||||
|
@ -109,7 +109,7 @@ context("Create 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")
|
||||
.click()
|
||||
cy.contains("Edit").click()
|
||||
|
@ -121,8 +121,8 @@ context("Create a View", () => {
|
|||
})
|
||||
|
||||
it("deletes a view", () => {
|
||||
cy.contains("[data-cy=model-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").click()
|
||||
cy.contains("[data-cy=table-nav-item]", "Test View Updated")
|
||||
.find(".ri-more-line")
|
||||
.click()
|
||||
cy.contains("Delete").click()
|
||||
|
|
|
@ -3,7 +3,7 @@ context('Screen Tests', () => {
|
|||
before(() => {
|
||||
cy.server()
|
||||
cy.visit('localhost:4001/_builder')
|
||||
cy.createApp('Conor Cy App', 'Model App Description')
|
||||
cy.createApp('Conor Cy App', 'Table App Description')
|
||||
cy.navigateToFrontend()
|
||||
})
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ Cypress.Commands.add("createTestTableWithData", () => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("createTable", tableName => {
|
||||
// Enter model name
|
||||
// Enter table name
|
||||
cy.contains("Create New Table").click()
|
||||
cy.get(".modal").within(() => {
|
||||
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.get(".modal").within(() => {
|
||||
|
|
|
@ -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
|
||||
* @propperty {Object} screen - current screen - where componentInstanceId lives
|
||||
* @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
|
||||
* @returns {Array.<BindableProperty>}
|
||||
*/
|
||||
export default function({ componentInstanceId, screen, components, models }) {
|
||||
export default function({ componentInstanceId, screen, components, tables }) {
|
||||
const walkResult = walk({
|
||||
// cloning so we are free to mutate props (e.g. by adding _contexts)
|
||||
instance: cloneDeep(screen.props),
|
||||
targetId: componentInstanceId,
|
||||
components,
|
||||
models,
|
||||
tables,
|
||||
})
|
||||
|
||||
return [
|
||||
|
@ -38,7 +38,7 @@ export default function({ componentInstanceId, screen, components, models }) {
|
|||
.map(componentInstanceToBindable(walkResult)),
|
||||
|
||||
...(walkResult.target?._contexts
|
||||
.map(contextToBindables(models, walkResult))
|
||||
.map(contextToBindables(tables, walkResult))
|
||||
.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 modelId = context.model?.modelId ?? context.model
|
||||
const model = models.find(model => model._id === modelId)
|
||||
const tableId = context.table?.tableId ?? context.table
|
||||
const table = tables.find(table => table._id === tableId)
|
||||
let schema =
|
||||
context.model?.type === "view"
|
||||
? model?.views?.[context.model.name]?.schema
|
||||
: model?.schema
|
||||
context.table?.type === "view"
|
||||
? table?.views?.[context.table.name]?.schema
|
||||
: table?.schema
|
||||
|
||||
// Avoid crashing whenever no data source has been selected
|
||||
if (!schema) {
|
||||
|
@ -98,9 +98,9 @@ const contextToBindables = (models, walkResult) => context => {
|
|||
// how the binding expression persists, and is used in the app at runtime
|
||||
runtimeBinding: `${contextParentPath}data.${runtimeBoundKey}`,
|
||||
// how the binding expressions looks to the user of the builder
|
||||
readableBinding: `${context.instance._instanceName}.${model.name}.${key}`,
|
||||
// model / view info
|
||||
model: context.model,
|
||||
readableBinding: `${context.instance._instanceName}.${table.name}.${key}`,
|
||||
// table / view info
|
||||
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) {
|
||||
result = {
|
||||
target: null,
|
||||
|
@ -165,8 +165,8 @@ const walk = ({ instance, targetId, components, models, result }) => {
|
|||
if (contextualInstance) {
|
||||
// add to currentContexts (ancestory of context)
|
||||
// before walking children
|
||||
const model = instance[component.context]
|
||||
result.currentContexts.push({ instance, model })
|
||||
const table = instance[component.context]
|
||||
result.currentContexts.push({ instance, table })
|
||||
}
|
||||
|
||||
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
|
||||
// original component instances
|
||||
child._contexts = currentContexts
|
||||
walk({ instance: child, targetId, components, models, result })
|
||||
walk({ instance: child, targetId, components, tables, result })
|
||||
}
|
||||
|
||||
if (contextualInstance) {
|
||||
|
|
|
@ -26,16 +26,16 @@ export default {
|
|||
],
|
||||
trigger: {
|
||||
id: "iRzYMOqND",
|
||||
name: "Record Saved",
|
||||
event: "record:save",
|
||||
name: "Row Saved",
|
||||
event: "row:save",
|
||||
icon: "ri-save-line",
|
||||
tagline: "Record is added to <b>{{model.name}}</b>",
|
||||
description: "Fired when a record is saved to your database.",
|
||||
params: { model: "model" },
|
||||
tagline: "Row is added to <b>{{table.name}}</b>",
|
||||
description: "Fired when a row is saved to your database.",
|
||||
params: { table: "table" },
|
||||
type: "TRIGGER",
|
||||
args: {
|
||||
model: {
|
||||
type: "model",
|
||||
table: {
|
||||
type: "table",
|
||||
views: {},
|
||||
name: "users",
|
||||
schema: {
|
||||
|
@ -65,7 +65,7 @@ export default {
|
|||
_rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff",
|
||||
},
|
||||
},
|
||||
stepId: "RECORD_SAVED",
|
||||
stepId: "ROW_SAVED",
|
||||
},
|
||||
},
|
||||
type: "automation",
|
||||
|
|
|
@ -3,12 +3,12 @@ import { cloneDeep } from "lodash/fp"
|
|||
import api from "../api"
|
||||
|
||||
const INITIAL_BACKEND_UI_STATE = {
|
||||
models: [],
|
||||
tables: [],
|
||||
views: [],
|
||||
users: [],
|
||||
selectedDatabase: {},
|
||||
selectedModel: {},
|
||||
draftModel: {},
|
||||
selectedTable: {},
|
||||
draftTable: {},
|
||||
}
|
||||
|
||||
export const getBackendUiStore = () => {
|
||||
|
@ -18,16 +18,16 @@ export const getBackendUiStore = () => {
|
|||
reset: () => store.set({ ...INITIAL_BACKEND_UI_STATE }),
|
||||
database: {
|
||||
select: async db => {
|
||||
const modelsResponse = await api.get(`/api/models`)
|
||||
const models = await modelsResponse.json()
|
||||
const tablesResponse = await api.get(`/api/tables`)
|
||||
const tables = await tablesResponse.json()
|
||||
store.update(state => {
|
||||
state.selectedDatabase = db
|
||||
state.models = models
|
||||
state.tables = tables
|
||||
return state
|
||||
})
|
||||
},
|
||||
},
|
||||
records: {
|
||||
rows: {
|
||||
save: () =>
|
||||
store.update(state => {
|
||||
state.selectedView = state.selectedView
|
||||
|
@ -38,56 +38,56 @@ export const getBackendUiStore = () => {
|
|||
state.selectedView = state.selectedView
|
||||
return state
|
||||
}),
|
||||
select: record =>
|
||||
select: row =>
|
||||
store.update(state => {
|
||||
state.selectedRecord = record
|
||||
state.selectedRow = row
|
||||
return state
|
||||
}),
|
||||
},
|
||||
models: {
|
||||
tables: {
|
||||
fetch: async () => {
|
||||
const modelsResponse = await api.get(`/api/models`)
|
||||
const models = await modelsResponse.json()
|
||||
const tablesResponse = await api.get(`/api/tables`)
|
||||
const tables = await tablesResponse.json()
|
||||
store.update(state => {
|
||||
state.models = models
|
||||
state.tables = tables
|
||||
return state
|
||||
})
|
||||
},
|
||||
select: model =>
|
||||
select: table =>
|
||||
store.update(state => {
|
||||
state.selectedModel = model
|
||||
state.draftModel = cloneDeep(model)
|
||||
state.selectedView = { name: `all_${model._id}` }
|
||||
state.selectedTable = table
|
||||
state.draftTable = cloneDeep(table)
|
||||
state.selectedView = { name: `all_${table._id}` }
|
||||
return state
|
||||
}),
|
||||
save: async model => {
|
||||
const updatedModel = cloneDeep(model)
|
||||
save: async table => {
|
||||
const updatedTable = cloneDeep(table)
|
||||
|
||||
// update any renamed schema keys to reflect their names
|
||||
for (let key in updatedModel.schema) {
|
||||
const field = updatedModel.schema[key]
|
||||
for (let key in updatedTable.schema) {
|
||||
const field = updatedTable.schema[key]
|
||||
// field has been renamed
|
||||
if (field.name && field.name !== key) {
|
||||
updatedModel.schema[field.name] = field
|
||||
updatedModel._rename = { old: key, updated: field.name }
|
||||
delete updatedModel.schema[key]
|
||||
updatedTable.schema[field.name] = field
|
||||
updatedTable._rename = { old: key, updated: field.name }
|
||||
delete updatedTable.schema[key]
|
||||
}
|
||||
}
|
||||
|
||||
const SAVE_MODEL_URL = `/api/models`
|
||||
const response = await api.post(SAVE_MODEL_URL, updatedModel)
|
||||
const savedModel = await response.json()
|
||||
await store.actions.models.fetch()
|
||||
store.actions.models.select(savedModel)
|
||||
return savedModel
|
||||
const SAVE_TABLE_URL = `/api/tables`
|
||||
const response = await api.post(SAVE_TABLE_URL, updatedTable)
|
||||
const savedTable = await response.json()
|
||||
await store.actions.tables.fetch()
|
||||
store.actions.tables.select(savedTable)
|
||||
return savedTable
|
||||
},
|
||||
delete: async model => {
|
||||
await api.delete(`/api/models/${model._id}/${model._rev}`)
|
||||
delete: async table => {
|
||||
await api.delete(`/api/tables/${table._id}/${table._rev}`)
|
||||
store.update(state => {
|
||||
state.models = state.models.filter(
|
||||
existing => existing._id !== model._id
|
||||
state.tables = state.tables.filter(
|
||||
existing => existing._id !== table._id
|
||||
)
|
||||
state.selectedModel = {}
|
||||
state.selectedTable = {}
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -95,23 +95,23 @@ export const getBackendUiStore = () => {
|
|||
store.update(state => {
|
||||
// delete the original if renaming
|
||||
if (originalName) {
|
||||
delete state.draftModel.schema[originalName]
|
||||
state.draftModel._rename = {
|
||||
delete state.draftTable.schema[originalName]
|
||||
state.draftTable._rename = {
|
||||
old: originalName,
|
||||
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
|
||||
})
|
||||
},
|
||||
deleteField: field => {
|
||||
store.update(state => {
|
||||
delete state.draftModel.schema[field.name]
|
||||
store.actions.models.save(state.draftModel)
|
||||
delete state.draftTable.schema[field.name]
|
||||
store.actions.tables.save(state.draftTable)
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -120,12 +120,12 @@ export const getBackendUiStore = () => {
|
|||
select: view =>
|
||||
store.update(state => {
|
||||
state.selectedView = view
|
||||
state.selectedModel = {}
|
||||
state.selectedTable = {}
|
||||
return state
|
||||
}),
|
||||
delete: async view => {
|
||||
await api.delete(`/api/views/${view}`)
|
||||
await store.actions.models.fetch()
|
||||
await store.actions.tables.fetch()
|
||||
},
|
||||
save: async view => {
|
||||
const response = await api.post(`/api/views`, view)
|
||||
|
@ -137,14 +137,14 @@ export const getBackendUiStore = () => {
|
|||
}
|
||||
|
||||
store.update(state => {
|
||||
const viewModel = state.models.find(
|
||||
model => model._id === view.modelId
|
||||
const viewTable = state.tables.find(
|
||||
table => table._id === view.tableId
|
||||
)
|
||||
|
||||
if (view.originalName) delete viewModel.views[view.originalName]
|
||||
viewModel.views[view.name] = viewMeta
|
||||
if (view.originalName) delete viewTable.views[view.originalName]
|
||||
viewTable.views[view.name] = viewMeta
|
||||
|
||||
state.models = state.models
|
||||
state.tables = state.tables
|
||||
state.selectedView = viewMeta
|
||||
return state
|
||||
})
|
||||
|
|
|
@ -15,7 +15,7 @@ const createScreen = () => ({
|
|||
},
|
||||
_children: [],
|
||||
_instanceName: "",
|
||||
model: "",
|
||||
table: "",
|
||||
},
|
||||
route: "",
|
||||
name: "screen-id",
|
|
@ -15,7 +15,7 @@ const createScreen = () => ({
|
|||
},
|
||||
_children: [],
|
||||
_instanceName: "",
|
||||
model: "",
|
||||
table: "",
|
||||
},
|
||||
route: "",
|
||||
name: "screen-id",
|
|
@ -1,19 +1,19 @@
|
|||
import newRecordScreen from "./newRecordScreen"
|
||||
import recordDetailScreen from "./recordDetailScreen"
|
||||
import recordListScreen from "./recordListScreen"
|
||||
import emptyNewRecordScreen from "./emptyNewRecordScreen"
|
||||
import newRowScreen from "./newRowScreen"
|
||||
import rowDetailScreen from "./rowDetailScreen"
|
||||
import rowListScreen from "./rowListScreen"
|
||||
import emptyNewRowScreen from "./emptyNewRowScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
import emptyRecordDetailScreen from "./emptyRecordDetailScreen"
|
||||
import emptyRowDetailScreen from "./emptyRowDetailScreen"
|
||||
import { generateNewIdsForComponent } from "../../storeUtils"
|
||||
import { uuid } from "builderStore/uuid"
|
||||
|
||||
const allTemplates = models => [
|
||||
const allTemplates = tables => [
|
||||
createFromScratchScreen,
|
||||
...newRecordScreen(models),
|
||||
...recordDetailScreen(models),
|
||||
...recordListScreen(models),
|
||||
emptyNewRecordScreen,
|
||||
emptyRecordDetailScreen,
|
||||
...newRowScreen(tables),
|
||||
...rowDetailScreen(tables),
|
||||
...rowListScreen(tables),
|
||||
emptyNewRowScreen,
|
||||
emptyRowDetailScreen,
|
||||
]
|
||||
|
||||
// allows us to apply common behaviour to all create() functions
|
||||
|
@ -28,8 +28,8 @@ const createTemplateOverride = (frontendState, create) => () => {
|
|||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, models) =>
|
||||
allTemplates(models).map(template => ({
|
||||
export default (frontendState, tables) =>
|
||||
allTemplates(tables).map(template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template.create),
|
||||
}))
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
export default function(models) {
|
||||
return models.map(model => {
|
||||
const fields = Object.keys(model.schema)
|
||||
export default function(tables) {
|
||||
return tables.map(table => {
|
||||
const fields = Object.keys(table.schema)
|
||||
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Add Row"
|
||||
return {
|
||||
name: `${model.name} - New`,
|
||||
create: () => createScreen(model, heading),
|
||||
id: NEW_RECORD_TEMPLATE,
|
||||
name: `${table.name} - New`,
|
||||
create: () => createScreen(table, heading),
|
||||
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: {
|
||||
_id: "",
|
||||
_component: "@budibase/standard-components/newrow",
|
||||
|
@ -22,7 +22,7 @@ const createScreen = (model, heading) => ({
|
|||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
model: model._id,
|
||||
table: table._id,
|
||||
_children: [
|
||||
{
|
||||
_id: "",
|
||||
|
@ -50,7 +50,7 @@ const createScreen = (model, heading) => ({
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
_instanceName: `${model.name} Form`,
|
||||
_instanceName: `${table.name} Form`,
|
||||
_children: [],
|
||||
},
|
||||
{
|
||||
|
@ -91,7 +91,7 @@ const createScreen = (model, heading) => ({
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
url: `/${model.name.toLowerCase()}`,
|
||||
url: `/${table.name.toLowerCase()}`,
|
||||
},
|
||||
"##eventHandlerType": "Navigate To",
|
||||
},
|
||||
|
@ -116,9 +116,9 @@ const createScreen = (model, heading) => ({
|
|||
{
|
||||
parameters: {
|
||||
contextPath: "data",
|
||||
modelId: model._id,
|
||||
tableId: table._id,
|
||||
},
|
||||
"##eventHandlerType": "Save Record",
|
||||
"##eventHandlerType": "Save Row",
|
||||
},
|
||||
],
|
||||
_instanceName: "Save Button",
|
||||
|
@ -127,9 +127,9 @@ const createScreen = (model, heading) => ({
|
|||
],
|
||||
},
|
||||
],
|
||||
_instanceName: `${model.name} - New`,
|
||||
_instanceName: `${table.name} - New`,
|
||||
_code: "",
|
||||
},
|
||||
route: `/${model.name.toLowerCase()}/new`,
|
||||
route: `/${table.name.toLowerCase()}/new`,
|
||||
name: "",
|
||||
})
|
|
@ -1,18 +1,18 @@
|
|||
export default function(models) {
|
||||
return models.map(model => {
|
||||
const fields = Object.keys(model.schema)
|
||||
export default function(tables) {
|
||||
return tables.map(table => {
|
||||
const fields = Object.keys(table.schema)
|
||||
const heading = fields.length > 0 ? `{{ data.${fields[0]} }}` : "Detail"
|
||||
return {
|
||||
name: `${model.name} - Detail`,
|
||||
create: () => createScreen(model, heading),
|
||||
id: RECORD_DETAIL_TEMPLATE,
|
||||
name: `${table.name} - Detail`,
|
||||
create: () => createScreen(table, heading),
|
||||
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: {
|
||||
_id: "",
|
||||
_component: "@budibase/standard-components/rowdetail",
|
||||
|
@ -22,7 +22,7 @@ const createScreen = (model, heading) => ({
|
|||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
model: model._id,
|
||||
table: table._id,
|
||||
_children: [
|
||||
{
|
||||
_id: "",
|
||||
|
@ -50,7 +50,7 @@ const createScreen = (model, heading) => ({
|
|||
selected: {},
|
||||
},
|
||||
_code: "",
|
||||
_instanceName: `${model.name} Form`,
|
||||
_instanceName: `${table.name} Form`,
|
||||
_children: [],
|
||||
},
|
||||
{
|
||||
|
@ -91,7 +91,7 @@ const createScreen = (model, heading) => ({
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
url: `/${model.name.toLowerCase()}`,
|
||||
url: `/${table.name.toLowerCase()}`,
|
||||
},
|
||||
"##eventHandlerType": "Navigate To",
|
||||
},
|
||||
|
@ -116,9 +116,9 @@ const createScreen = (model, heading) => ({
|
|||
{
|
||||
parameters: {
|
||||
contextPath: "data",
|
||||
modelId: model._id,
|
||||
tableId: table._id,
|
||||
},
|
||||
"##eventHandlerType": "Save Record",
|
||||
"##eventHandlerType": "Save Row",
|
||||
},
|
||||
],
|
||||
_instanceName: "Save Button",
|
||||
|
@ -127,9 +127,9 @@ const createScreen = (model, heading) => ({
|
|||
],
|
||||
},
|
||||
],
|
||||
_instanceName: `${model.name} - Detail`,
|
||||
_instanceName: `${table.name} - Detail`,
|
||||
_code: "",
|
||||
},
|
||||
route: `/${model.name.toLowerCase()}/:id`,
|
||||
route: `/${table.name.toLowerCase()}/:id`,
|
||||
name: "",
|
||||
})
|
|
@ -1,16 +1,16 @@
|
|||
export default function(models) {
|
||||
return models.map(model => {
|
||||
export default function(tables) {
|
||||
return tables.map(table => {
|
||||
return {
|
||||
name: `${model.name} - List`,
|
||||
create: () => createScreen(model),
|
||||
id: RECORD_LIST_TEMPLATE,
|
||||
name: `${table.name} - List`,
|
||||
create: () => createScreen(table),
|
||||
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: {
|
||||
_id: "",
|
||||
_component: "@budibase/standard-components/container",
|
||||
|
@ -53,7 +53,7 @@ const createScreen = model => ({
|
|||
},
|
||||
_code: "",
|
||||
className: "",
|
||||
text: `${model.name} List`,
|
||||
text: `${table.name} List`,
|
||||
type: "h1",
|
||||
_instanceName: "Heading 1",
|
||||
_children: [],
|
||||
|
@ -74,7 +74,7 @@ const createScreen = model => ({
|
|||
onClick: [
|
||||
{
|
||||
parameters: {
|
||||
url: `/${model.name}/new`,
|
||||
url: `/${table.name}/new`,
|
||||
},
|
||||
"##eventHandlerType": "Navigate To",
|
||||
},
|
||||
|
@ -96,19 +96,19 @@ const createScreen = model => ({
|
|||
_code: "",
|
||||
datasource: {
|
||||
label: "Deals",
|
||||
name: `all_${model._id}`,
|
||||
modelId: model._id,
|
||||
isModel: true,
|
||||
name: `all_${table._id}`,
|
||||
tableId: table._id,
|
||||
isTable: true,
|
||||
},
|
||||
_instanceName: `${model.name} Table`,
|
||||
_instanceName: `${table.name} Table`,
|
||||
_children: [],
|
||||
},
|
||||
],
|
||||
_instanceName: `${model.name} - List`,
|
||||
_instanceName: `${table.name} - List`,
|
||||
_code: "",
|
||||
className: "",
|
||||
onLoad: [],
|
||||
},
|
||||
route: `/${model.name.toLowerCase()}`,
|
||||
route: `/${table.name.toLowerCase()}`,
|
||||
name: "",
|
||||
})
|
|
@ -13,10 +13,10 @@
|
|||
|
||||
function enrichInputs(inputs) {
|
||||
let enrichedInputs = { ...inputs, enriched: {} }
|
||||
const modelId = inputs.modelId || inputs.record?.modelId
|
||||
if (modelId) {
|
||||
enrichedInputs.enriched.model = $backendUiStore.models.find(
|
||||
model => model._id === modelId
|
||||
const tableId = inputs.tableId || inputs.row?.tableId
|
||||
if (tableId) {
|
||||
enrichedInputs.enriched.table = $backendUiStore.tables.find(
|
||||
table => table._id === tableId
|
||||
)
|
||||
}
|
||||
return enrichedInputs
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import TableSelector from "./ParamInputs/TableSelector.svelte"
|
||||
import RowSelector from "./ParamInputs/RowSelector.svelte"
|
||||
import { Input, TextArea, Select, Label } from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import BindableInput from "../../userInterface/BindableInput.svelte"
|
||||
|
@ -60,10 +60,10 @@
|
|||
</Select>
|
||||
{:else if value.customType === 'password'}
|
||||
<Input type="password" thin bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'model'}
|
||||
<ModelSelector bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'record'}
|
||||
<RecordSelector bind:value={block.inputs[key]} {bindings} />
|
||||
{:else if value.customType === 'table'}
|
||||
<TableSelector bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'row'}
|
||||
<RowSelector bind:value={block.inputs[key]} {bindings} />
|
||||
{:else if value.type === 'string' || value.type === 'number'}
|
||||
<BindableInput
|
||||
type="string"
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
export let value
|
||||
export let bindings
|
||||
|
||||
$: model = $backendUiStore.models.find(model => model._id === value?.modelId)
|
||||
$: schemaFields = Object.entries(model?.schema ?? {})
|
||||
$: table = $backendUiStore.tables.find(table => table._id === value?.tableId)
|
||||
$: 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
|
||||
$: if (value?.modelId == null) value = { modelId: "" }
|
||||
$: if (value?.tableId == null) value = { tableId: "" }
|
||||
|
||||
function schemaHasOptions(schema) {
|
||||
return !!schema.constraints?.inclusion?.length
|
||||
|
@ -19,10 +19,10 @@
|
|||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<Select bind:value={value.modelId} thin secondary>
|
||||
<Select bind:value={value.tableId} thin secondary>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{#each $backendUiStore.tables as table}
|
||||
<option value={table._id}>{table.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
|
@ -8,8 +8,8 @@
|
|||
<div class="block-field">
|
||||
<Select bind:value secondary thin>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{#each $backendUiStore.tables as table}
|
||||
<option value={table._id}>{table.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
|
@ -10,19 +10,19 @@
|
|||
let data = []
|
||||
let loading = false
|
||||
|
||||
$: title = $backendUiStore.selectedModel.name
|
||||
$: schema = $backendUiStore.selectedModel.schema
|
||||
$: modelView = {
|
||||
$: title = $backendUiStore.selectedTable.name
|
||||
$: schema = $backendUiStore.selectedTable.schema
|
||||
$: tableView = {
|
||||
schema,
|
||||
name: $backendUiStore.selectedView.name,
|
||||
}
|
||||
|
||||
// Fetch records for specified model
|
||||
// Fetch rows for specified table
|
||||
$: {
|
||||
if ($backendUiStore.selectedView?.name?.startsWith("all_")) {
|
||||
loading = true
|
||||
api.fetchDataForView($backendUiStore.selectedView).then(records => {
|
||||
data = records || []
|
||||
api.fetchDataForView($backendUiStore.selectedView).then(rows => {
|
||||
data = rows || []
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
|
@ -34,6 +34,6 @@
|
|||
{#if Object.keys(schema).length > 0}
|
||||
<CreateRowButton />
|
||||
<CreateViewButton />
|
||||
<ExportButton view={modelView} />
|
||||
<ExportButton view={tableView} />
|
||||
{/if}
|
||||
</Table>
|
|
@ -4,37 +4,37 @@
|
|||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let modelId
|
||||
export let recordId
|
||||
export let tableId
|
||||
export let rowId
|
||||
export let fieldName
|
||||
|
||||
let record
|
||||
let row
|
||||
let title
|
||||
|
||||
$: data = record?.[fieldName] ?? []
|
||||
$: linkedModelId = data?.length ? data[0].modelId : null
|
||||
$: linkedModel = $backendUiStore.models.find(
|
||||
model => model._id === linkedModelId
|
||||
$: data = row?.[fieldName] ?? []
|
||||
$: linkedTableId = data?.length ? data[0].tableId : null
|
||||
$: linkedTable = $backendUiStore.tables.find(
|
||||
table => table._id === linkedTableId
|
||||
)
|
||||
$: schema = linkedModel?.schema
|
||||
$: model = $backendUiStore.models.find(model => model._id === modelId)
|
||||
$: fetchData(modelId, recordId)
|
||||
$: schema = linkedTable?.schema
|
||||
$: table = $backendUiStore.tables.find(table => table._id === tableId)
|
||||
$: fetchData(tableId, rowId)
|
||||
$: {
|
||||
let recordLabel = record?.[model?.primaryDisplay]
|
||||
if (recordLabel) {
|
||||
title = `${recordLabel} - ${fieldName}`
|
||||
let rowLabel = row?.[table?.primaryDisplay]
|
||||
if (rowLabel) {
|
||||
title = `${rowLabel} - ${fieldName}`
|
||||
} else {
|
||||
title = fieldName
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData(modelId, recordId) {
|
||||
const QUERY_VIEW_URL = `/api/${modelId}/${recordId}/enrich`
|
||||
async function fetchData(tableId, rowId) {
|
||||
const QUERY_VIEW_URL = `/api/${tableId}/${rowId}/enrich`
|
||||
const response = await api.get(QUERY_VIEW_URL)
|
||||
record = await response.json()
|
||||
row = await response.json()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if record && record._id === recordId}
|
||||
{#if row && row._id === rowId}
|
||||
<Table {title} {schema} {data} />
|
||||
{/if}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { Input, Select, Label, DatePicker, Toggle } from "@budibase/bbui"
|
||||
import Dropzone from "components/common/Dropzone.svelte"
|
||||
import { capitalise } from "../../../helpers"
|
||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
|
||||
export let meta
|
||||
export let value = meta.type === "boolean" ? false : ""
|
||||
|
@ -28,7 +28,7 @@
|
|||
{:else if type === 'boolean'}
|
||||
<Toggle text={label} bind:checked={value} data-cy="{meta.name}-input" />
|
||||
{:else if type === 'link'}
|
||||
<LinkedRecordSelector bind:linkedRecords={value} schema={meta} />
|
||||
<LinkedRowSelector bind:linkedRows={value} schema={meta} />
|
||||
{:else}
|
||||
<Input thin {label} data-cy="{meta.name}-input" {type} bind:value />
|
||||
{/if}
|
|
@ -10,7 +10,7 @@
|
|||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import AttachmentList from "./AttachmentList.svelte"
|
||||
import TablePagination from "./TablePagination.svelte"
|
||||
import CreateEditRecordModal from "./modals/CreateEditRecordModal.svelte"
|
||||
import CreateEditRowModal from "./modals/CreateEditRowModal.svelte"
|
||||
import RowPopover from "./buttons/CreateRowButton.svelte"
|
||||
import ColumnPopover from "./buttons/CreateColumnButton.svelte"
|
||||
import ViewPopover from "./buttons/CreateViewButton.svelte"
|
||||
|
@ -39,14 +39,14 @@
|
|||
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) {
|
||||
if (!record?.[fieldName]?.length) {
|
||||
function selectRelationship(row, fieldName) {
|
||||
if (!row?.[fieldName]?.length) {
|
||||
return
|
||||
}
|
||||
$goto(
|
||||
`/${$params.application}/backend/model/${modelId}/relationship/${record._id}/${fieldName}`
|
||||
`/${$params.application}/backend/table/${tableId}/relationship/${row._id}/${fieldName}`
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
$: name = view.name
|
||||
|
||||
// Fetch records for specified view
|
||||
// Fetch rows for specified view
|
||||
$: {
|
||||
if (!name.startsWith("all_")) {
|
||||
fetchViewData(name, view.field, view.groupBy)
|
||||
|
|
|
@ -6,22 +6,22 @@ export async function createUser(user) {
|
|||
return await response.json()
|
||||
}
|
||||
|
||||
export async function saveRecord(record, modelId) {
|
||||
const SAVE_RECORDS_URL = `/api/${modelId}/records`
|
||||
const response = await api.post(SAVE_RECORDS_URL, record)
|
||||
export async function saveRow(row, tableId) {
|
||||
const SAVE_ROWS_URL = `/api/${tableId}/rows`
|
||||
const response = await api.post(SAVE_ROWS_URL, row)
|
||||
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
export async function deleteRecord(record) {
|
||||
const DELETE_RECORDS_URL = `/api/${record.modelId}/records/${record._id}/${record._rev}`
|
||||
const response = await api.delete(DELETE_RECORDS_URL)
|
||||
export async function deleteRow(row) {
|
||||
const DELETE_ROWS_URL = `/api/${row.tableId}/rows/${row._id}/${row._rev}`
|
||||
const response = await api.delete(DELETE_ROWS_URL)
|
||||
return response
|
||||
}
|
||||
|
||||
export async function fetchDataForView(view) {
|
||||
const FETCH_RECORDS_URL = `/api/views/${view.name}`
|
||||
const FETCH_ROWS_URL = `/api/views/${view.name}`
|
||||
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
const response = await api.get(FETCH_ROWS_URL)
|
||||
return await response.json()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { TextButton as Button, Icon, Modal } from "@budibase/bbui"
|
||||
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
|
||||
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
@ -12,5 +12,5 @@
|
|||
</Button>
|
||||
</div>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditRecordModal />
|
||||
<CreateEditRowModal />
|
||||
</Modal>
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import RecordFieldControl from "../RecordFieldControl.svelte"
|
||||
import * as api from "../api"
|
||||
import { ModalContent } from "@budibase/bbui"
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
|
||||
export let record = {}
|
||||
|
||||
let errors = []
|
||||
|
||||
$: creating = record?._id == null
|
||||
$: model = record.modelId
|
||||
? $backendUiStore.models.find(model => model._id === record?.modelId)
|
||||
: $backendUiStore.selectedModel
|
||||
$: modelSchema = Object.entries(model?.schema ?? {})
|
||||
|
||||
async function saveRecord() {
|
||||
const recordResponse = await api.saveRecord(
|
||||
{ ...record, modelId: model._id },
|
||||
model._id
|
||||
)
|
||||
if (recordResponse.errors) {
|
||||
errors = Object.keys(recordResponse.errors)
|
||||
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
|
||||
.flat()
|
||||
// Prevent modal closing if there were errors
|
||||
return false
|
||||
}
|
||||
notifier.success("Record saved successfully.")
|
||||
backendUiStore.actions.records.save(recordResponse)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={creating ? 'Create Row' : 'Edit Row'}
|
||||
confirmText={creating ? 'Create Row' : 'Save Row'}
|
||||
onConfirm={saveRecord}>
|
||||
<ErrorsBox {errors} />
|
||||
{#each modelSchema as [key, meta]}
|
||||
<div>
|
||||
<RecordFieldControl {meta} bind:value={record[key]} />
|
||||
</div>
|
||||
{/each}
|
||||
</ModalContent>
|
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import RowFieldControl from "../RowFieldControl.svelte"
|
||||
import * as api from "../api"
|
||||
import { ModalContent } from "@budibase/bbui"
|
||||
import ErrorsBox from "components/common/ErrorsBox.svelte"
|
||||
|
||||
export let row = {}
|
||||
|
||||
let errors = []
|
||||
|
||||
$: creating = row?._id == null
|
||||
$: table = row.tableId
|
||||
? $backendUiStore.tables.find(table => table._id === row?.tableId)
|
||||
: $backendUiStore.selectedTable
|
||||
$: tableSchema = Object.entries(table?.schema ?? {})
|
||||
|
||||
async function saveRow() {
|
||||
const rowResponse = await api.saveRow(
|
||||
{ ...row, tableId: table._id },
|
||||
table._id
|
||||
)
|
||||
if (rowResponse.errors) {
|
||||
errors = Object.keys(rowResponse.errors)
|
||||
.map(k => ({ dataPath: k, message: rowResponse.errors[k] }))
|
||||
.flat()
|
||||
// Prevent modal closing if there were errors
|
||||
return false
|
||||
}
|
||||
notifier.success("Row saved successfully.")
|
||||
backendUiStore.actions.rows.save(rowResponse)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title={creating ? 'Create Row' : 'Edit Row'}
|
||||
confirmText={creating ? 'Create Row' : 'Save Row'}
|
||||
onConfirm={saveRow}>
|
||||
<ErrorsBox {errors} />
|
||||
{#each tableSchema as [key, meta]}
|
||||
<div>
|
||||
<RowFieldControl {meta} bind:value={row[key]} />
|
||||
</div>
|
||||
{/each}
|
||||
</ModalContent>
|
|
@ -14,13 +14,13 @@
|
|||
export let view = {}
|
||||
export let onClosed
|
||||
|
||||
$: viewModel = $backendUiStore.models.find(
|
||||
({ _id }) => _id === $backendUiStore.selectedView.modelId
|
||||
$: viewTable = $backendUiStore.tables.find(
|
||||
({ _id }) => _id === $backendUiStore.selectedView.tableId
|
||||
)
|
||||
$: fields =
|
||||
viewModel &&
|
||||
Object.keys(viewModel.schema).filter(
|
||||
field => viewModel.schema[field].type === "number"
|
||||
viewTable &&
|
||||
Object.keys(viewTable.schema).filter(
|
||||
field => viewTable.schema[field].type === "number"
|
||||
)
|
||||
|
||||
function saveView() {
|
||||
|
|
|
@ -32,10 +32,10 @@
|
|||
}
|
||||
|
||||
function deleteColumn() {
|
||||
if (field.name === $backendUiStore.selectedModel.primaryDisplay) {
|
||||
if (field.name === $backendUiStore.selectedTable.primaryDisplay) {
|
||||
notifier.danger("You cannot delete the primary display column")
|
||||
} else {
|
||||
backendUiStore.actions.models.deleteField(field)
|
||||
backendUiStore.actions.tables.deleteField(field)
|
||||
notifier.success("Column deleted")
|
||||
}
|
||||
hideEditor()
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
import Checkbox from "components/common/Checkbox.svelte"
|
||||
import ActionButton from "components/common/ActionButton.svelte"
|
||||
import DatePicker from "components/common/DatePicker.svelte"
|
||||
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
|
||||
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
|
||||
import * as api from "../api"
|
||||
|
||||
let fieldDefinitions = cloneDeep(FIELDS)
|
||||
|
@ -31,14 +31,14 @@
|
|||
}
|
||||
|
||||
let originalName = field.name
|
||||
$: modelOptions = $backendUiStore.models.filter(
|
||||
model => model._id !== $backendUiStore.draftModel._id
|
||||
$: tableOptions = $backendUiStore.tables.filter(
|
||||
table => table._id !== $backendUiStore.draftTable._id
|
||||
)
|
||||
$: required = !!field?.constraints?.presence
|
||||
|
||||
async function saveColumn() {
|
||||
backendUiStore.update(state => {
|
||||
backendUiStore.actions.models.saveField({
|
||||
backendUiStore.actions.tables.saveField({
|
||||
originalName,
|
||||
field,
|
||||
})
|
||||
|
@ -111,10 +111,10 @@
|
|||
label="Max Value"
|
||||
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
|
||||
{: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>
|
||||
{#each modelOptions as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{#each tableOptions as table}
|
||||
<option value={table._id}>{table.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
<Input
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
let name
|
||||
let field
|
||||
|
||||
$: fields = Object.keys($backendUiStore.selectedModel.schema).filter(key => {
|
||||
return $backendUiStore.selectedModel.schema[key].type === "number"
|
||||
$: fields = Object.keys($backendUiStore.selectedTable.schema).filter(key => {
|
||||
return $backendUiStore.selectedTable.schema[key].type === "number"
|
||||
})
|
||||
$: views = $backendUiStore.models.flatMap(model =>
|
||||
Object.keys(model.views || {})
|
||||
$: views = $backendUiStore.tables.flatMap(table =>
|
||||
Object.keys(table.views || {})
|
||||
)
|
||||
|
||||
function saveView() {
|
||||
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
backendUiStore.actions.views.save({
|
||||
name,
|
||||
modelId: $backendUiStore.selectedModel._id,
|
||||
tableId: $backendUiStore.selectedTable._id,
|
||||
field,
|
||||
})
|
||||
notifier.success(`View ${name} created`)
|
||||
|
|
|
@ -45,10 +45,10 @@
|
|||
export let view = {}
|
||||
export let onClosed
|
||||
|
||||
$: viewModel = $backendUiStore.models.find(
|
||||
({ _id }) => _id === $backendUiStore.selectedView.modelId
|
||||
$: viewTable = $backendUiStore.tables.find(
|
||||
({ _id }) => _id === $backendUiStore.selectedView.tableId
|
||||
)
|
||||
$: fields = viewModel && Object.keys(viewModel.schema)
|
||||
$: fields = viewTable && Object.keys(viewTable.schema)
|
||||
|
||||
function saveView() {
|
||||
backendUiStore.actions.views.save(view)
|
||||
|
@ -71,25 +71,25 @@
|
|||
|
||||
function isMultipleChoice(field) {
|
||||
return (
|
||||
(viewModel.schema[field].constraints &&
|
||||
viewModel.schema[field].constraints.inclusion &&
|
||||
viewModel.schema[field].constraints.inclusion.length) ||
|
||||
viewModel.schema[field].type === "boolean"
|
||||
(viewTable.schema[field].constraints &&
|
||||
viewTable.schema[field].constraints.inclusion &&
|
||||
viewTable.schema[field].constraints.inclusion.length) ||
|
||||
viewTable.schema[field].type === "boolean"
|
||||
)
|
||||
}
|
||||
|
||||
function fieldOptions(field) {
|
||||
return viewModel.schema[field].type === "options"
|
||||
? viewModel.schema[field].constraints.inclusion
|
||||
return viewTable.schema[field].type === "options"
|
||||
? viewTable.schema[field].constraints.inclusion
|
||||
: [true, false]
|
||||
}
|
||||
|
||||
function isDate(field) {
|
||||
return viewModel.schema[field].type === "datetime"
|
||||
return viewTable.schema[field].type === "datetime"
|
||||
}
|
||||
|
||||
function isNumber(field) {
|
||||
return viewModel.schema[field].type === "number"
|
||||
return viewTable.schema[field].type === "number"
|
||||
}
|
||||
|
||||
const fieldChanged = filter => ev => {
|
||||
|
@ -97,8 +97,8 @@
|
|||
if (
|
||||
filter.key &&
|
||||
ev.target.value &&
|
||||
viewModel.schema[filter.key].type !==
|
||||
viewModel.schema[ev.target.value].type
|
||||
viewTable.schema[filter.key].type !==
|
||||
viewTable.schema[ev.target.value].type
|
||||
) {
|
||||
filter.value = ""
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
export let view = {}
|
||||
export let onClosed
|
||||
|
||||
$: viewModel = $backendUiStore.models.find(
|
||||
({ _id }) => _id === $backendUiStore.selectedView.modelId
|
||||
$: viewTable = $backendUiStore.tables.find(
|
||||
({ _id }) => _id === $backendUiStore.selectedView.tableId
|
||||
)
|
||||
$: fields = viewModel && Object.keys(viewModel.schema)
|
||||
$: fields = viewTable && Object.keys(viewTable.schema)
|
||||
|
||||
function saveView() {
|
||||
backendUiStore.actions.views.save(view)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { DropdownMenu, Icon, Modal } from "@budibase/bbui"
|
||||
import CreateEditRecordModal from "../modals/CreateEditRecordModal.svelte"
|
||||
import CreateEditRowModal from "../modals/CreateEditRowModal.svelte"
|
||||
import * as api from "../api"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
@ -24,9 +24,9 @@
|
|||
}
|
||||
|
||||
async function deleteRow() {
|
||||
await api.deleteRecord(row)
|
||||
notifier.success("Record deleted")
|
||||
backendUiStore.actions.records.delete(row)
|
||||
await api.deleteRow(row)
|
||||
notifier.success("Row deleted")
|
||||
backendUiStore.actions.rows.delete(row)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -52,7 +52,7 @@
|
|||
onOk={deleteRow}
|
||||
title="Confirm Delete" />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateEditRecordModal record={row} />
|
||||
<CreateEditRowModal {row} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
data-cy="model-nav-item"
|
||||
data-cy="table-nav-item"
|
||||
class:indented
|
||||
class:selected
|
||||
on:click
|
|
@ -21,28 +21,28 @@
|
|||
!schema || Object.keys(schema).every(column => schema[column].success)
|
||||
$: dataImport = {
|
||||
valid,
|
||||
schema: buildModelSchema(schema),
|
||||
schema: buildTableSchema(schema),
|
||||
path: files[0] && files[0].path,
|
||||
}
|
||||
|
||||
function buildModelSchema(schema) {
|
||||
const modelSchema = {}
|
||||
function buildTableSchema(schema) {
|
||||
const tableSchema = {}
|
||||
for (let key in schema) {
|
||||
const type = schema[key].type
|
||||
|
||||
if (type === "omit") continue
|
||||
|
||||
modelSchema[key] = {
|
||||
tableSchema[key] = {
|
||||
name: key,
|
||||
type,
|
||||
constraints: FIELDS[type.toUpperCase()].constraints,
|
||||
}
|
||||
}
|
||||
return modelSchema
|
||||
return tableSchema
|
||||
}
|
||||
|
||||
async function validateCSV() {
|
||||
const response = await api.post("/api/models/csv/validate", {
|
||||
const response = await api.post("/api/tables/csv/validate", {
|
||||
file: files[0],
|
||||
schema: schema || {},
|
||||
})
|
|
@ -11,9 +11,9 @@
|
|||
$: selectedView =
|
||||
$backendUiStore.selectedView && $backendUiStore.selectedView.name
|
||||
|
||||
function selectModel(model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
$goto(`./model/${model._id}`)
|
||||
function selectTable(table) {
|
||||
backendUiStore.actions.tables.select(table)
|
||||
$goto(`./table/${table._id}`)
|
||||
}
|
||||
|
||||
function selectView(view) {
|
||||
|
@ -30,15 +30,15 @@
|
|||
<Spacer medium />
|
||||
<CreateTableModal />
|
||||
<div class="hierarchy-items-container">
|
||||
{#each $backendUiStore.models as model}
|
||||
{#each $backendUiStore.tables as table}
|
||||
<ListItem
|
||||
selected={selectedView === `all_${model._id}`}
|
||||
title={model.name}
|
||||
selected={selectedView === `all_${table._id}`}
|
||||
title={table.name}
|
||||
icon="ri-table-fill"
|
||||
on:click={() => selectModel(model)}>
|
||||
<EditTablePopover table={model} />
|
||||
on:click={() => selectTable(table)}>
|
||||
<EditTablePopover {table} />
|
||||
</ListItem>
|
||||
{#each Object.keys(model.views || {}) as viewName}
|
||||
{#each Object.keys(table.views || {}) as viewName}
|
||||
<ListItem
|
||||
indented
|
||||
selected={selectedView === viewName}
|
||||
|
@ -46,10 +46,10 @@
|
|||
icon="ri-eye-line"
|
||||
on:click={() => (selectedView === viewName ? {} : selectView({
|
||||
name: viewName,
|
||||
...model.views[viewName],
|
||||
...table.views[viewName],
|
||||
}))}>
|
||||
<EditViewPopover
|
||||
view={{ name: viewName, ...model.views[viewName] }} />
|
||||
view={{ name: viewName, ...table.views[viewName] }} />
|
||||
</ListItem>
|
||||
{/each}
|
||||
{/each}
|
|
@ -7,14 +7,14 @@
|
|||
import TableDataImport from "../TableDataImport.svelte"
|
||||
import analytics from "analytics"
|
||||
import screenTemplates from "builderStore/store/screenTemplates"
|
||||
import { NEW_RECORD_TEMPLATE } from "builderStore/store/screenTemplates/newRecordScreen"
|
||||
import { RECORD_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/recordDetailScreen"
|
||||
import { RECORD_LIST_TEMPLATE } from "builderStore/store/screenTemplates/recordListScreen"
|
||||
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
|
||||
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
|
||||
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
|
||||
|
||||
const defaultScreens = [
|
||||
NEW_RECORD_TEMPLATE,
|
||||
RECORD_DETAIL_TEMPLATE,
|
||||
RECORD_LIST_TEMPLATE,
|
||||
NEW_ROW_TEMPLATE,
|
||||
ROW_DETAIL_TEMPLATE,
|
||||
ROW_LIST_TEMPLATE,
|
||||
]
|
||||
|
||||
let modal
|
||||
|
@ -27,16 +27,16 @@
|
|||
}
|
||||
|
||||
async function saveTable() {
|
||||
const model = await backendUiStore.actions.models.save({
|
||||
const table = await backendUiStore.actions.tables.save({
|
||||
name,
|
||||
schema: dataImport.schema || {},
|
||||
dataImport,
|
||||
})
|
||||
notifier.success(`Table ${name} created successfully.`)
|
||||
$goto(`./model/${model._id}`)
|
||||
$goto(`./table/${table._id}`)
|
||||
analytics.captureEvent("Table Created", { name })
|
||||
|
||||
const screens = screenTemplates($store, [model])
|
||||
const screens = screenTemplates($store, [table])
|
||||
.filter(template => defaultScreens.includes(template.id))
|
||||
.map(template => template.create())
|
||||
|
||||
|
@ -46,9 +46,9 @@
|
|||
} catch (_) {
|
||||
// TODO: this is temporary
|
||||
// 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)
|
||||
// does not have NewRecord
|
||||
// does not have NewRow
|
||||
// we should remove this after this has been released
|
||||
}
|
||||
}
|
|
@ -29,13 +29,13 @@
|
|||
}
|
||||
|
||||
async function deleteTable() {
|
||||
await backendUiStore.actions.models.delete(table)
|
||||
await backendUiStore.actions.tables.delete(table)
|
||||
notifier.success("Table deleted")
|
||||
hideEditor()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
await backendUiStore.actions.models.save(table)
|
||||
await backendUiStore.actions.tables.save(table)
|
||||
notifier.success("Table renamed successfully")
|
||||
hideEditor()
|
||||
}
|
|
@ -39,10 +39,10 @@
|
|||
|
||||
async function deleteView() {
|
||||
const name = view.name
|
||||
const id = view.modelId
|
||||
const id = view.tableId
|
||||
await backendUiStore.actions.views.delete(name)
|
||||
notifier.success("View deleted")
|
||||
$goto(`./model/${id}`)
|
||||
$goto(`./table/${id}`)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Select, Label, Multiselect } from "@budibase/bbui"
|
||||
import { capitalise } from "../../helpers"
|
||||
|
||||
export let schema
|
||||
export let linkedRecords = []
|
||||
|
||||
let records = []
|
||||
|
||||
$: label = capitalise(schema.name)
|
||||
$: linkedModelId = schema.modelId
|
||||
$: linkedModel = $backendUiStore.models.find(
|
||||
model => model._id === linkedModelId
|
||||
)
|
||||
$: fetchRecords(linkedModelId)
|
||||
|
||||
async function fetchRecords(linkedModelId) {
|
||||
const FETCH_RECORDS_URL = `/api/${linkedModelId}/records`
|
||||
try {
|
||||
const response = await api.get(FETCH_RECORDS_URL)
|
||||
records = await response.json()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
records = []
|
||||
}
|
||||
}
|
||||
|
||||
function getPrettyName(record) {
|
||||
return record[linkedModel.primaryDisplay || "_id"]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if linkedModel.primaryDisplay == null}
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
<Label small black>
|
||||
Please choose a primary display column for the
|
||||
<b>{linkedModel.name}</b>
|
||||
table.
|
||||
</Label>
|
||||
{:else}
|
||||
<Multiselect
|
||||
secondary
|
||||
bind:value={linkedRecords}
|
||||
{label}
|
||||
placeholder="Choose some options">
|
||||
{#each records as record}
|
||||
<option value={record._id}>{getPrettyName(record)}</option>
|
||||
{/each}
|
||||
</Multiselect>
|
||||
{/if}
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
import api from "builderStore/api"
|
||||
import { Select, Label, Multiselect } from "@budibase/bbui"
|
||||
import { capitalise } from "../../helpers"
|
||||
|
||||
export let schema
|
||||
export let linkedRows = []
|
||||
|
||||
let rows = []
|
||||
|
||||
$: label = capitalise(schema.name)
|
||||
$: linkedTableId = schema.tableId
|
||||
$: linkedTable = $backendUiStore.tables.find(
|
||||
table => table._id === linkedTableId
|
||||
)
|
||||
$: fetchRows(linkedTableId)
|
||||
|
||||
async function fetchRows(linkedTableId) {
|
||||
const FETCH_ROWS_URL = `/api/${linkedTableId}/rows`
|
||||
try {
|
||||
const response = await api.get(FETCH_ROWS_URL)
|
||||
rows = await response.json()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
rows = []
|
||||
}
|
||||
}
|
||||
|
||||
function getPrettyName(row) {
|
||||
return row[linkedTable.primaryDisplay || "_id"]
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if linkedTable.primaryDisplay == null}
|
||||
<Label extraSmall grey>{label}</Label>
|
||||
<Label small black>
|
||||
Please choose a primary display column for the
|
||||
<b>{linkedTable.name}</b>
|
||||
table.
|
||||
</Label>
|
||||
{:else}
|
||||
<Multiselect
|
||||
secondary
|
||||
bind:value={linkedRows}
|
||||
{label}
|
||||
placeholder="Choose some options">
|
||||
{#each rows as row}
|
||||
<option value={row._id}>{getPrettyName(row)}</option>
|
||||
{/each}
|
||||
</Multiselect>
|
||||
{/if}
|
|
@ -10,23 +10,23 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
|
||||
// just wraps binding in {{ ... }}
|
||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||
|
||||
const modelFields = modelId => {
|
||||
const model = $backendUiStore.models.find(m => m._id === modelId)
|
||||
const tableFields = tableId => {
|
||||
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,
|
||||
type: model.schema[k].type,
|
||||
type: table.schema[k].type,
|
||||
}))
|
||||
}
|
||||
|
||||
$: schemaFields =
|
||||
parameters && parameters.modelId ? modelFields(parameters.modelId) : []
|
||||
parameters && parameters.tableId ? tableFields(parameters.tableId) : []
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = e.detail
|
||||
|
@ -35,14 +35,14 @@
|
|||
|
||||
<div class="root">
|
||||
<Label size="m" color="dark">Table</Label>
|
||||
<Select secondary bind:value={parameters.modelId}>
|
||||
<Select secondary bind:value={parameters.tableId}>
|
||||
<option value="" />
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{#each $backendUiStore.tables as table}
|
||||
<option value={table._id}>{table.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
|
||||
{#if parameters.modelId}
|
||||
{#if parameters.tableId}
|
||||
<SaveFields
|
||||
parameterFields={parameters.fields}
|
||||
{schemaFields}
|
|
@ -35,7 +35,7 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
|
||||
const addField = () => {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
runtimeToReadableBinding,
|
||||
} 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
|
||||
export let parameters
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
|
||||
$: {
|
||||
|
@ -65,18 +65,18 @@
|
|||
|
||||
const component = $store.components[instance._component]
|
||||
|
||||
// component.context is the name of the prop that holds the modelId
|
||||
const modelInfo = instance[component.context]
|
||||
const modelId =
|
||||
typeof modelInfo === "string" ? modelInfo : modelInfo.modelId
|
||||
// component.context is the name of the prop that holds the tableId
|
||||
const tableInfo = instance[component.context]
|
||||
const tableId =
|
||||
typeof tableInfo === "string" ? tableInfo : tableInfo.tableId
|
||||
|
||||
if (!modelInfo) return []
|
||||
if (!tableInfo) return []
|
||||
|
||||
const model = $backendUiStore.models.find(m => m._id === modelId)
|
||||
parameters.modelId = modelId
|
||||
return Object.keys(model.schema).map(k => ({
|
||||
const table = $backendUiStore.tables.find(m => m._id === tableId)
|
||||
parameters.tableId = tableId
|
||||
return Object.keys(table.schema).map(k => ({
|
||||
name: k,
|
||||
type: model.schema[k].type,
|
||||
type: table.schema[k].type,
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -88,8 +88,8 @@
|
|||
<div class="root">
|
||||
{#if idFields.length === 0}
|
||||
<div class="cannot-use">
|
||||
Update record can only be used within a component that provides data, such
|
||||
as a List
|
||||
Update row can only be used within a component that provides data, such as
|
||||
a List
|
||||
</div>
|
||||
{:else}
|
||||
<Label size="m" color="dark">Datasource</Label>
|
|
@ -14,29 +14,29 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
|
||||
let idFields
|
||||
let recordId
|
||||
let rowId
|
||||
$: {
|
||||
idFields = bindableProperties.filter(
|
||||
bindable =>
|
||||
bindable.type === "context" && bindable.runtimeBinding.endsWith("._id")
|
||||
)
|
||||
// ensure recordId is always defaulted - there is usually only one option
|
||||
// ensure rowId is always defaulted - there is usually only one option
|
||||
if (idFields.length > 0 && !parameters._id) {
|
||||
recordId = idFields[0].runtimeBinding
|
||||
rowId = idFields[0].runtimeBinding
|
||||
parameters = parameters
|
||||
} else if (!recordId && parameters._id) {
|
||||
recordId = parameters._id
|
||||
} else if (!rowId && parameters._id) {
|
||||
rowId = parameters._id
|
||||
.replace("{{", "")
|
||||
.replace("}}", "")
|
||||
.trim()
|
||||
}
|
||||
}
|
||||
|
||||
$: parameters._id = `{{ ${recordId} }}`
|
||||
$: parameters._id = `{{ ${rowId} }}`
|
||||
|
||||
// just wraps binding in {{ ... }}
|
||||
const toBindingExpression = bindingPath => `{{ ${bindingPath} }}`
|
||||
|
@ -44,11 +44,11 @@
|
|||
// finds the selected idBinding, then reads the table/view
|
||||
// from the component instance that it belongs to.
|
||||
// then returns the field names for that schema
|
||||
const schemaFromIdBinding = recordId => {
|
||||
if (!recordId) return []
|
||||
const schemaFromIdBinding = rowId => {
|
||||
if (!rowId) return []
|
||||
|
||||
const idBinding = bindableProperties.find(
|
||||
prop => prop.runtimeBinding === recordId
|
||||
prop => prop.runtimeBinding === rowId
|
||||
)
|
||||
if (!idBinding) return []
|
||||
|
||||
|
@ -56,23 +56,23 @@
|
|||
|
||||
const component = $store.components[instance._component]
|
||||
|
||||
// component.context is the name of the prop that holds the modelId
|
||||
const modelInfo = instance[component.context]
|
||||
// component.context is the name of the prop that holds the tableId
|
||||
const tableInfo = instance[component.context]
|
||||
|
||||
if (!modelInfo) return []
|
||||
if (!tableInfo) return []
|
||||
|
||||
const model = $backendUiStore.models.find(m => m._id === modelInfo.modelId)
|
||||
parameters.modelId = modelInfo.modelId
|
||||
return Object.keys(model.schema).map(k => ({
|
||||
const table = $backendUiStore.tables.find(m => m._id === tableInfo.tableId)
|
||||
parameters.tableId = tableInfo.tableId
|
||||
return Object.keys(table.schema).map(k => ({
|
||||
name: k,
|
||||
type: model.schema[k].type,
|
||||
type: table.schema[k].type,
|
||||
}))
|
||||
}
|
||||
|
||||
let schemaFields
|
||||
$: {
|
||||
if (parameters && recordId) {
|
||||
schemaFields = schemaFromIdBinding(recordId)
|
||||
if (parameters && rowId) {
|
||||
schemaFields = schemaFromIdBinding(rowId)
|
||||
} else {
|
||||
schemaFields = []
|
||||
}
|
||||
|
@ -86,12 +86,12 @@
|
|||
<div class="root">
|
||||
{#if idFields.length === 0}
|
||||
<div class="cannot-use">
|
||||
Update record can only be used within a component that provides data, such
|
||||
as a List
|
||||
Update row can only be used within a component that provides data, such as
|
||||
a List
|
||||
</div>
|
||||
{:else}
|
||||
<Label size="m" color="dark">Record Id</Label>
|
||||
<Select secondary bind:value={recordId}>
|
||||
<Label size="m" color="dark">Row Id</Label>
|
||||
<Select secondary bind:value={rowId}>
|
||||
<option value="" />
|
||||
{#each idFields as idField}
|
||||
<option value={idField.runtimeBinding}>
|
||||
|
@ -101,7 +101,7 @@
|
|||
</Select>
|
||||
{/if}
|
||||
|
||||
{#if recordId}
|
||||
{#if rowId}
|
||||
<SaveFields
|
||||
parameterFields={parameters.fields}
|
||||
{schemaFields}
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
// the component is the setup panel for the action
|
||||
|
@ -8,8 +8,8 @@ import SaveRecord from "./SaveRecord.svelte"
|
|||
|
||||
export default [
|
||||
{
|
||||
name: "Save Record",
|
||||
component: SaveRecord,
|
||||
name: "Save Row",
|
||||
component: SaveRow,
|
||||
},
|
||||
{
|
||||
name: "Navigate To",
|
||||
|
|
|
@ -4568,8 +4568,8 @@ export default [
|
|||
label: "receipt",
|
||||
},
|
||||
{
|
||||
value: "fas fa-record-vinyl",
|
||||
label: "record-vinyl",
|
||||
value: "fas fa-row-vinyl",
|
||||
label: "row-vinyl",
|
||||
},
|
||||
{
|
||||
value: "fas fa-recycle",
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
let templateIndex
|
||||
let draftScreen
|
||||
|
||||
$: templates = getTemplates($store, $backendUiStore.models)
|
||||
$: templates = getTemplates($store, $backendUiStore.tables)
|
||||
|
||||
$: route = !route && $store.screens.length === 0 ? "*" : route
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
|
||||
const detailScreens = $store.screens.filter(screen =>
|
||||
|
@ -42,11 +42,11 @@
|
|||
if (
|
||||
p.type === "context" &&
|
||||
p.runtimeBinding.endsWith("._id") &&
|
||||
p.model
|
||||
p.table
|
||||
) {
|
||||
const modelId =
|
||||
typeof p.model === "string" ? p.model : p.model.modelId
|
||||
return modelId === detailScreen.props.model
|
||||
const tableId =
|
||||
typeof p.table === "string" ? p.table : p.table.tableId
|
||||
return tableId === detailScreen.props.table
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
<div>
|
||||
<Select thin secondary wide on:change {value}>
|
||||
<option value="">Choose a table</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{#each $backendUiStore.tables as table}
|
||||
<option value={table._id}>{table.name}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
</div>
|
|
@ -7,20 +7,20 @@
|
|||
export let value = ""
|
||||
export let onChange = (val = {})
|
||||
|
||||
const models = $backendUiStore.models
|
||||
const tables = $backendUiStore.tables
|
||||
|
||||
let options = []
|
||||
|
||||
$: model = componentInstance.datasource
|
||||
? models.find(m => m._id === componentInstance.datasource.modelId)
|
||||
$: table = componentInstance.datasource
|
||||
? tables.find(m => m._id === componentInstance.datasource.tableId)
|
||||
: null
|
||||
|
||||
$: type = componentInstance.datasource.type
|
||||
$: if (model) {
|
||||
$: if (table) {
|
||||
options =
|
||||
type === "model" || type === "link"
|
||||
? Object.keys(model.schema)
|
||||
: Object.keys(model.views[componentInstance.datasource.name].schema)
|
||||
type === "table" || type === "link"
|
||||
? Object.keys(table.schema)
|
||||
: Object.keys(table.views[componentInstance.datasource.name].schema)
|
||||
}
|
||||
</script>
|
||||
|
|
@ -14,14 +14,14 @@
|
|||
dropdownRight.hide()
|
||||
}
|
||||
|
||||
$: models = $backendUiStore.models.map(m => ({
|
||||
$: tables = $backendUiStore.tables.map(m => ({
|
||||
label: m.name,
|
||||
name: `all_${m._id}`,
|
||||
modelId: m._id,
|
||||
type: "model",
|
||||
tableId: m._id,
|
||||
type: "table",
|
||||
}))
|
||||
|
||||
$: views = $backendUiStore.models.reduce((acc, cur) => {
|
||||
$: views = $backendUiStore.tables.reduce((acc, cur) => {
|
||||
let viewsArr = Object.entries(cur.views).map(([key, value]) => ({
|
||||
label: key,
|
||||
name: key,
|
||||
|
@ -35,7 +35,7 @@
|
|||
componentInstanceId: $store.currentComponentInfo._id,
|
||||
components: $store.components,
|
||||
screen: $store.currentPreviewItem,
|
||||
models: $backendUiStore.models,
|
||||
tables: $backendUiStore.tables,
|
||||
})
|
||||
|
||||
$: links = bindableProperties
|
||||
|
@ -43,8 +43,8 @@
|
|||
.map(property => ({
|
||||
label: property.readableBinding,
|
||||
fieldName: property.fieldSchema.name,
|
||||
name: `all_${property.fieldSchema.modelId}`,
|
||||
modelId: property.fieldSchema.modelId,
|
||||
name: `all_${property.fieldSchema.tableId}`,
|
||||
tableId: property.fieldSchema.tableId,
|
||||
type: "link",
|
||||
}))
|
||||
</script>
|
||||
|
@ -53,7 +53,7 @@
|
|||
class="dropdownbutton"
|
||||
bind:this={anchorRight}
|
||||
on:click={dropdownRight.show}>
|
||||
<span>{value.label ? value.label : 'Model / View'}</span>
|
||||
<span>{value.label ? value.label : 'Table / View'}</span>
|
||||
<Icon name="arrowdown" />
|
||||
</div>
|
||||
<DropdownMenu bind:this={dropdownRight} anchor={anchorRight}>
|
||||
|
@ -62,11 +62,11 @@
|
|||
<Heading extraSmall>Tables</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
{#each models as model}
|
||||
{#each tables as table}
|
||||
<li
|
||||
class:selected={value === model}
|
||||
on:click={() => handleSelected(model)}>
|
||||
{model.label}
|
||||
class:selected={value === table}
|
||||
on:click={() => handleSelected(table)}>
|
||||
{table.label}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
|
@ -19,7 +19,7 @@ export const TYPE_MAP = {
|
|||
"##bbstate": "",
|
||||
},
|
||||
},
|
||||
models: {
|
||||
tables: {
|
||||
default: {},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Input from "./PropertyPanelControls/Input.svelte"
|
||||
import OptionSelect from "./OptionSelect.svelte"
|
||||
import Checkbox from "../common/Checkbox.svelte"
|
||||
import ModelSelect from "components/userInterface/ModelSelect.svelte"
|
||||
import ModelViewSelect from "components/userInterface/ModelViewSelect.svelte"
|
||||
import ModelViewFieldSelect from "components/userInterface/ModelViewFieldSelect.svelte"
|
||||
import TableSelect from "components/userInterface/TableSelect.svelte"
|
||||
import TableViewSelect from "components/userInterface/TableViewSelect.svelte"
|
||||
import TableViewFieldSelect from "components/userInterface/TableViewFieldSelect.svelte"
|
||||
import Event from "components/userInterface/EventsEditor/EventPropertyControl.svelte"
|
||||
import ScreenSelect from "components/userInterface/ScreenSelect.svelte"
|
||||
import { IconSelect } from "components/userInterface/IconSelect"
|
||||
|
@ -299,7 +299,7 @@ export default {
|
|||
{
|
||||
name: "List",
|
||||
_component: "@budibase/standard-components/list",
|
||||
description: "Renders all children once per record, of a given table",
|
||||
description: "Renders all children once per row, of a given table",
|
||||
icon: "ri-file-list-line",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
|
@ -307,7 +307,7 @@ export default {
|
|||
{
|
||||
label: "Data",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -325,7 +325,7 @@ export default {
|
|||
{
|
||||
label: "Source",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Editable",
|
||||
|
@ -589,7 +589,7 @@ export default {
|
|||
{
|
||||
label: "Data",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Stripe Color",
|
||||
|
@ -615,7 +615,7 @@ export default {
|
|||
control: Colorpicker,
|
||||
defaultValue: "#FFFFFF",
|
||||
},
|
||||
{ label: "Table", key: "model", control: ModelSelect },
|
||||
{ label: "Table", key: "table", control: TableSelect },
|
||||
],
|
||||
},
|
||||
children: [],
|
||||
|
@ -661,19 +661,19 @@ export default {
|
|||
{
|
||||
label: "Data",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Name Field",
|
||||
key: "nameKey",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Value Field",
|
||||
key: "valueKey",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Animate Chart",
|
||||
|
@ -755,19 +755,19 @@ export default {
|
|||
{
|
||||
label: "Data",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Name Label",
|
||||
key: "nameLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Value Label",
|
||||
key: "valueLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Y Axis Label",
|
||||
|
@ -869,25 +869,25 @@ export default {
|
|||
{
|
||||
label: "Data",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Name Label",
|
||||
key: "nameLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Group Label",
|
||||
key: "groupLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Value Label",
|
||||
key: "valueLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Color",
|
||||
|
@ -972,25 +972,25 @@ export default {
|
|||
{
|
||||
label: "Data",
|
||||
key: "datasource",
|
||||
control: ModelViewSelect,
|
||||
control: TableViewSelect,
|
||||
},
|
||||
{
|
||||
label: "Value Label",
|
||||
key: "valueLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Topic Label",
|
||||
key: "topicLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Date Label",
|
||||
key: "dateLabel",
|
||||
dependsOn: "datasource",
|
||||
control: ModelViewFieldSelect,
|
||||
control: TableViewFieldSelect,
|
||||
},
|
||||
{
|
||||
label: "Colors",
|
||||
|
@ -1137,7 +1137,7 @@ export default {
|
|||
// icon: "ri-file-list-line",
|
||||
// properties: {
|
||||
// design: { ...all },
|
||||
// settings: [{ label: "Table", key: "model", control: ModelSelect }],
|
||||
// settings: [{ label: "Table", key: "table", control: TableSelect }],
|
||||
// },
|
||||
// children: [],
|
||||
// },
|
||||
|
@ -1145,11 +1145,11 @@ export default {
|
|||
name: "Row Detail",
|
||||
_component: "@budibase/standard-components/rowdetail",
|
||||
description:
|
||||
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
|
||||
"Loads a row, using an id from the URL, which can be used with {{ context }}, in children",
|
||||
icon: "ri-profile-line",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Table", key: "model", control: ModelSelect }],
|
||||
settings: [{ label: "Table", key: "table", control: TableSelect }],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
|
@ -1157,11 +1157,11 @@ export default {
|
|||
name: "New Row",
|
||||
_component: "@budibase/standard-components/newrow",
|
||||
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",
|
||||
properties: {
|
||||
design: { ...all },
|
||||
settings: [{ label: "Table", key: "model", control: ModelSelect }],
|
||||
settings: [{ label: "Table", key: "table", control: TableSelect }],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
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>
|
||||
|
||||
<!-- routify:options index=1 -->
|
||||
<div class="root">
|
||||
<div class="nav">
|
||||
<ModelNavigator />
|
||||
<TableNavigator />
|
||||
</div>
|
||||
<div class="content">
|
||||
<slot />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { goto } from "@sveltech/routify"
|
||||
$goto("../model")
|
||||
$goto("../table")
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=false -->
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
if ($params.selectedModel) {
|
||||
const model = $backendUiStore.models.find(
|
||||
m => m._id === $params.selectedModel
|
||||
)
|
||||
if (model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,32 +0,0 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { goto, leftover } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
async function selectModel(model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first model in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedModel)
|
||||
if (
|
||||
!$leftover &&
|
||||
$backendUiStore.models.length > 0 &&
|
||||
(!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)
|
||||
) {
|
||||
$goto(`./${$backendUiStore.models[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
|
@ -1,35 +0,0 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto, leftover } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
async function selectModel(model) {
|
||||
backendUiStore.actions.models.select(model)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first model in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedModel)
|
||||
if (
|
||||
!$leftover &&
|
||||
$backendUiStore.models.length > 0 &&
|
||||
(!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)
|
||||
) {
|
||||
// this file routes as .../models/index, so, go up one.
|
||||
$goto(`../${$backendUiStore.models[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $backendUiStore.models.length === 0}
|
||||
<i>Create your first table to start building</i>
|
||||
{:else}
|
||||
<i>Select a table to edit</i>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
i {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--grey-4);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<script>
|
||||
import { params } from "@sveltech/routify"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
if ($params.selectedTable) {
|
||||
const table = $backendUiStore.tables.find(
|
||||
m => m._id === $params.selectedTable
|
||||
)
|
||||
if (table) {
|
||||
backendUiStore.actions.tables.select(table)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -1,12 +1,12 @@
|
|||
<script>
|
||||
import ModelDataTable from "components/backend/DataTable/ModelDataTable.svelte"
|
||||
import TableDataTable from "components/backend/DataTable/DataTable.svelte"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
$: selectedModel = $backendUiStore.selectedModel
|
||||
$: selectedTable = $backendUiStore.selectedTable
|
||||
</script>
|
||||
|
||||
{#if $backendUiStore.selectedDatabase._id && selectedModel.name}
|
||||
<ModelDataTable />
|
||||
{#if $backendUiStore.selectedDatabase._id && selectedTable.name}
|
||||
<TableDataTable />
|
||||
{:else}
|
||||
<i>Create your first table to start building</i>
|
||||
{/if}
|
|
@ -4,6 +4,6 @@
|
|||
</script>
|
||||
|
||||
<RelationshipDataTable
|
||||
modelId={$params.selectedModel}
|
||||
recordId={$params.selectedRecord}
|
||||
tableId={$params.selectedTable}
|
||||
rowId={$params.selectedRow}
|
||||
fieldName={decodeURI($params.selectedField)} />
|
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { goto, leftover } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
async function selectTable(table) {
|
||||
backendUiStore.actions.tables.select(table)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first table in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedTable)
|
||||
if (
|
||||
!$leftover &&
|
||||
$backendUiStore.tables.length > 0 &&
|
||||
(!$backendUiStore.selectedTable || !$backendUiStore.selectedTable._id)
|
||||
) {
|
||||
$goto(`./${$backendUiStore.tables[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { store, backendUiStore } from "builderStore"
|
||||
import { goto, leftover } from "@sveltech/routify"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
async function selectTable(table) {
|
||||
backendUiStore.actions.tables.select(table)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// navigate to first table in list, if not already selected
|
||||
// and this is the final url (i.e. no selectedTable)
|
||||
if (
|
||||
!$leftover &&
|
||||
$backendUiStore.tables.length > 0 &&
|
||||
(!$backendUiStore.selectedTable || !$backendUiStore.selectedTable._id)
|
||||
) {
|
||||
// this file routes as .../tables/index, so, go up one.
|
||||
$goto(`../${$backendUiStore.tables[0]._id}`)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $backendUiStore.tables.length === 0}
|
||||
<i>Create your first table to start building</i>
|
||||
{:else}
|
||||
<i>Select a table to edit</i>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
i {
|
||||
font-size: var(--font-size-xl);
|
||||
color: var(--grey-4);
|
||||
}
|
||||
</style>
|
|
@ -5,9 +5,9 @@
|
|||
if ($params.selectedView) {
|
||||
let view
|
||||
const viewName = decodeURI($params.selectedView)
|
||||
for (let model of $backendUiStore.models) {
|
||||
if (model.views && model.views[viewName]) {
|
||||
view = model.views[viewName]
|
||||
for (let table of $backendUiStore.tables) {
|
||||
if (table.views && table.views[viewName]) {
|
||||
view = table.views[viewName]
|
||||
}
|
||||
}
|
||||
if (view) {
|
||||
|
|
|
@ -22,7 +22,7 @@ describe("fetch bindable properties", () => {
|
|||
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({
|
||||
componentInstanceId: "list-item-input-id",
|
||||
...testData()
|
||||
|
@ -33,41 +33,41 @@ describe("fetch bindable properties", () => {
|
|||
|
||||
const namebinding = contextBindings.find(b => b.runtimeBinding === "data.name")
|
||||
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")
|
||||
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")
|
||||
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({
|
||||
componentInstanceId: "child-list-item-input-id",
|
||||
...testData()
|
||||
})
|
||||
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)
|
||||
|
||||
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "parent.data.name")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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", () => {
|
||||
|
@ -126,7 +126,7 @@ const testData = () => {
|
|||
_id: "list-id",
|
||||
_component: "@budibase/standard-components/list",
|
||||
_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: [
|
||||
{
|
||||
_id: "list-item-heading-id",
|
||||
|
@ -144,7 +144,7 @@ const testData = () => {
|
|||
_id: "child-list-id",
|
||||
_component: "@budibase/standard-components/list",
|
||||
_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: [
|
||||
{
|
||||
_id: "child-list-item-heading-id",
|
||||
|
@ -166,9 +166,9 @@ const testData = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const models = [{
|
||||
_id: "test-model-id",
|
||||
name: "Test Model",
|
||||
const tables = [{
|
||||
_id: "test-table-id",
|
||||
name: "Test Table",
|
||||
schema: {
|
||||
name: {
|
||||
type: "string"
|
||||
|
@ -184,9 +184,9 @@ const testData = () => {
|
|||
props: {},
|
||||
},
|
||||
"@budibase/standard-components/list" : {
|
||||
context: "model",
|
||||
context: "table",
|
||||
props: {
|
||||
model: "string"
|
||||
table: "string"
|
||||
},
|
||||
},
|
||||
"@budibase/standard-components/input" : {
|
||||
|
@ -202,6 +202,6 @@ const testData = () => {
|
|||
},
|
||||
}
|
||||
|
||||
return { screen, models, components }
|
||||
return { screen, tables, components }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ export const componentsAndScreens = () => ({
|
|||
},
|
||||
},
|
||||
{
|
||||
_instanceName: "Record View",
|
||||
tags: ["record"],
|
||||
_instanceName: "Row View",
|
||||
tags: ["row"],
|
||||
props: {
|
||||
data: "state",
|
||||
},
|
||||
|
|
|
@ -52,27 +52,27 @@ const apiOpts = {
|
|||
delete: del,
|
||||
}
|
||||
|
||||
const saveRecord = async (params, state) =>
|
||||
const saveRow = async (params, state) =>
|
||||
await post({
|
||||
url: `/api/${params.modelId}/records`,
|
||||
body: makeRecordRequestBody(params, state),
|
||||
url: `/api/${params.tableId}/rows`,
|
||||
body: makeRowRequestBody(params, state),
|
||||
})
|
||||
|
||||
const updateRecord = async (params, state) => {
|
||||
const record = makeRecordRequestBody(params, state)
|
||||
record._id = params._id
|
||||
const updateRow = async (params, state) => {
|
||||
const row = makeRowRequestBody(params, state)
|
||||
row._id = params._id
|
||||
await patch({
|
||||
url: `/api/${params.modelId}/records/${params._id}`,
|
||||
body: record,
|
||||
url: `/api/${params.tableId}/rows/${params._id}`,
|
||||
body: row,
|
||||
})
|
||||
}
|
||||
|
||||
const makeRecordRequestBody = (parameters, state) => {
|
||||
// start with the record thats currently in context
|
||||
const makeRowRequestBody = (parameters, state) => {
|
||||
// start with the row thats currently in context
|
||||
const body = { ...(state.data || {}) }
|
||||
|
||||
// dont send the model
|
||||
if (body._model) delete body._model
|
||||
// dont send the table
|
||||
if (body._table) delete body._table
|
||||
|
||||
// then override with supplied parameters
|
||||
for (let fieldName in parameters.fields) {
|
||||
|
@ -101,6 +101,6 @@ const makeRecordRequestBody = (parameters, state) => {
|
|||
|
||||
export default {
|
||||
authenticate: authenticate(apiOpts),
|
||||
saveRecord,
|
||||
updateRecord,
|
||||
saveRow,
|
||||
updateRow,
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
|||
export const eventHandlers = routeTo => {
|
||||
const handlers = {
|
||||
"Navigate To": param => routeTo(param && param.url),
|
||||
"Update Record": api.updateRecord,
|
||||
"Save Record": api.saveRecord,
|
||||
"Update Row": api.updateRow,
|
||||
"Save Row": api.saveRow,
|
||||
"Trigger Workflow": api.triggerWorkflow,
|
||||
}
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
"request": "launch",
|
||||
"name": "Jest - Models",
|
||||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||||
"args": ["model.spec", "--runInBand"],
|
||||
"args": ["table.spec", "--runInBand"],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"disableOptimisticBPs": true,
|
||||
|
@ -136,4 +136,4 @@
|
|||
"console": "externalTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,14 +24,14 @@ async function run() {
|
|||
quotaReset: Date.now() + 2592000000,
|
||||
usageQuota: {
|
||||
automationRuns: 0,
|
||||
records: 0,
|
||||
rows: 0,
|
||||
storage: 0,
|
||||
users: 0,
|
||||
views: 0,
|
||||
},
|
||||
usageLimits: {
|
||||
automationRuns: 10,
|
||||
records: 10,
|
||||
rows: 10,
|
||||
storage: 1000,
|
||||
users: 10,
|
||||
views: 10,
|
||||
|
@ -48,8 +48,8 @@ async function run() {
|
|||
|
||||
run()
|
||||
.then(() => {
|
||||
console.log("Records should have been created.")
|
||||
console.log("Rows should have been created.")
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Cannot create records - " + err)
|
||||
console.error("Cannot create rows - " + err)
|
||||
})
|
||||
|
|
|
@ -39,9 +39,9 @@ async function replicateCouch({ instanceId, clientId, credentials }) {
|
|||
async function getCurrentInstanceQuota(instanceId) {
|
||||
const db = new PouchDB(instanceId)
|
||||
|
||||
const records = await db.allDocs({
|
||||
startkey: DocumentTypes.RECORD + SEPARATOR,
|
||||
endkey: DocumentTypes.RECORD + SEPARATOR + UNICODE_MAX,
|
||||
const rows = await db.allDocs({
|
||||
startkey: DocumentTypes.ROW + SEPARATOR,
|
||||
endkey: DocumentTypes.ROW + SEPARATOR + UNICODE_MAX,
|
||||
})
|
||||
|
||||
const users = await db.allDocs({
|
||||
|
@ -49,13 +49,13 @@ async function getCurrentInstanceQuota(instanceId) {
|
|||
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX,
|
||||
})
|
||||
|
||||
const existingRecords = records.rows.length
|
||||
const existingRows = rows.rows.length
|
||||
const existingUsers = users.rows.length
|
||||
|
||||
const designDoc = await db.get("_design/database")
|
||||
|
||||
return {
|
||||
records: existingRecords,
|
||||
rows: existingRows,
|
||||
users: existingUsers,
|
||||
views: Object.keys(designDoc.views).length,
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ const fs = require("fs")
|
|||
const CouchDB = require("../../db")
|
||||
const client = require("../../db/clientDb")
|
||||
const newid = require("../../db/newid")
|
||||
const { createLinkView } = require("../../db/linkedRecords")
|
||||
const { createLinkView } = require("../../db/linkedRows")
|
||||
const { join } = require("../../utilities/centralPath")
|
||||
const { downloadTemplate } = require("../../utilities/templates")
|
||||
|
||||
|
@ -27,7 +27,7 @@ exports.create = async function(ctx) {
|
|||
// https://docs.couchdb.org/en/master/ddocs/views/collation.html#collation-specification
|
||||
views: {},
|
||||
})
|
||||
// add view for linked records
|
||||
// add view for linked rows
|
||||
await createLinkView(instanceId)
|
||||
|
||||
// Add the new instance under the app clientDB
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
const CouchDB = require("../../db")
|
||||
const linkRecords = require("../../db/linkedRecords")
|
||||
const csvParser = require("../../utilities/csvParser")
|
||||
const {
|
||||
getRecordParams,
|
||||
getModelParams,
|
||||
generateModelID,
|
||||
generateRecordID,
|
||||
} = require("../../db/utils")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const body = await db.allDocs(
|
||||
getModelParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
ctx.body = body.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.get(ctx.params.id)
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const { dataImport, ...rest } = ctx.request.body
|
||||
const modelToSave = {
|
||||
type: "model",
|
||||
_id: generateModelID(),
|
||||
views: {},
|
||||
...rest,
|
||||
}
|
||||
let renameDocs = []
|
||||
|
||||
// if the model obj had an _id then it will have been retrieved
|
||||
const oldModel = ctx.preExisting
|
||||
|
||||
// rename record fields when table column is renamed
|
||||
const { _rename } = modelToSave
|
||||
if (_rename && modelToSave.schema[_rename.updated].type === "link") {
|
||||
throw "Cannot rename a linked field."
|
||||
} else if (_rename && modelToSave.primaryDisplay === _rename.old) {
|
||||
throw "Cannot rename the primary display field."
|
||||
} else if (_rename) {
|
||||
const records = await db.allDocs(
|
||||
getRecordParams(modelToSave._id, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
renameDocs = records.rows.map(({ doc }) => {
|
||||
doc[_rename.updated] = doc[_rename.old]
|
||||
delete doc[_rename.old]
|
||||
return doc
|
||||
})
|
||||
delete modelToSave._rename
|
||||
}
|
||||
|
||||
// update schema of non-statistics views when new columns are added
|
||||
for (let view in modelToSave.views) {
|
||||
const modelView = modelToSave.views[view]
|
||||
if (!modelView) continue
|
||||
|
||||
if (modelView.schema.group || modelView.schema.field) continue
|
||||
modelView.schema = modelToSave.schema
|
||||
}
|
||||
|
||||
// update linked records
|
||||
await linkRecords.updateLinks({
|
||||
instanceId,
|
||||
eventType: oldModel
|
||||
? linkRecords.EventType.MODEL_UPDATED
|
||||
: linkRecords.EventType.MODEL_SAVE,
|
||||
model: modelToSave,
|
||||
oldModel: oldModel,
|
||||
})
|
||||
|
||||
// don't perform any updates until relationships have been
|
||||
// checked by the updateLinks function
|
||||
if (renameDocs.length !== 0) {
|
||||
await db.bulkDocs(renameDocs)
|
||||
}
|
||||
const result = await db.post(modelToSave)
|
||||
modelToSave._rev = result.rev
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitModel(`model:save`, instanceId, modelToSave)
|
||||
|
||||
if (dataImport && dataImport.path) {
|
||||
// Populate the table with records imported from CSV in a bulk update
|
||||
const data = await csvParser.transform(dataImport)
|
||||
|
||||
for (let row of data) {
|
||||
row._id = generateRecordID(modelToSave._id)
|
||||
row.modelId = modelToSave._id
|
||||
}
|
||||
|
||||
await db.bulkDocs(data)
|
||||
}
|
||||
|
||||
ctx.status = 200
|
||||
ctx.message = `Model ${ctx.request.body.name} saved successfully.`
|
||||
ctx.body = modelToSave
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const modelToDelete = await db.get(ctx.params.modelId)
|
||||
|
||||
// Delete all records for that model
|
||||
const records = await db.allDocs(
|
||||
getRecordParams(ctx.params.modelId, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
await db.bulkDocs(
|
||||
records.rows.map(record => ({ ...record.doc, _deleted: true }))
|
||||
)
|
||||
|
||||
// update linked records
|
||||
await linkRecords.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRecords.EventType.MODEL_DELETE,
|
||||
model: modelToDelete,
|
||||
})
|
||||
|
||||
// don't remove the table itself until very end
|
||||
await db.remove(modelToDelete)
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitModel(`model:delete`, instanceId, modelToDelete)
|
||||
ctx.status = 200
|
||||
ctx.message = `Model ${ctx.params.modelId} deleted.`
|
||||
}
|
||||
|
||||
exports.validateCSVSchema = async function(ctx) {
|
||||
const { file, schema = {} } = ctx.request.body
|
||||
const result = await csvParser.parse(file.path, schema)
|
||||
ctx.body = {
|
||||
schema: result,
|
||||
path: file.path,
|
||||
}
|
||||
}
|
|
@ -1,382 +0,0 @@
|
|||
const CouchDB = require("../../db")
|
||||
const validateJs = require("validate.js")
|
||||
const linkRecords = require("../../db/linkedRecords")
|
||||
const {
|
||||
getRecordParams,
|
||||
generateRecordID,
|
||||
DocumentTypes,
|
||||
SEPARATOR,
|
||||
} = require("../../db/utils")
|
||||
const { cloneDeep } = require("lodash")
|
||||
|
||||
const MODEL_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.MODEL}${SEPARATOR}`
|
||||
|
||||
validateJs.extend(validateJs.validators.datetime, {
|
||||
parse: function(value) {
|
||||
return new Date(value).getTime()
|
||||
},
|
||||
// Input is a unix timestamp
|
||||
format: function(value) {
|
||||
return new Date(value).toISOString()
|
||||
},
|
||||
})
|
||||
|
||||
exports.patch = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
let record = await db.get(ctx.params.id)
|
||||
const model = await db.get(record.modelId)
|
||||
const patchfields = ctx.request.body
|
||||
record = coerceRecordValues(record, model)
|
||||
|
||||
for (let key of Object.keys(patchfields)) {
|
||||
if (!model.schema[key]) continue
|
||||
record[key] = patchfields[key]
|
||||
}
|
||||
|
||||
const validateResult = await validate({
|
||||
record,
|
||||
model,
|
||||
})
|
||||
|
||||
if (!validateResult.valid) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
errors: validateResult.errors,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// returned record is cleaned and prepared for writing to DB
|
||||
record = await linkRecords.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRecords.EventType.RECORD_UPDATE,
|
||||
record,
|
||||
modelId: record.modelId,
|
||||
model,
|
||||
})
|
||||
const response = await db.put(record)
|
||||
record._rev = response.rev
|
||||
record.type = "record"
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRecord(`record:update`, instanceId, record, model)
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} updated successfully.`
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
if (ctx.request.body.type === "delete") {
|
||||
await bulkDelete(ctx)
|
||||
} else {
|
||||
await saveRecord(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
exports.fetchView = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const { stats, group, field } = ctx.query
|
||||
const viewName = ctx.params.viewName
|
||||
|
||||
// if this is a model view being looked for just transfer to that
|
||||
if (viewName.indexOf(MODEL_VIEW_BEGINS_WITH) === 0) {
|
||||
ctx.params.modelId = viewName.substring(4)
|
||||
await exports.fetchModelRecords(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await db.query(`database/${viewName}`, {
|
||||
include_docs: !stats,
|
||||
group,
|
||||
})
|
||||
|
||||
if (stats) {
|
||||
response.rows = response.rows.map(row => ({
|
||||
group: row.key,
|
||||
field,
|
||||
...row.value,
|
||||
avg: row.value.sum / row.value.count,
|
||||
}))
|
||||
} else {
|
||||
response.rows = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows)
|
||||
}
|
||||
|
||||
exports.fetchModelRecords = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const response = await db.allDocs(
|
||||
getRecordParams(ctx.params.modelId, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
ctx.body = await linkRecords.attachLinkInfo(
|
||||
instanceId,
|
||||
response.rows.map(row => row.doc)
|
||||
)
|
||||
}
|
||||
|
||||
exports.search = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const response = await db.allDocs({
|
||||
include_docs: true,
|
||||
...ctx.request.body,
|
||||
})
|
||||
ctx.body = await linkRecords.attachLinkInfo(
|
||||
instanceId,
|
||||
response.rows.map(row => row.doc)
|
||||
)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId does not match the records modelId")
|
||||
return
|
||||
}
|
||||
ctx.body = await linkRecords.attachLinkInfo(instanceId, record)
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doesn't match the record's modelId")
|
||||
return
|
||||
}
|
||||
await linkRecords.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRecords.EventType.RECORD_DELETE,
|
||||
record,
|
||||
modelId: record.modelId,
|
||||
})
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
ctx.status = 200
|
||||
|
||||
// for automations include the record that was deleted
|
||||
ctx.record = record
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record)
|
||||
}
|
||||
|
||||
exports.validate = async function(ctx) {
|
||||
const errors = await validate({
|
||||
instanceId: ctx.user.instanceId,
|
||||
modelId: ctx.params.modelId,
|
||||
record: ctx.request.body,
|
||||
})
|
||||
ctx.status = 200
|
||||
ctx.body = errors
|
||||
}
|
||||
|
||||
async function validate({ instanceId, modelId, record, model }) {
|
||||
if (!model) {
|
||||
const db = new CouchDB(instanceId)
|
||||
model = await db.get(modelId)
|
||||
}
|
||||
const errors = {}
|
||||
for (let fieldName of Object.keys(model.schema)) {
|
||||
const res = validateJs.single(
|
||||
record[fieldName],
|
||||
model.schema[fieldName].constraints
|
||||
)
|
||||
if (res) errors[fieldName] = res
|
||||
}
|
||||
return { valid: Object.keys(errors).length === 0, errors }
|
||||
}
|
||||
|
||||
exports.fetchEnrichedRecord = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const modelId = ctx.params.modelId
|
||||
const recordId = ctx.params.recordId
|
||||
if (instanceId == null || modelId == null || recordId == null) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
error:
|
||||
"Cannot handle request, URI params have not been successfully prepared.",
|
||||
}
|
||||
return
|
||||
}
|
||||
// need model to work out where links go in record
|
||||
const [model, record] = await Promise.all([db.get(modelId), db.get(recordId)])
|
||||
// get the link docs
|
||||
const linkVals = await linkRecords.getLinkDocuments({
|
||||
instanceId,
|
||||
modelId,
|
||||
recordId,
|
||||
})
|
||||
// look up the actual records based on the ids
|
||||
const response = await db.allDocs({
|
||||
include_docs: true,
|
||||
keys: linkVals.map(linkVal => linkVal.id),
|
||||
})
|
||||
// need to include the IDs in these records for any links they may have
|
||||
let linkedRecords = await linkRecords.attachLinkInfo(
|
||||
instanceId,
|
||||
response.rows.map(row => row.doc)
|
||||
)
|
||||
// insert the link records in the correct place throughout the main record
|
||||
for (let fieldName of Object.keys(model.schema)) {
|
||||
let field = model.schema[fieldName]
|
||||
if (field.type === "link") {
|
||||
record[fieldName] = linkedRecords.filter(
|
||||
linkRecord => linkRecord.modelId === field.modelId
|
||||
)
|
||||
}
|
||||
}
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
function coerceRecordValues(rec, model) {
|
||||
const record = cloneDeep(rec)
|
||||
for (let [key, value] of Object.entries(record)) {
|
||||
const field = model.schema[key]
|
||||
if (!field) continue
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
|
||||
record[key] = TYPE_TRANSFORM_MAP[field.type][value]
|
||||
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
|
||||
record[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
|
||||
}
|
||||
}
|
||||
return record
|
||||
}
|
||||
|
||||
const TYPE_TRANSFORM_MAP = {
|
||||
link: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
},
|
||||
options: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
string: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
number: {
|
||||
"": null,
|
||||
[null]: null,
|
||||
[undefined]: undefined,
|
||||
parse: n => parseFloat(n),
|
||||
},
|
||||
datetime: {
|
||||
"": null,
|
||||
[undefined]: undefined,
|
||||
[null]: null,
|
||||
},
|
||||
attachment: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
},
|
||||
boolean: {
|
||||
"": null,
|
||||
[null]: null,
|
||||
[undefined]: undefined,
|
||||
true: true,
|
||||
false: false,
|
||||
},
|
||||
}
|
||||
|
||||
async function bulkDelete(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const { records } = ctx.request.body
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
|
||||
await db.bulkDocs(
|
||||
records.map(
|
||||
record => ({ ...record, _deleted: true }),
|
||||
err => {
|
||||
if (err) {
|
||||
ctx.status = 500
|
||||
} else {
|
||||
records.forEach(record => {
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRecord(`record:delete`, instanceId, record)
|
||||
})
|
||||
ctx.status = 200
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async function saveRecord(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
let record = ctx.request.body
|
||||
record.modelId = ctx.params.modelId
|
||||
|
||||
if (!record._rev && !record._id) {
|
||||
record._id = generateRecordID(record.modelId)
|
||||
}
|
||||
|
||||
// if the record obj had an _id then it will have been retrieved
|
||||
const existingRecord = ctx.preExisting
|
||||
|
||||
const model = await db.get(record.modelId)
|
||||
|
||||
record = coerceRecordValues(record, model)
|
||||
|
||||
const validateResult = await validate({
|
||||
record,
|
||||
model,
|
||||
})
|
||||
|
||||
if (!validateResult.valid) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
errors: validateResult.errors,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// make sure link records are up to date
|
||||
record = await linkRecords.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRecords.EventType.RECORD_SAVE,
|
||||
record,
|
||||
modelId: record.modelId,
|
||||
model,
|
||||
})
|
||||
|
||||
if (existingRecord) {
|
||||
const response = await db.put(record)
|
||||
record._rev = response.rev
|
||||
record.type = "record"
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} updated successfully.`
|
||||
return
|
||||
}
|
||||
|
||||
record.type = "record"
|
||||
const response = await db.post(record)
|
||||
record._rev = response.rev
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model)
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} created successfully`
|
||||
}
|
|
@ -0,0 +1,350 @@
|
|||
const CouchDB = require("../../db")
|
||||
const validateJs = require("validate.js")
|
||||
const linkRows = require("../../db/linkedRows")
|
||||
const {
|
||||
getRowParams,
|
||||
generateRowID,
|
||||
DocumentTypes,
|
||||
SEPARATOR,
|
||||
} = require("../../db/utils")
|
||||
const { cloneDeep } = require("lodash")
|
||||
|
||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||
|
||||
validateJs.extend(validateJs.validators.datetime, {
|
||||
parse: function(value) {
|
||||
return new Date(value).getTime()
|
||||
},
|
||||
// Input is a unix timestamp
|
||||
format: function(value) {
|
||||
return new Date(value).toISOString()
|
||||
},
|
||||
})
|
||||
|
||||
exports.patch = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
let row = await db.get(ctx.params.id)
|
||||
const table = await db.get(row.tableId)
|
||||
const patchfields = ctx.request.body
|
||||
row = coerceRowValues(row, table)
|
||||
|
||||
for (let key of Object.keys(patchfields)) {
|
||||
if (!table.schema[key]) continue
|
||||
row[key] = patchfields[key]
|
||||
}
|
||||
|
||||
const validateResult = await validate({
|
||||
row,
|
||||
table,
|
||||
})
|
||||
|
||||
if (!validateResult.valid) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
errors: validateResult.errors,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// returned row is cleaned and prepared for writing to DB
|
||||
row = await linkRows.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRows.EventType.ROW_UPDATE,
|
||||
row,
|
||||
tableId: row.tableId,
|
||||
table,
|
||||
})
|
||||
const response = await db.put(row)
|
||||
row._rev = response.rev
|
||||
row.type = "row"
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRow(`row:update`, instanceId, row, table)
|
||||
ctx.body = row
|
||||
ctx.status = 200
|
||||
ctx.message = `${table.name} updated successfully.`
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
let row = ctx.request.body
|
||||
row.tableId = ctx.params.tableId
|
||||
|
||||
if (!row._rev && !row._id) {
|
||||
row._id = generateRowID(row.tableId)
|
||||
}
|
||||
|
||||
// if the row obj had an _id then it will have been retrieved
|
||||
const existingRow = ctx.preExisting
|
||||
|
||||
const table = await db.get(row.tableId)
|
||||
|
||||
row = coerceRowValues(row, table)
|
||||
|
||||
const validateResult = await validate({
|
||||
row,
|
||||
table,
|
||||
})
|
||||
|
||||
if (!validateResult.valid) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
errors: validateResult.errors,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// make sure link rows are up to date
|
||||
row = await linkRows.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRows.EventType.ROW_SAVE,
|
||||
row,
|
||||
tableId: row.tableId,
|
||||
table,
|
||||
})
|
||||
|
||||
if (existingRow) {
|
||||
const response = await db.put(row)
|
||||
row._rev = response.rev
|
||||
row.type = "row"
|
||||
ctx.body = row
|
||||
ctx.status = 200
|
||||
ctx.message = `${table.name} updated successfully.`
|
||||
return
|
||||
}
|
||||
|
||||
row.type = "row"
|
||||
const response = await db.post(row)
|
||||
row._rev = response.rev
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitRow(`row:save`, instanceId, row, table)
|
||||
ctx.body = row
|
||||
ctx.status = 200
|
||||
ctx.message = `${table.name} created successfully`
|
||||
}
|
||||
|
||||
exports.fetchView = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const { stats, group, field } = ctx.query
|
||||
const viewName = ctx.params.viewName
|
||||
|
||||
// if this is a table view being looked for just transfer to that
|
||||
if (viewName.indexOf(TABLE_VIEW_BEGINS_WITH) === 0) {
|
||||
ctx.params.tableId = viewName.substring(4)
|
||||
await exports.fetchTableRows(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
const response = await db.query(`database/${viewName}`, {
|
||||
include_docs: !stats,
|
||||
group,
|
||||
})
|
||||
|
||||
if (stats) {
|
||||
response.rows = response.rows.map(row => ({
|
||||
group: row.key,
|
||||
field,
|
||||
...row.value,
|
||||
avg: row.value.sum / row.value.count,
|
||||
}))
|
||||
} else {
|
||||
response.rows = response.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
|
||||
}
|
||||
|
||||
exports.fetchTableRows = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const response = await db.allDocs(
|
||||
getRowParams(ctx.params.tableId, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
ctx.body = response.rows.map(row => row.doc)
|
||||
ctx.body = await linkRows.attachLinkInfo(
|
||||
instanceId,
|
||||
response.rows.map(row => row.doc)
|
||||
)
|
||||
}
|
||||
|
||||
exports.search = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const response = await db.allDocs({
|
||||
include_docs: true,
|
||||
...ctx.request.body,
|
||||
})
|
||||
ctx.body = await linkRows.attachLinkInfo(
|
||||
instanceId,
|
||||
response.rows.map(row => row.doc)
|
||||
)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const row = await db.get(ctx.params.rowId)
|
||||
if (row.tableId !== ctx.params.tableId) {
|
||||
ctx.throw(400, "Supplied tableId does not match the rows tableId")
|
||||
return
|
||||
}
|
||||
ctx.body = await linkRows.attachLinkInfo(instanceId, row)
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const row = await db.get(ctx.params.rowId)
|
||||
if (row.tableId !== ctx.params.tableId) {
|
||||
ctx.throw(400, "Supplied tableId doesn't match the row's tableId")
|
||||
return
|
||||
}
|
||||
await linkRows.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRows.EventType.ROW_DELETE,
|
||||
row,
|
||||
tableId: row.tableId,
|
||||
})
|
||||
ctx.body = await db.remove(ctx.params.rowId, ctx.params.revId)
|
||||
ctx.status = 200
|
||||
|
||||
// for automations include the row that was deleted
|
||||
ctx.row = row
|
||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, instanceId, row)
|
||||
}
|
||||
|
||||
exports.validate = async function(ctx) {
|
||||
const errors = await validate({
|
||||
instanceId: ctx.user.instanceId,
|
||||
tableId: ctx.params.tableId,
|
||||
row: ctx.request.body,
|
||||
})
|
||||
ctx.status = 200
|
||||
ctx.body = errors
|
||||
}
|
||||
|
||||
async function validate({ instanceId, tableId, row, table }) {
|
||||
if (!table) {
|
||||
const db = new CouchDB(instanceId)
|
||||
table = await db.get(tableId)
|
||||
}
|
||||
const errors = {}
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
const res = validateJs.single(
|
||||
row[fieldName],
|
||||
table.schema[fieldName].constraints
|
||||
)
|
||||
if (res) errors[fieldName] = res
|
||||
}
|
||||
return { valid: Object.keys(errors).length === 0, errors }
|
||||
}
|
||||
|
||||
exports.fetchEnrichedRow = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const tableId = ctx.params.tableId
|
||||
const rowId = ctx.params.rowId
|
||||
if (instanceId == null || tableId == null || rowId == null) {
|
||||
ctx.status = 400
|
||||
ctx.body = {
|
||||
status: 400,
|
||||
error:
|
||||
"Cannot handle request, URI params have not been successfully prepared.",
|
||||
}
|
||||
return
|
||||
}
|
||||
// need table to work out where links go in row
|
||||
const [table, row] = await Promise.all([db.get(tableId), db.get(rowId)])
|
||||
// get the link docs
|
||||
const linkVals = await linkRows.getLinkDocuments({
|
||||
instanceId,
|
||||
tableId,
|
||||
rowId,
|
||||
})
|
||||
// look up the actual rows based on the ids
|
||||
const response = await db.allDocs({
|
||||
include_docs: true,
|
||||
keys: linkVals.map(linkVal => linkVal.id),
|
||||
})
|
||||
// need to include the IDs in these rows for any links they may have
|
||||
let linkedRows = await linkRows.attachLinkInfo(
|
||||
instanceId,
|
||||
response.rows.map(row => row.doc)
|
||||
)
|
||||
// insert the link rows in the correct place throughout the main row
|
||||
for (let fieldName of Object.keys(table.schema)) {
|
||||
let field = table.schema[fieldName]
|
||||
if (field.type === "link") {
|
||||
row[fieldName] = linkedRows.filter(
|
||||
linkRow => linkRow.tableId === field.tableId
|
||||
)
|
||||
}
|
||||
}
|
||||
ctx.body = row
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
function coerceRowValues(rec, table) {
|
||||
const row = cloneDeep(rec)
|
||||
for (let [key, value] of Object.entries(row)) {
|
||||
const field = table.schema[key]
|
||||
if (!field) continue
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (TYPE_TRANSFORM_MAP[field.type].hasOwnProperty(value)) {
|
||||
row[key] = TYPE_TRANSFORM_MAP[field.type][value]
|
||||
} else if (TYPE_TRANSFORM_MAP[field.type].parse) {
|
||||
row[key] = TYPE_TRANSFORM_MAP[field.type].parse(value)
|
||||
}
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
const TYPE_TRANSFORM_MAP = {
|
||||
link: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
},
|
||||
options: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
string: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
number: {
|
||||
"": null,
|
||||
[null]: null,
|
||||
[undefined]: undefined,
|
||||
parse: n => parseFloat(n),
|
||||
},
|
||||
datetime: {
|
||||
"": null,
|
||||
[undefined]: undefined,
|
||||
[null]: null,
|
||||
},
|
||||
attachment: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
},
|
||||
boolean: {
|
||||
"": null,
|
||||
[null]: null,
|
||||
[undefined]: undefined,
|
||||
true: true,
|
||||
false: false,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
const CouchDB = require("../../db")
|
||||
const linkRows = require("../../db/linkedRows")
|
||||
const csvParser = require("../../utilities/csvParser")
|
||||
const {
|
||||
getRowParams,
|
||||
getTableParams,
|
||||
generateTableID,
|
||||
generateRowID,
|
||||
} = require("../../db/utils")
|
||||
|
||||
exports.fetch = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const body = await db.allDocs(
|
||||
getTableParams(null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
ctx.body = body.rows.map(row => row.doc)
|
||||
}
|
||||
|
||||
exports.find = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.get(ctx.params.id)
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const { dataImport, ...rest } = ctx.request.body
|
||||
const tableToSave = {
|
||||
type: "table",
|
||||
_id: generateTableID(),
|
||||
views: {},
|
||||
...rest,
|
||||
}
|
||||
let renameDocs = []
|
||||
|
||||
// if the table obj had an _id then it will have been retrieved
|
||||
const oldTable = ctx.preExisting
|
||||
|
||||
// rename row fields when table column is renamed
|
||||
const { _rename } = tableToSave
|
||||
if (_rename && tableToSave.schema[_rename.updated].type === "link") {
|
||||
throw "Cannot rename a linked field."
|
||||
} else if (_rename && tableToSave.primaryDisplay === _rename.old) {
|
||||
throw "Cannot rename the primary display field."
|
||||
} else if (_rename) {
|
||||
const rows = await db.allDocs(
|
||||
getRowParams(tableToSave._id, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
renameDocs = rows.rows.map(({ doc }) => {
|
||||
doc[_rename.updated] = doc[_rename.old]
|
||||
delete doc[_rename.old]
|
||||
return doc
|
||||
})
|
||||
delete tableToSave._rename
|
||||
}
|
||||
|
||||
// update schema of non-statistics views when new columns are added
|
||||
for (let view in tableToSave.views) {
|
||||
const tableView = tableToSave.views[view]
|
||||
if (!tableView) continue
|
||||
|
||||
if (tableView.schema.group || tableView.schema.field) continue
|
||||
tableView.schema = tableToSave.schema
|
||||
}
|
||||
|
||||
// update linked rows
|
||||
await linkRows.updateLinks({
|
||||
instanceId,
|
||||
eventType: oldTable
|
||||
? linkRows.EventType.TABLE_UPDATED
|
||||
: linkRows.EventType.TABLE_SAVE,
|
||||
table: tableToSave,
|
||||
oldTable: oldTable,
|
||||
})
|
||||
|
||||
// don't perform any updates until relationships have been
|
||||
// checked by the updateLinks function
|
||||
if (renameDocs.length !== 0) {
|
||||
await db.bulkDocs(renameDocs)
|
||||
}
|
||||
const result = await db.post(tableToSave)
|
||||
tableToSave._rev = result.rev
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitTable(`table:save`, instanceId, tableToSave)
|
||||
|
||||
if (dataImport && dataImport.path) {
|
||||
// Populate the table with rows imported from CSV in a bulk update
|
||||
const data = await csvParser.transform(dataImport)
|
||||
|
||||
for (let row of data) {
|
||||
row._id = generateRowID(tableToSave._id)
|
||||
row.tableId = tableToSave._id
|
||||
}
|
||||
|
||||
await db.bulkDocs(data)
|
||||
}
|
||||
|
||||
ctx.status = 200
|
||||
ctx.message = `Table ${ctx.request.body.name} saved successfully.`
|
||||
ctx.body = tableToSave
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const instanceId = ctx.user.instanceId
|
||||
const db = new CouchDB(instanceId)
|
||||
const tableToDelete = await db.get(ctx.params.tableId)
|
||||
|
||||
// Delete all rows for that table
|
||||
const rows = await db.allDocs(
|
||||
getRowParams(ctx.params.tableId, null, {
|
||||
include_docs: true,
|
||||
})
|
||||
)
|
||||
await db.bulkDocs(rows.rows.map(row => ({ ...row.doc, _deleted: true })))
|
||||
|
||||
// update linked rows
|
||||
await linkRows.updateLinks({
|
||||
instanceId,
|
||||
eventType: linkRows.EventType.TABLE_DELETE,
|
||||
table: tableToDelete,
|
||||
})
|
||||
|
||||
// don't remove the table itself until very end
|
||||
await db.remove(tableToDelete)
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emitTable(`table:delete`, instanceId, tableToDelete)
|
||||
ctx.status = 200
|
||||
ctx.message = `Table ${ctx.params.tableId} deleted.`
|
||||
}
|
||||
|
||||
exports.validateCSVSchema = async function(ctx) {
|
||||
const { file, schema = {} } = ctx.request.body
|
||||
const result = await csvParser.parse(file.path, schema)
|
||||
ctx.body = {
|
||||
schema: result,
|
||||
path: file.path,
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ const fs = require("fs")
|
|||
const { join } = require("../../../utilities/centralPath")
|
||||
const os = require("os")
|
||||
const exporters = require("./exporters")
|
||||
const { fetchView } = require("../record")
|
||||
const { fetchView } = require("../row")
|
||||
|
||||
const controller = {
|
||||
fetch: async ctx => {
|
||||
|
@ -45,21 +45,21 @@ const controller = {
|
|||
|
||||
await db.put(designDoc)
|
||||
|
||||
// add views to model document
|
||||
const model = await db.get(ctx.request.body.modelId)
|
||||
if (!model.views) model.views = {}
|
||||
// add views to table document
|
||||
const table = await db.get(ctx.request.body.tableId)
|
||||
if (!table.views) table.views = {}
|
||||
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) {
|
||||
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.`
|
||||
},
|
||||
destroy: async ctx => {
|
||||
|
@ -74,9 +74,9 @@ const controller = {
|
|||
|
||||
await db.put(designDoc)
|
||||
|
||||
const model = await db.get(view.meta.modelId)
|
||||
delete model.views[viewName]
|
||||
await db.put(model)
|
||||
const table = await db.get(view.meta.tableId)
|
||||
delete table.views[viewName]
|
||||
await db.put(table)
|
||||
|
||||
ctx.body = view
|
||||
ctx.message = `View ${ctx.params.viewName} saved successfully.`
|
||||
|
@ -85,7 +85,7 @@ const controller = {
|
|||
const view = ctx.request.body
|
||||
const format = ctx.query.format
|
||||
|
||||
// Fetch view records
|
||||
// Fetch view rows
|
||||
ctx.params.viewName = view.name
|
||||
ctx.query.group = view.groupBy
|
||||
if (view.field) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = `
|
||||
Object {
|
||||
"map": "function (doc) {
|
||||
if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
|
||||
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
|
||||
emit(doc[\\"_id\\"], doc[\\"myField\\"]);
|
||||
}
|
||||
}",
|
||||
|
@ -12,7 +12,6 @@ Object {
|
|||
"field": "myField",
|
||||
"filters": Array [],
|
||||
"groupBy": undefined,
|
||||
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"schema": Object {
|
||||
"avg": Object {
|
||||
"type": "number",
|
||||
|
@ -36,6 +35,7 @@ Object {
|
|||
"type": "number",
|
||||
},
|
||||
},
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
},
|
||||
"reduce": "_stats",
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ Object {
|
|||
exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = `
|
||||
Object {
|
||||
"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);
|
||||
}
|
||||
}",
|
||||
|
@ -65,8 +65,8 @@ Object {
|
|||
},
|
||||
],
|
||||
"groupBy": undefined,
|
||||
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"schema": null,
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
@ -74,7 +74,7 @@ Object {
|
|||
exports[`viewBuilder Group By creates a view emitting the group by field 1`] = `
|
||||
Object {
|
||||
"map": "function (doc) {
|
||||
if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
|
||||
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
|
||||
emit(doc[\\"age\\"], doc[\\"score\\"]);
|
||||
}
|
||||
}",
|
||||
|
@ -83,8 +83,8 @@ Object {
|
|||
"field": "score",
|
||||
"filters": Array [],
|
||||
"groupBy": "age",
|
||||
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"schema": null,
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
},
|
||||
"reduce": "_stats",
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ describe("viewBuilder", () => {
|
|||
it("creates a view with multiple filters and conjunctions", () => {
|
||||
expect(viewTemplate({
|
||||
"name": "Test View",
|
||||
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"filters": [{
|
||||
"value": "Test",
|
||||
"condition": "EQUALS",
|
||||
|
@ -27,7 +27,7 @@ describe("viewBuilder", () => {
|
|||
"name": "Calculate View",
|
||||
"field": "myField",
|
||||
"calculation": "stats",
|
||||
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"filters": []
|
||||
})).toMatchSnapshot()
|
||||
})
|
||||
|
@ -37,7 +37,7 @@ describe("viewBuilder", () => {
|
|||
it("creates a view emitting the group by field", () => {
|
||||
expect(viewTemplate({
|
||||
"name": "Test Scores Grouped By Age",
|
||||
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||
"groupBy": "age",
|
||||
"field": "score",
|
||||
"filters": [],
|
||||
|
|
|
@ -90,12 +90,12 @@ function parseEmitExpression(field, groupBy) {
|
|||
*
|
||||
* @param {Object} viewDefinition - the JSON definition for a custom view.
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
function viewTemplate({ field, modelId, groupBy, filters = [], calculation }) {
|
||||
function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
|
||||
const parsedFilters = parseFilterExpression(filters)
|
||||
const filterExpression = parsedFilters ? `&& ${parsedFilters}` : ""
|
||||
|
||||
|
@ -115,14 +115,14 @@ function viewTemplate({ field, modelId, groupBy, filters = [], calculation }) {
|
|||
return {
|
||||
meta: {
|
||||
field,
|
||||
modelId,
|
||||
tableId,
|
||||
groupBy,
|
||||
filters,
|
||||
schema,
|
||||
calculation,
|
||||
},
|
||||
map: `function (doc) {
|
||||
if (doc.modelId === "${modelId}" ${filterExpression}) {
|
||||
if (doc.tableId === "${tableId}" ${filterExpression}) {
|
||||
${emitExpression}
|
||||
}
|
||||
}`,
|
||||
|
|
|
@ -11,8 +11,8 @@ const {
|
|||
instanceRoutes,
|
||||
clientRoutes,
|
||||
applicationRoutes,
|
||||
recordRoutes,
|
||||
modelRoutes,
|
||||
rowRoutes,
|
||||
tableRoutes,
|
||||
viewRoutes,
|
||||
staticRoutes,
|
||||
componentRoutes,
|
||||
|
@ -74,11 +74,11 @@ router.use(authRoutes.allowedMethods())
|
|||
router.use(viewRoutes.routes())
|
||||
router.use(viewRoutes.allowedMethods())
|
||||
|
||||
router.use(modelRoutes.routes())
|
||||
router.use(modelRoutes.allowedMethods())
|
||||
router.use(tableRoutes.routes())
|
||||
router.use(tableRoutes.allowedMethods())
|
||||
|
||||
router.use(recordRoutes.routes())
|
||||
router.use(recordRoutes.allowedMethods())
|
||||
router.use(rowRoutes.routes())
|
||||
router.use(rowRoutes.allowedMethods())
|
||||
|
||||
router.use(userRoutes.routes())
|
||||
router.use(userRoutes.allowedMethods())
|
||||
|
|
|
@ -4,8 +4,8 @@ const userRoutes = require("./user")
|
|||
const instanceRoutes = require("./instance")
|
||||
const clientRoutes = require("./client")
|
||||
const applicationRoutes = require("./application")
|
||||
const modelRoutes = require("./model")
|
||||
const recordRoutes = require("./record")
|
||||
const tableRoutes = require("./table")
|
||||
const rowRoutes = require("./row")
|
||||
const viewRoutes = require("./view")
|
||||
const staticRoutes = require("./static")
|
||||
const componentRoutes = require("./component")
|
||||
|
@ -24,8 +24,8 @@ module.exports = {
|
|||
instanceRoutes,
|
||||
clientRoutes,
|
||||
applicationRoutes,
|
||||
recordRoutes,
|
||||
modelRoutes,
|
||||
rowRoutes,
|
||||
tableRoutes,
|
||||
viewRoutes,
|
||||
staticRoutes,
|
||||
componentRoutes,
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
const Router = require("@koa/router")
|
||||
const modelController = require("../controllers/model")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/models", authorized(BUILDER), modelController.fetch)
|
||||
.get(
|
||||
"/api/models/:id",
|
||||
authorized(READ_MODEL, ctx => ctx.params.id),
|
||||
modelController.find
|
||||
)
|
||||
.post("/api/models", authorized(BUILDER), modelController.save)
|
||||
.post(
|
||||
"/api/models/csv/validate",
|
||||
authorized(BUILDER),
|
||||
modelController.validateCSVSchema
|
||||
)
|
||||
.delete(
|
||||
"/api/models/:modelId/:revId",
|
||||
authorized(BUILDER),
|
||||
modelController.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
|
@ -1,49 +0,0 @@
|
|||
const Router = require("@koa/router")
|
||||
const recordController = require("../controllers/record")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const usage = require("../../middleware/usageQuota")
|
||||
const { READ_MODEL, WRITE_MODEL } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get(
|
||||
"/api/:modelId/:recordId/enrich",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.fetchEnrichedRecord
|
||||
)
|
||||
.get(
|
||||
"/api/:modelId/records",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.fetchModelRecords
|
||||
)
|
||||
.get(
|
||||
"/api/:modelId/records/:recordId",
|
||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.find
|
||||
)
|
||||
.post("/api/records/search", recordController.search)
|
||||
.post(
|
||||
"/api/:modelId/records",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
usage,
|
||||
recordController.save
|
||||
)
|
||||
.patch(
|
||||
"/api/:modelId/records/:id",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.patch
|
||||
)
|
||||
.post(
|
||||
"/api/:modelId/records/validate",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
recordController.validate
|
||||
)
|
||||
.delete(
|
||||
"/api/:modelId/records/:recordId/:revId",
|
||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||
usage,
|
||||
recordController.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
|
@ -0,0 +1,49 @@
|
|||
const Router = require("@koa/router")
|
||||
const rowController = require("../controllers/row")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const usage = require("../../middleware/usageQuota")
|
||||
const { READ_TABLE, WRITE_TABLE } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get(
|
||||
"/api/:tableId/:rowId/enrich",
|
||||
authorized(READ_TABLE, ctx => ctx.params.tableId),
|
||||
rowController.fetchEnrichedRow
|
||||
)
|
||||
.get(
|
||||
"/api/:tableId/rows",
|
||||
authorized(READ_TABLE, ctx => ctx.params.tableId),
|
||||
rowController.fetchTableRows
|
||||
)
|
||||
.get(
|
||||
"/api/:tableId/rows/:rowId",
|
||||
authorized(READ_TABLE, ctx => ctx.params.tableId),
|
||||
rowController.find
|
||||
)
|
||||
.post("/api/rows/search", rowController.search)
|
||||
.post(
|
||||
"/api/:tableId/rows",
|
||||
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
|
||||
usage,
|
||||
rowController.save
|
||||
)
|
||||
.patch(
|
||||
"/api/:tableId/rows/:id",
|
||||
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
|
||||
rowController.patch
|
||||
)
|
||||
.post(
|
||||
"/api/:tableId/rows/validate",
|
||||
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
|
||||
rowController.validate
|
||||
)
|
||||
.delete(
|
||||
"/api/:tableId/rows/:rowId/:revId",
|
||||
authorized(WRITE_TABLE, ctx => ctx.params.tableId),
|
||||
usage,
|
||||
rowController.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
|
@ -0,0 +1,27 @@
|
|||
const Router = require("@koa/router")
|
||||
const tableController = require("../controllers/table")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const { BUILDER, READ_TABLE } = require("../../utilities/accessLevels")
|
||||
|
||||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/tables", authorized(BUILDER), tableController.fetch)
|
||||
.get(
|
||||
"/api/tables/:id",
|
||||
authorized(READ_TABLE, ctx => ctx.params.id),
|
||||
tableController.find
|
||||
)
|
||||
.post("/api/tables", authorized(BUILDER), tableController.save)
|
||||
.post(
|
||||
"/api/tables/csv/validate",
|
||||
authorized(BUILDER),
|
||||
tableController.validateCSVSchema
|
||||
)
|
||||
.delete(
|
||||
"/api/tables/:tableId/:revId",
|
||||
authorized(BUILDER),
|
||||
tableController.destroy
|
||||
)
|
||||
|
||||
module.exports = router
|
|
@ -2,7 +2,7 @@ const {
|
|||
createInstance,
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
createModel,
|
||||
createTable,
|
||||
createView,
|
||||
supertest,
|
||||
defaultHeaders
|
||||
|
@ -12,8 +12,8 @@ const {
|
|||
generatePowerUserPermissions,
|
||||
POWERUSER_LEVEL_ID,
|
||||
ADMIN_LEVEL_ID,
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
READ_TABLE,
|
||||
WRITE_TABLE,
|
||||
} = require("../../../utilities/accessLevels")
|
||||
|
||||
describe("/accesslevels", () => {
|
||||
|
@ -21,7 +21,7 @@ describe("/accesslevels", () => {
|
|||
let server
|
||||
let request
|
||||
let instanceId
|
||||
let model
|
||||
let table
|
||||
let view
|
||||
|
||||
beforeAll(async () => {
|
||||
|
@ -36,8 +36,8 @@ describe("/accesslevels", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
instanceId = (await createInstance(request, appId))._id
|
||||
model = await createModel(request, appId, instanceId)
|
||||
view = await createView(request, appId, instanceId, model._id)
|
||||
table = await createTable(request, appId, instanceId)
|
||||
view = await createView(request, appId, instanceId, table._id)
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
|
@ -63,7 +63,7 @@ describe("/accesslevels", () => {
|
|||
it("should list custom levels, plus 2 default levels", async () => {
|
||||
const createRes = await request
|
||||
.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))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -96,7 +96,7 @@ describe("/accesslevels", () => {
|
|||
it("should delete custom access level", async () => {
|
||||
const createRes = await request
|
||||
.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))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -119,7 +119,7 @@ describe("/accesslevels", () => {
|
|||
it("should add given permissions", async () => {
|
||||
const createRes = await request
|
||||
.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))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -130,7 +130,7 @@ describe("/accesslevels", () => {
|
|||
.patch(`/api/accesslevels/${customLevel._id}`)
|
||||
.send({
|
||||
_rev: customLevel._rev,
|
||||
addedPermissions: [ { itemId: model._id, name: WRITE_MODEL } ]
|
||||
addedPermissions: [ { itemId: table._id, name: WRITE_TABLE } ]
|
||||
})
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -142,8 +142,8 @@ describe("/accesslevels", () => {
|
|||
.expect(200)
|
||||
|
||||
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 === READ_MODEL)).toBe(true)
|
||||
expect(finalRes.body.permissions.some(p => p.name === WRITE_TABLE)).toBe(true)
|
||||
expect(finalRes.body.permissions.some(p => p.name === READ_TABLE)).toBe(true)
|
||||
})
|
||||
|
||||
it("should remove given permissions", async () => {
|
||||
|
@ -152,8 +152,8 @@ describe("/accesslevels", () => {
|
|||
.send({
|
||||
name: "user",
|
||||
permissions: [
|
||||
{ itemId: model._id, name: READ_MODEL },
|
||||
{ itemId: model._id, name: WRITE_MODEL },
|
||||
{ itemId: table._id, name: READ_TABLE },
|
||||
{ itemId: table._id, name: WRITE_TABLE },
|
||||
]
|
||||
})
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
|
@ -166,7 +166,7 @@ describe("/accesslevels", () => {
|
|||
.patch(`/api/accesslevels/${customLevel._id}`)
|
||||
.send({
|
||||
_rev: customLevel._rev,
|
||||
removedPermissions: [ { itemId: model._id, name: WRITE_MODEL }]
|
||||
removedPermissions: [ { itemId: table._id, name: WRITE_TABLE }]
|
||||
})
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -178,7 +178,7 @@ describe("/accesslevels", () => {
|
|||
.expect(200)
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -2,8 +2,8 @@ const {
|
|||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
createModel,
|
||||
getAllFromModel,
|
||||
createTable,
|
||||
getAllFromTable,
|
||||
defaultHeaders,
|
||||
supertest,
|
||||
insertDocument,
|
||||
|
@ -126,10 +126,10 @@ describe("/automations", () => {
|
|||
|
||||
describe("create", () => {
|
||||
it("should setup the automation fully", () => {
|
||||
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
||||
let trigger = TRIGGER_DEFINITIONS["ROW_SAVED"]
|
||||
trigger.id = "wadiawdo34"
|
||||
let createAction = ACTION_DEFINITIONS["CREATE_RECORD"]
|
||||
createAction.inputs.record = {
|
||||
let createAction = ACTION_DEFINITIONS["CREATE_ROW"]
|
||||
createAction.inputs.row = {
|
||||
name: "{{trigger.name}}",
|
||||
description: "{{trigger.description}}"
|
||||
}
|
||||
|
@ -167,9 +167,9 @@ describe("/automations", () => {
|
|||
|
||||
describe("trigger", () => {
|
||||
it("trigger the automation successfully", async () => {
|
||||
let model = await createModel(request, app._id, instance._id)
|
||||
TEST_AUTOMATION.definition.trigger.inputs.modelId = model._id
|
||||
TEST_AUTOMATION.definition.steps[0].inputs.record.modelId = model._id
|
||||
let table = await createTable(request, app._id, instance._id)
|
||||
TEST_AUTOMATION.definition.trigger.inputs.tableId = table._id
|
||||
TEST_AUTOMATION.definition.steps[0].inputs.row.tableId = table._id
|
||||
await createAutomation()
|
||||
// this looks a bit mad but we don't actually have a way to wait for a response from the automation to
|
||||
// know that it has finished all of its actions - this is currently the best way
|
||||
|
@ -180,7 +180,7 @@ describe("/automations", () => {
|
|||
expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`)
|
||||
expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name)
|
||||
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
|
||||
if (elements.length === 1) {
|
||||
expect(elements.length).toEqual(1)
|
||||
|
@ -189,7 +189,7 @@ describe("/automations", () => {
|
|||
return
|
||||
}
|
||||
}
|
||||
throw "Failed to find the records"
|
||||
throw "Failed to find the rows"
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -39,13 +39,13 @@ exports.defaultHeaders = (appId, instanceId) => {
|
|||
}
|
||||
}
|
||||
|
||||
exports.createModel = async (request, appId, instanceId, model) => {
|
||||
if (model != null && model._id) {
|
||||
delete model._id
|
||||
exports.createTable = async (request, appId, instanceId, table) => {
|
||||
if (table != null && table._id) {
|
||||
delete table._id
|
||||
}
|
||||
model = model || {
|
||||
name: "TestModel",
|
||||
type: "model",
|
||||
table = table || {
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: {
|
||||
|
@ -64,23 +64,23 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
|||
}
|
||||
|
||||
const res = await request
|
||||
.post(`/api/models`)
|
||||
.post(`/api/tables`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
.send(model)
|
||||
.send(table)
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.getAllFromModel = async (request, appId, instanceId, modelId) => {
|
||||
exports.getAllFromTable = async (request, appId, instanceId, tableId) => {
|
||||
const res = await request
|
||||
.get(`/api/${modelId}/records`)
|
||||
.get(`/api/${tableId}/rows`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.createView = async (request, appId, instanceId, modelId, view) => {
|
||||
exports.createView = async (request, appId, instanceId, tableId, view) => {
|
||||
view = view || {
|
||||
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
|
||||
modelId: modelId,
|
||||
tableId: tableId,
|
||||
}
|
||||
|
||||
const res = await request
|
||||
|
|
|
@ -2,17 +2,17 @@ const {
|
|||
createApplication,
|
||||
createClientDatabase,
|
||||
createInstance,
|
||||
createModel,
|
||||
createTable,
|
||||
supertest,
|
||||
defaultHeaders,
|
||||
} = require("./couchTestUtils");
|
||||
|
||||
describe("/records", () => {
|
||||
describe("/rows", () => {
|
||||
let request
|
||||
let server
|
||||
let instance
|
||||
let model
|
||||
let record
|
||||
let table
|
||||
let row
|
||||
let app
|
||||
|
||||
beforeAll(async () => {
|
||||
|
@ -27,26 +27,26 @@ describe("/records", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
instance = await createInstance(request, app._id)
|
||||
model = await createModel(request, app._id, instance._id)
|
||||
record = {
|
||||
table = await createTable(request, app._id, instance._id)
|
||||
row = {
|
||||
name: "Test Contact",
|
||||
description: "original description",
|
||||
status: "new",
|
||||
modelId: model._id
|
||||
tableId: table._id
|
||||
}
|
||||
})
|
||||
|
||||
const createRecord = async r =>
|
||||
const createRow = async r =>
|
||||
await request
|
||||
.post(`/api/${r ? r.modelId : record.modelId}/records`)
|
||||
.send(r || record)
|
||||
.post(`/api/${r ? r.tableId : row.tableId}/rows`)
|
||||
.send(r || row)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const loadRecord = async id =>
|
||||
const loadRow = async id =>
|
||||
await request
|
||||
.get(`/api/${model._id}/records/${id}`)
|
||||
.get(`/api/${table._id}/rows/${id}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -55,114 +55,114 @@ describe("/records", () => {
|
|||
describe("save, load, update, delete", () => {
|
||||
|
||||
|
||||
it("returns a success message when the record is created", async () => {
|
||||
const res = await createRecord()
|
||||
expect(res.res.statusMessage).toEqual(`${model.name} created successfully`)
|
||||
it("returns a success message when the row is created", async () => {
|
||||
const res = await createRow()
|
||||
expect(res.res.statusMessage).toEqual(`${table.name} created successfully`)
|
||||
expect(res.body.name).toEqual("Test Contact")
|
||||
expect(res.body._rev).toBeDefined()
|
||||
})
|
||||
|
||||
it("updates a record successfully", async () => {
|
||||
const rec = await createRecord()
|
||||
it("updates a row successfully", async () => {
|
||||
const rec = await createRow()
|
||||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.post(`/api/${model._id}/records`)
|
||||
.post(`/api/${table._id}/rows`)
|
||||
.send({
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
modelId: model._id,
|
||||
tableId: table._id,
|
||||
name: "Updated Name",
|
||||
})
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.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")
|
||||
})
|
||||
|
||||
it("should load a record", async () => {
|
||||
const rec = await createRecord()
|
||||
it("should load a row", async () => {
|
||||
const rec = await createRow()
|
||||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${model._id}/records/${existing._id}`)
|
||||
.get(`/api/${table._id}/rows/${existing._id}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body).toEqual({
|
||||
...record,
|
||||
...row,
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
type: "record",
|
||||
type: "row",
|
||||
})
|
||||
})
|
||||
|
||||
it("should list all records for given modelId", async () => {
|
||||
const newRecord = {
|
||||
modelId: model._id,
|
||||
it("should list all rows for given tableId", async () => {
|
||||
const newRow = {
|
||||
tableId: table._id,
|
||||
name: "Second Contact",
|
||||
status: "new"
|
||||
}
|
||||
await createRecord()
|
||||
await createRecord(newRecord)
|
||||
await createRow()
|
||||
await createRow(newRow)
|
||||
|
||||
const res = await request
|
||||
.get(`/api/${model._id}/records`)
|
||||
.get(`/api/${table._id}/rows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.length).toBe(2)
|
||||
expect(res.body.find(r => r.name === newRecord.name)).toBeDefined()
|
||||
expect(res.body.find(r => r.name === record.name)).toBeDefined()
|
||||
expect(res.body.find(r => r.name === newRow.name)).toBeDefined()
|
||||
expect(res.body.find(r => r.name === row.name)).toBeDefined()
|
||||
})
|
||||
|
||||
it("lists records when queried by their ID", async () => {
|
||||
const newRecord = {
|
||||
modelId: model._id,
|
||||
it("lists rows when queried by their ID", async () => {
|
||||
const newRow = {
|
||||
tableId: table._id,
|
||||
name: "Second Contact",
|
||||
status: "new"
|
||||
}
|
||||
const record = await createRecord()
|
||||
const secondRecord = await createRecord(newRecord)
|
||||
const row = await createRow()
|
||||
const secondRow = await createRow(newRow)
|
||||
|
||||
const recordIds = [record.body._id, secondRecord.body._id]
|
||||
const rowIds = [row.body._id, secondRow.body._id]
|
||||
|
||||
const res = await request
|
||||
.post(`/api/records/search`)
|
||||
.post(`/api/rows/search`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.send({
|
||||
keys: recordIds
|
||||
keys: rowIds
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.length).toBe(2)
|
||||
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(recordIds))
|
||||
expect(res.body.map(response => response._id)).toEqual(expect.arrayContaining(rowIds))
|
||||
})
|
||||
|
||||
it("load should return 404 when record does not exist", async () => {
|
||||
await createRecord()
|
||||
it("load should return 404 when row does not exist", async () => {
|
||||
await createRow()
|
||||
await request
|
||||
.get(`/api/${model._id}/records/not-a-valid-id`)
|
||||
.get(`/api/${table._id}/rows/not-a-valid-id`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(404)
|
||||
})
|
||||
|
||||
it("record values are coerced", async () => {
|
||||
it("row values are coerced", async () => {
|
||||
const str = {type:"string", constraints: { type: "string", presence: false }}
|
||||
const attachment = {type:"attachment", constraints: { type: "array", presence: false }}
|
||||
const bool = {type:"boolean", constraints: { type: "boolean", presence: false }}
|
||||
const number = {type:"number", constraints: { type: "number", presence: false }}
|
||||
const datetime = {type:"datetime", constraints: { type: "string", presence: false, datetime: {earliest:"", latest: ""} }}
|
||||
|
||||
model = await createModel(request, app._id, instance._id, {
|
||||
name: "TestModel2",
|
||||
type: "model",
|
||||
table = await createTable(request, app._id, instance._id, {
|
||||
name: "TestTable2",
|
||||
type: "table",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: str,
|
||||
|
@ -189,8 +189,8 @@ describe("/records", () => {
|
|||
},
|
||||
})
|
||||
|
||||
record = {
|
||||
name: "Test Record",
|
||||
row = {
|
||||
name: "Test Row",
|
||||
stringUndefined: undefined,
|
||||
stringNull: null,
|
||||
stringString: "i am a string",
|
||||
|
@ -209,15 +209,15 @@ describe("/records", () => {
|
|||
boolUndefined: undefined,
|
||||
boolString: "true",
|
||||
boolBool: true,
|
||||
modelId: model._id,
|
||||
tableId: table._id,
|
||||
attachmentNull : null,
|
||||
attachmentUndefined : undefined,
|
||||
attachmentEmpty : "",
|
||||
}
|
||||
|
||||
const id = (await createRecord(record)).body._id
|
||||
const id = (await createRow(row)).body._id
|
||||
|
||||
const saved = (await loadRecord(id)).body
|
||||
const saved = (await loadRow(id)).body
|
||||
|
||||
expect(saved.stringUndefined).toBe(undefined)
|
||||
expect(saved.stringNull).toBe("")
|
||||
|
@ -230,8 +230,8 @@ describe("/records", () => {
|
|||
expect(saved.datetimeEmptyString).toBe(null)
|
||||
expect(saved.datetimeNull).toBe(null)
|
||||
expect(saved.datetimeUndefined).toBe(undefined)
|
||||
expect(saved.datetimeString).toBe(new Date(record.datetimeString).toISOString())
|
||||
expect(saved.datetimeDate).toBe(record.datetimeDate.toISOString())
|
||||
expect(saved.datetimeString).toBe(new Date(row.datetimeString).toISOString())
|
||||
expect(saved.datetimeDate).toBe(row.datetimeDate.toISOString())
|
||||
expect(saved.boolNull).toBe(null)
|
||||
expect(saved.boolEmpty).toBe(null)
|
||||
expect(saved.boolUndefined).toBe(undefined)
|
||||
|
@ -245,37 +245,37 @@ describe("/records", () => {
|
|||
|
||||
describe("patch", () => {
|
||||
it("should update only the fields that are supplied", async () => {
|
||||
const rec = await createRecord()
|
||||
const rec = await createRow()
|
||||
const existing = rec.body
|
||||
|
||||
const res = await request
|
||||
.patch(`/api/${model._id}/records/${existing._id}`)
|
||||
.patch(`/api/${table._id}/rows/${existing._id}`)
|
||||
.send({
|
||||
_id: existing._id,
|
||||
_rev: existing._rev,
|
||||
modelId: model._id,
|
||||
tableId: table._id,
|
||||
name: "Updated Name",
|
||||
})
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.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.description).toEqual(existing.description)
|
||||
|
||||
const savedRecord = await loadRecord(res.body._id)
|
||||
const savedRow = await loadRow(res.body._id)
|
||||
|
||||
expect(savedRecord.body.description).toEqual(existing.description)
|
||||
expect(savedRecord.body.name).toEqual("Updated Name")
|
||||
expect(savedRow.body.description).toEqual(existing.description)
|
||||
expect(savedRow.body.name).toEqual("Updated Name")
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe("validate", () => {
|
||||
it("should return no errors on valid record", async () => {
|
||||
it("should return no errors on valid row", async () => {
|
||||
const result = await request
|
||||
.post(`/api/${model._id}/records/validate`)
|
||||
.post(`/api/${table._id}/rows/validate`)
|
||||
.send({ name: "ivan" })
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -285,9 +285,9 @@ describe("/records", () => {
|
|||
expect(Object.keys(result.body.errors)).toEqual([])
|
||||
})
|
||||
|
||||
it("should errors on invalid record", async () => {
|
||||
it("should errors on invalid row", async () => {
|
||||
const result = await request
|
||||
.post(`/api/${model._id}/records/validate`)
|
||||
.post(`/api/${table._id}/rows/validate`)
|
||||
.send({ name: 1 })
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
|
@ -1,6 +1,6 @@
|
|||
const {
|
||||
createInstance,
|
||||
createModel,
|
||||
createTable,
|
||||
supertest,
|
||||
createClientDatabase,
|
||||
createApplication,
|
||||
|
@ -9,7 +9,7 @@ const {
|
|||
getDocument
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
describe("/models", () => {
|
||||
describe("/tables", () => {
|
||||
let request
|
||||
let server
|
||||
let app
|
||||
|
@ -30,11 +30,11 @@ describe("/models", () => {
|
|||
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
|
||||
.post(`/api/models`)
|
||||
.post(`/api/tables`)
|
||||
.send({
|
||||
name: "TestModel",
|
||||
name: "TestTable",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: { type: "string" }
|
||||
|
@ -44,17 +44,17 @@ describe("/models", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (err, res) => {
|
||||
expect(res.res.statusMessage).toEqual("Model TestModel saved successfully.");
|
||||
expect(res.body.name).toEqual("TestModel");
|
||||
expect(res.res.statusMessage).toEqual("Table TestTable saved successfully.");
|
||||
expect(res.body.name).toEqual("TestTable");
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it("renames all the record fields for a model when a schema key is renamed", async () => {
|
||||
const testModel = await createModel(request, app._id, instance._id);
|
||||
it("renames all the row fields for a table when a schema key is renamed", async () => {
|
||||
const testTable = await createTable(request, app._id, instance._id);
|
||||
|
||||
const testRecord = await request
|
||||
.post(`/api/${testModel._id}/records`)
|
||||
const testRow = await request
|
||||
.post(`/api/${testTable._id}/rows`)
|
||||
.send({
|
||||
name: "test"
|
||||
})
|
||||
|
@ -62,12 +62,12 @@ describe("/models", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const updatedModel = await request
|
||||
.post(`/api/models`)
|
||||
const updatedTable = await request
|
||||
.post(`/api/tables`)
|
||||
.send({
|
||||
_id: testModel._id,
|
||||
_rev: testModel._rev,
|
||||
name: "TestModel",
|
||||
_id: testTable._id,
|
||||
_rev: testTable._rev,
|
||||
name: "TestTable",
|
||||
key: "name",
|
||||
_rename: {
|
||||
old: "name",
|
||||
|
@ -81,11 +81,11 @@ describe("/models", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(updatedModel.res.statusMessage).toEqual("Model TestModel saved successfully.");
|
||||
expect(updatedModel.body.name).toEqual("TestModel");
|
||||
expect(updatedTable.res.statusMessage).toEqual("Table TestTable saved successfully.");
|
||||
expect(updatedTable.body.name).toEqual("TestTable");
|
||||
|
||||
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))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -98,11 +98,11 @@ describe("/models", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "POST",
|
||||
url: `/api/models`,
|
||||
url: `/api/tables`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
body: {
|
||||
name: "TestModel",
|
||||
name: "TestTable",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: { type: "string" }
|
||||
|
@ -113,27 +113,27 @@ describe("/models", () => {
|
|||
});
|
||||
|
||||
describe("fetch", () => {
|
||||
let testModel
|
||||
let testTable
|
||||
|
||||
beforeEach(async () => {
|
||||
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(() => {
|
||||
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
|
||||
.get(`/api/models`)
|
||||
.get(`/api/tables`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
const fetchedModel = res.body[0];
|
||||
expect(fetchedModel.name).toEqual(testModel.name);
|
||||
expect(fetchedModel.type).toEqual("model");
|
||||
const fetchedTable = res.body[0];
|
||||
expect(fetchedTable.name).toEqual(testTable.name);
|
||||
expect(fetchedTable.type).toEqual("table");
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
@ -142,7 +142,7 @@ describe("/models", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "GET",
|
||||
url: `/api/models`,
|
||||
url: `/api/tables`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
||||
|
@ -150,33 +150,33 @@ describe("/models", () => {
|
|||
});
|
||||
|
||||
describe("destroy", () => {
|
||||
let testModel;
|
||||
let testTable;
|
||||
|
||||
beforeEach(async () => {
|
||||
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(() => {
|
||||
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
|
||||
.delete(`/api/models/${testModel._id}/${testModel._rev}`)
|
||||
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`);
|
||||
expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`);
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
||||
it("deletes linked references to the model after deletion", async done => {
|
||||
const linkedModel = await createModel(request, app._id, instance._id, {
|
||||
name: "LinkedModel",
|
||||
type: "model",
|
||||
it("deletes linked references to the table after deletion", async done => {
|
||||
const linkedTable = await createTable(request, app._id, instance._id, {
|
||||
name: "LinkedTable",
|
||||
type: "table",
|
||||
key: "name",
|
||||
schema: {
|
||||
name: {
|
||||
|
@ -185,9 +185,9 @@ describe("/models", () => {
|
|||
type: "string",
|
||||
},
|
||||
},
|
||||
TestModel: {
|
||||
TestTable: {
|
||||
type: "link",
|
||||
modelId: testModel._id,
|
||||
tableId: testTable._id,
|
||||
constraints: {
|
||||
type: "array"
|
||||
}
|
||||
|
@ -196,14 +196,14 @@ describe("/models", () => {
|
|||
})
|
||||
|
||||
request
|
||||
.delete(`/api/models/${testModel._id}/${testModel._rev}`)
|
||||
.delete(`/api/tables/${testTable._id}/${testTable._rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
.end(async (_, res) => {
|
||||
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`);
|
||||
const dependentModel = await getDocument(instance._id, linkedModel._id)
|
||||
expect(dependentModel.schema.TestModel).not.toBeDefined();
|
||||
expect(res.res.statusMessage).toEqual(`Table ${testTable._id} deleted.`);
|
||||
const dependentTable = await getDocument(instance._id, linkedTable._id)
|
||||
expect(dependentTable.schema.TestTable).not.toBeDefined();
|
||||
done();
|
||||
});
|
||||
})
|
||||
|
@ -212,7 +212,7 @@ describe("/models", () => {
|
|||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
url: `/api/models/${testModel._id}/${testModel._rev}`,
|
||||
url: `/api/tables/${testTable._id}/${testTable._rev}`,
|
||||
instanceId: instance._id,
|
||||
appId: app._id,
|
||||
})
|
|
@ -2,7 +2,7 @@ const {
|
|||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
createModel,
|
||||
createTable,
|
||||
supertest,
|
||||
defaultHeaders,
|
||||
getDocument
|
||||
|
@ -13,12 +13,12 @@ describe("/views", () => {
|
|||
let server
|
||||
let app
|
||||
let instance
|
||||
let model
|
||||
let table
|
||||
|
||||
const createView = async (config = {
|
||||
name: "TestView",
|
||||
field: "Price",
|
||||
modelId: model._id
|
||||
tableId: table._id
|
||||
}) =>
|
||||
await request
|
||||
.post(`/api/views`)
|
||||
|
@ -27,9 +27,9 @@ describe("/views", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
const createRecord = async record => request
|
||||
.post(`/api/${model._id}/records`)
|
||||
.send(record)
|
||||
const createRow = async row => request
|
||||
.post(`/api/${table._id}/rows`)
|
||||
.send(row)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
@ -50,7 +50,7 @@ describe("/views", () => {
|
|||
|
||||
describe("create", () => {
|
||||
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 () => {
|
||||
|
@ -58,14 +58,14 @@ describe("/views", () => {
|
|||
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()
|
||||
expect(res.res.statusMessage).toEqual("View TestView saved successfully.");
|
||||
const updatedModel = await getDocument(instance._id, model._id)
|
||||
expect(updatedModel.views).toEqual({
|
||||
const updatedTable = await getDocument(instance._id, table._id)
|
||||
expect(updatedTable.views).toEqual({
|
||||
TestView: {
|
||||
field: "Price",
|
||||
modelId: model._id,
|
||||
tableId: table._id,
|
||||
filters: [],
|
||||
schema: {
|
||||
name: {
|
||||
|
@ -88,7 +88,7 @@ describe("/views", () => {
|
|||
|
||||
describe("fetch", () => {
|
||||
beforeEach(async () => {
|
||||
model = await createModel(request, app._id, instance._id);
|
||||
table = await createTable(request, app._id, instance._id);
|
||||
});
|
||||
|
||||
it("returns only custom views", async () => {
|
||||
|
@ -105,21 +105,21 @@ describe("/views", () => {
|
|||
|
||||
describe("query", () => {
|
||||
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 () => {
|
||||
await createView()
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
await createRow({
|
||||
tableId: table._id,
|
||||
Price: 1000
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
await createRow({
|
||||
tableId: table._id,
|
||||
Price: 2000
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
await createRow({
|
||||
tableId: table._id,
|
||||
Price: 4000
|
||||
})
|
||||
const res = await request
|
||||
|
@ -136,20 +136,20 @@ describe("/views", () => {
|
|||
name: "TestView",
|
||||
field: "Price",
|
||||
groupBy: "Category",
|
||||
modelId: model._id
|
||||
tableId: table._id
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
await createRow({
|
||||
tableId: table._id,
|
||||
Price: 1000,
|
||||
Category: "One"
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
await createRow({
|
||||
tableId: table._id,
|
||||
Price: 2000,
|
||||
Category: "One"
|
||||
})
|
||||
await createRecord({
|
||||
modelId: model._id,
|
||||
await createRow({
|
||||
tableId: table._id,
|
||||
Price: 4000,
|
||||
Category: "Two"
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue