WIP - start of auto columns like autonumber, createdBy, createdAt, updatedBy etc.
This commit is contained in:
parent
4f1546d057
commit
f4a503d015
|
@ -6,8 +6,6 @@
|
||||||
Input,
|
Input,
|
||||||
Label,
|
Label,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Button,
|
|
||||||
Spacer,
|
|
||||||
Toggle,
|
Toggle,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
|
@ -28,6 +26,13 @@
|
||||||
let dataImport
|
let dataImport
|
||||||
let error = ""
|
let error = ""
|
||||||
let createAutoscreens = true
|
let createAutoscreens = true
|
||||||
|
let autoColumns = {
|
||||||
|
createdBy: false,
|
||||||
|
createdAt: false,
|
||||||
|
updatedBy: false,
|
||||||
|
updatedAt: false,
|
||||||
|
autoNumber: false,
|
||||||
|
}
|
||||||
|
|
||||||
function checkValid(evt) {
|
function checkValid(evt) {
|
||||||
const tableName = evt.target.value
|
const tableName = evt.target.value
|
||||||
|
@ -42,6 +47,7 @@
|
||||||
let newTable = {
|
let newTable = {
|
||||||
name,
|
name,
|
||||||
schema: dataImport.schema || {},
|
schema: dataImport.schema || {},
|
||||||
|
autoColumns,
|
||||||
dataImport,
|
dataImport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +99,30 @@
|
||||||
on:input={checkValid}
|
on:input={checkValid}
|
||||||
bind:value={name}
|
bind:value={name}
|
||||||
{error} />
|
{error} />
|
||||||
|
<div class="autocolumns">
|
||||||
|
<label>Auto columns</label>
|
||||||
|
<div class="toggles">
|
||||||
|
<div class="toggle-1">
|
||||||
|
<Toggle
|
||||||
|
text="Created by"
|
||||||
|
bind:checked={autoColumns.createdBy} />
|
||||||
|
<Toggle
|
||||||
|
text="Created at"
|
||||||
|
bind:checked={autoColumns.createdAt} />
|
||||||
|
<Toggle
|
||||||
|
text="Autonumber"
|
||||||
|
bind:checked={autoColumns.autoNumber} />
|
||||||
|
</div>
|
||||||
|
<div class="toggle-2">
|
||||||
|
<Toggle
|
||||||
|
text="Updated by"
|
||||||
|
bind:checked={autoColumns.updatedBy} />
|
||||||
|
<Toggle
|
||||||
|
text="Updated at"
|
||||||
|
bind:checked={autoColumns.updatedAt} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Toggle
|
<Toggle
|
||||||
text="Generate screens in the design section"
|
text="Generate screens in the design section"
|
||||||
bind:checked={createAutoscreens} />
|
bind:checked={createAutoscreens} />
|
||||||
|
@ -101,3 +131,25 @@
|
||||||
<TableDataImport bind:dataImport />
|
<TableDataImport bind:dataImport />
|
||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.autocolumns {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 3px solid var(--grey-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggles {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-1 :global(> *) {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.toggle-2 :global(> *) {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -9,7 +9,10 @@ const {
|
||||||
ViewNames,
|
ViewNames,
|
||||||
} = require("../../db/utils")
|
} = require("../../db/utils")
|
||||||
const usersController = require("./user")
|
const usersController = require("./user")
|
||||||
const { coerceRowValues, enrichRows } = require("../../utilities")
|
const {
|
||||||
|
inputProcessing,
|
||||||
|
outputProcessing,
|
||||||
|
} = require("../../utilities/rowProcessor")
|
||||||
|
|
||||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||||
|
|
||||||
|
@ -64,7 +67,7 @@ exports.patch = async function(ctx) {
|
||||||
row[key] = patchfields[key]
|
row[key] = patchfields[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
row = coerceRowValues(row, table)
|
row = inputProcessing(ctx.user, table, row)
|
||||||
|
|
||||||
const validateResult = await validate({
|
const validateResult = await validate({
|
||||||
row,
|
row,
|
||||||
|
@ -134,7 +137,7 @@ exports.save = async function(ctx) {
|
||||||
|
|
||||||
const table = await db.get(row.tableId)
|
const table = await db.get(row.tableId)
|
||||||
|
|
||||||
row = coerceRowValues(row, table)
|
row = inputProcessing(ctx.user, table, row)
|
||||||
|
|
||||||
const validateResult = await validate({
|
const validateResult = await validate({
|
||||||
row,
|
row,
|
||||||
|
@ -204,7 +207,7 @@ exports.fetchView = async function(ctx) {
|
||||||
schema: {},
|
schema: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.body = await enrichRows(appId, table, response.rows)
|
ctx.body = await outputProcessing(appId, table, response.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calculation === CALCULATION_TYPES.STATS) {
|
if (calculation === CALCULATION_TYPES.STATS) {
|
||||||
|
@ -247,7 +250,7 @@ exports.fetchTableRows = async function(ctx) {
|
||||||
)
|
)
|
||||||
rows = response.rows.map(row => row.doc)
|
rows = response.rows.map(row => row.doc)
|
||||||
}
|
}
|
||||||
ctx.body = await enrichRows(appId, table, rows)
|
ctx.body = await outputProcessing(appId, table, rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
|
@ -256,7 +259,7 @@ exports.find = async function(ctx) {
|
||||||
try {
|
try {
|
||||||
const table = await db.get(ctx.params.tableId)
|
const table = await db.get(ctx.params.tableId)
|
||||||
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
|
const row = await findRow(db, appId, ctx.params.tableId, ctx.params.rowId)
|
||||||
ctx.body = await enrichRows(appId, table, row)
|
ctx.body = await outputProcessing(appId, table, row)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.throw(400, err)
|
ctx.throw(400, err)
|
||||||
}
|
}
|
||||||
|
@ -341,7 +344,7 @@ exports.fetchEnrichedRow = async function(ctx) {
|
||||||
keys: linkVals.map(linkVal => linkVal.id),
|
keys: linkVals.map(linkVal => linkVal.id),
|
||||||
})
|
})
|
||||||
// need to include the IDs in these rows for any links they may have
|
// need to include the IDs in these rows for any links they may have
|
||||||
let linkedRows = await enrichRows(
|
let linkedRows = await outputProcessing(
|
||||||
appId,
|
appId,
|
||||||
table,
|
table,
|
||||||
response.rows.map(row => row.doc)
|
response.rows.map(row => row.doc)
|
||||||
|
|
|
@ -7,9 +7,31 @@ const {
|
||||||
PermissionLevels,
|
PermissionLevels,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
} = require("../../utilities/security/permissions")
|
} = require("../../utilities/security/permissions")
|
||||||
|
const joiValidator = require("../../middleware/joi-validator")
|
||||||
|
const Joi = require("joi")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
function generateSaveValidator() {
|
||||||
|
// prettier-ignore
|
||||||
|
return joiValidator.body(Joi.object({
|
||||||
|
_id: Joi.string(),
|
||||||
|
_rev: Joi.string(),
|
||||||
|
type: Joi.string().valid("table"),
|
||||||
|
primaryDisplay: Joi.string(),
|
||||||
|
schema: Joi.object().required(),
|
||||||
|
name: Joi.string().required(),
|
||||||
|
views: Joi.object(),
|
||||||
|
autoColumns: Joi.object({
|
||||||
|
createdBy: Joi.boolean(),
|
||||||
|
createdAt: Joi.boolean(),
|
||||||
|
updatedBy: Joi.boolean(),
|
||||||
|
updatedAt: Joi.boolean(),
|
||||||
|
}),
|
||||||
|
dataImport: Joi.object(),
|
||||||
|
}).unknown(true))
|
||||||
|
}
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/tables", authorized(BUILDER), tableController.fetch)
|
.get("/api/tables", authorized(BUILDER), tableController.fetch)
|
||||||
.get(
|
.get(
|
||||||
|
@ -23,6 +45,7 @@ router
|
||||||
// allows control over updating a table
|
// allows control over updating a table
|
||||||
bodyResource("_id"),
|
bodyResource("_id"),
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
|
generateSaveValidator(),
|
||||||
tableController.save
|
tableController.save
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
|
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../db")
|
||||||
const emitter = require("../events/index")
|
const emitter = require("../events/index")
|
||||||
const InMemoryQueue = require("../utilities/queue/inMemoryQueue")
|
const InMemoryQueue = require("../utilities/queue/inMemoryQueue")
|
||||||
const { getAutomationParams } = require("../db/utils")
|
const { getAutomationParams } = require("../db/utils")
|
||||||
const { coerceValue } = require("../utilities")
|
const { coerce } = require("../utilities/rowProcessor")
|
||||||
|
|
||||||
let automationQueue = new InMemoryQueue("automationQueue")
|
let automationQueue = new InMemoryQueue("automationQueue")
|
||||||
|
|
||||||
|
@ -240,8 +240,8 @@ module.exports.externalTrigger = async function(automation, params) {
|
||||||
// values are likely to be submitted as strings, so we shall convert to correct type
|
// values are likely to be submitted as strings, so we shall convert to correct type
|
||||||
const coercedFields = {}
|
const coercedFields = {}
|
||||||
const fields = automation.definition.trigger.inputs.fields
|
const fields = automation.definition.trigger.inputs.fields
|
||||||
for (let key in fields) {
|
for (let key of Object.keys(fields)) {
|
||||||
coercedFields[key] = coerceValue(params.fields[key], fields[key])
|
coercedFields[key] = coerce(params.fields[key], fields[key])
|
||||||
}
|
}
|
||||||
params.fields = coercedFields
|
params.fields = coercedFields
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,60 +3,9 @@ const { DocumentTypes, SEPARATOR } = require("../db/utils")
|
||||||
const fs = require("fs")
|
const fs = require("fs")
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const CouchDB = require("../db")
|
const CouchDB = require("../db")
|
||||||
const { OBJ_STORE_DIRECTORY } = require("../constants")
|
|
||||||
const linkRows = require("../db/linkedRows")
|
|
||||||
|
|
||||||
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
const APP_PREFIX = DocumentTypes.APP + SEPARATOR
|
||||||
|
|
||||||
/**
|
|
||||||
* A map of how we convert various properties in rows to each other based on the row type.
|
|
||||||
*/
|
|
||||||
const TYPE_TRANSFORM_MAP = {
|
|
||||||
link: {
|
|
||||||
"": [],
|
|
||||||
[null]: [],
|
|
||||||
[undefined]: undefined,
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
"": "",
|
|
||||||
[null]: "",
|
|
||||||
[undefined]: undefined,
|
|
||||||
},
|
|
||||||
string: {
|
|
||||||
"": "",
|
|
||||||
[null]: "",
|
|
||||||
[undefined]: undefined,
|
|
||||||
},
|
|
||||||
longform: {
|
|
||||||
"": "",
|
|
||||||
[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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmAppId(possibleAppId) {
|
function confirmAppId(possibleAppId) {
|
||||||
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
return possibleAppId && possibleAppId.startsWith(APP_PREFIX)
|
||||||
? possibleAppId
|
? possibleAppId
|
||||||
|
@ -159,43 +108,6 @@ exports.walkDir = (dirPath, callback) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This will coerce a value to the correct types based on the type transform map
|
|
||||||
* @param {object} row The value to coerce
|
|
||||||
* @param {object} type The type fo coerce to
|
|
||||||
* @returns {object} The coerced value
|
|
||||||
*/
|
|
||||||
exports.coerceValue = (value, type) => {
|
|
||||||
// eslint-disable-next-line no-prototype-builtins
|
|
||||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) {
|
|
||||||
return TYPE_TRANSFORM_MAP[type][value]
|
|
||||||
} else if (TYPE_TRANSFORM_MAP[type].parse) {
|
|
||||||
return TYPE_TRANSFORM_MAP[type].parse(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will coerce the values in a row to the correct types based on the type transform map and the
|
|
||||||
* table schema.
|
|
||||||
* @param {object} row The row which is to be coerced to correct values based on schema, this input
|
|
||||||
* row will not be updated.
|
|
||||||
* @param {object} table The table that has been retrieved from DB, this must contain the expected
|
|
||||||
* schema for the rows.
|
|
||||||
* @returns {object} The updated row will be returned with all values coerced.
|
|
||||||
*/
|
|
||||||
exports.coerceRowValues = (row, table) => {
|
|
||||||
const clonedRow = cloneDeep(row)
|
|
||||||
for (let [key, value] of Object.entries(clonedRow)) {
|
|
||||||
const field = table.schema[key]
|
|
||||||
if (!field) continue
|
|
||||||
|
|
||||||
clonedRow[key] = exports.coerceValue(value, field.type)
|
|
||||||
}
|
|
||||||
return clonedRow
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getLogoUrl = () => {
|
exports.getLogoUrl = () => {
|
||||||
return "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
return "https://d33wubrfki0l68.cloudfront.net/aac32159d7207b5085e74a7ef67afbb7027786c5/2b1fd/img/logo/bb-emblem.svg"
|
||||||
}
|
}
|
||||||
|
@ -213,34 +125,3 @@ exports.getAllApps = async () => {
|
||||||
.map(({ value }) => value)
|
.map(({ value }) => value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function "enriches" the input rows with anything they are supposed to contain, for example
|
|
||||||
* link records or attachment links.
|
|
||||||
* @param {string} appId the ID of the application for which rows are being enriched.
|
|
||||||
* @param {object} table the table from which these rows came from originally, this is used to determine
|
|
||||||
* the schema of the rows and then enrich.
|
|
||||||
* @param {object[]} rows the rows which are to be enriched.
|
|
||||||
* @returns {object[]} the enriched rows will be returned.
|
|
||||||
*/
|
|
||||||
exports.enrichRows = async (appId, table, rows) => {
|
|
||||||
// attach any linked row information
|
|
||||||
const enriched = await linkRows.attachLinkInfo(appId, rows)
|
|
||||||
// update the attachments URL depending on hosting
|
|
||||||
if (env.CLOUD && env.SELF_HOSTED) {
|
|
||||||
for (let [property, column] of Object.entries(table.schema)) {
|
|
||||||
if (column.type === "attachment") {
|
|
||||||
for (let row of enriched) {
|
|
||||||
if (row[property] == null || row[property].length === 0) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
row[property].forEach(attachment => {
|
|
||||||
attachment.url = `${OBJ_STORE_DIRECTORY}/${appId}/${attachment.url}`
|
|
||||||
attachment.url = attachment.url.replace("//", "/")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return enriched
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
const env = require("../environment")
|
||||||
|
const { OBJ_STORE_DIRECTORY } = require("../constants")
|
||||||
|
const linkRows = require("../db/linkedRows")
|
||||||
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A map of how we convert various properties in rows to each other based on the row type.
|
||||||
|
*/
|
||||||
|
const TYPE_TRANSFORM_MAP = {
|
||||||
|
link: {
|
||||||
|
"": [],
|
||||||
|
[null]: [],
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
"": "",
|
||||||
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
string: {
|
||||||
|
"": "",
|
||||||
|
[null]: "",
|
||||||
|
[undefined]: undefined,
|
||||||
|
},
|
||||||
|
longform: {
|
||||||
|
"": "",
|
||||||
|
[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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will coerce a value to the correct types based on the type transform map
|
||||||
|
* @param {any} value The value to coerce
|
||||||
|
* @param {string} type The type fo coerce to
|
||||||
|
* @returns {any} The coerced value
|
||||||
|
*/
|
||||||
|
exports.coerce = (value, type) => {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(value)) {
|
||||||
|
return TYPE_TRANSFORM_MAP[type][value]
|
||||||
|
} else if (TYPE_TRANSFORM_MAP[type].parse) {
|
||||||
|
return TYPE_TRANSFORM_MAP[type].parse(value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an input route this function will apply all the necessary pre-processing to it, such as coercion
|
||||||
|
* of column values or adding auto-column values.
|
||||||
|
* @param {object} user the user which is performing the input.
|
||||||
|
* @param {object} row the row which is being created/updated.
|
||||||
|
* @param {object} table the table which the row is being saved to.
|
||||||
|
* @returns {object} the row which has been prepared to be written to the DB.
|
||||||
|
*/
|
||||||
|
exports.inputProcessing = (user, table, row) => {
|
||||||
|
const clonedRow = cloneDeep(row)
|
||||||
|
for (let [key, value] of Object.entries(clonedRow)) {
|
||||||
|
const field = table.schema[key]
|
||||||
|
if (!field) continue
|
||||||
|
|
||||||
|
clonedRow[key] = exports.coerce(value, field.type)
|
||||||
|
}
|
||||||
|
return clonedRow
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function enriches the input rows with anything they are supposed to contain, for example
|
||||||
|
* link records or attachment links.
|
||||||
|
* @param {string} appId the ID of the application for which rows are being enriched.
|
||||||
|
* @param {object} table the table from which these rows came from originally, this is used to determine
|
||||||
|
* the schema of the rows and then enrich.
|
||||||
|
* @param {object[]} rows the rows which are to be enriched.
|
||||||
|
* @returns {object[]} the enriched rows will be returned.
|
||||||
|
*/
|
||||||
|
exports.outputProcessing = async (appId, table, rows) => {
|
||||||
|
// attach any linked row information
|
||||||
|
const outputRows = await linkRows.attachLinkInfo(appId, rows)
|
||||||
|
// update the attachments URL depending on hosting
|
||||||
|
if (env.CLOUD && env.SELF_HOSTED) {
|
||||||
|
for (let [property, column] of Object.entries(table.schema)) {
|
||||||
|
if (column.type === "attachment") {
|
||||||
|
for (let row of outputRows) {
|
||||||
|
if (row[property] == null || row[property].length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[property].forEach(attachment => {
|
||||||
|
attachment.url = `${OBJ_STORE_DIRECTORY}/${appId}/${attachment.url}`
|
||||||
|
attachment.url = attachment.url.replace("//", "/")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputRows
|
||||||
|
}
|
Loading…
Reference in New Issue