Work in progress, getting the server backend mostly ready for this work.
This commit is contained in:
parent
6bc9123a86
commit
4b1855974c
|
@ -14,8 +14,7 @@
|
|||
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"
|
||||
import { FIELDS } from "constants/backend"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { AUTO_COLUMN_SUB_TYPES, buildAutoColumn } from "constants/backend"
|
||||
|
||||
const defaultScreens = [
|
||||
NEW_ROW_TEMPLATE,
|
||||
|
@ -23,33 +22,34 @@
|
|||
ROW_LIST_TEMPLATE,
|
||||
]
|
||||
|
||||
$: tableNames = $backendUiStore.tables.map(table => table.name)
|
||||
|
||||
let modal
|
||||
let name
|
||||
let dataImport
|
||||
let error = ""
|
||||
let createAutoscreens = true
|
||||
let autoColumns = {
|
||||
createdBy: true,
|
||||
createdAt: true,
|
||||
updatedBy: true,
|
||||
updatedAt: true,
|
||||
autoID: true,
|
||||
[AUTO_COLUMN_SUB_TYPES.AUTO_ID]: {enabled: true, name: "Auto ID"},
|
||||
[AUTO_COLUMN_SUB_TYPES.CREATED_BY]: {enabled: true, name: "Created By"},
|
||||
[AUTO_COLUMN_SUB_TYPES.CREATED_AT]: {enabled: true, name: "Created At"},
|
||||
[AUTO_COLUMN_SUB_TYPES.UPDATED_BY]: {enabled: true, name: "Updated By"},
|
||||
[AUTO_COLUMN_SUB_TYPES.UPDATED_AT]: {enabled: true, name: "Updated At"},
|
||||
}
|
||||
|
||||
function addAutoColumns(schema) {
|
||||
for (let [property, enabled] of Object.entries(autoColumns)) {
|
||||
if (!enabled) {
|
||||
function addAutoColumns(tableName, schema) {
|
||||
for (let [subtype, col] of Object.entries(autoColumns)) {
|
||||
if (!col.enabled) {
|
||||
continue
|
||||
}
|
||||
const autoColDef = cloneDeep(FIELDS.AUTO)
|
||||
autoColDef.subtype = property
|
||||
schema[property] = autoColDef
|
||||
schema[col.name] = buildAutoColumn(tableName, col.name, subtype)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
function checkValid(evt) {
|
||||
const tableName = evt.target.value
|
||||
if ($backendUiStore.models?.some(model => model.name === tableName)) {
|
||||
if (tableNames.includes(tableName)) {
|
||||
error = `Table with name ${tableName} already exists. Please choose another name.`
|
||||
return
|
||||
}
|
||||
|
@ -59,7 +59,7 @@
|
|||
async function saveTable() {
|
||||
let newTable = {
|
||||
name,
|
||||
schema: addAutoColumns(dataImport.schema || {}),
|
||||
schema: addAutoColumns(name, dataImport.schema || {}),
|
||||
dataImport,
|
||||
}
|
||||
|
||||
|
|
|
@ -80,13 +80,14 @@ export const FIELDS = {
|
|||
presence: false,
|
||||
},
|
||||
},
|
||||
AUTO: {
|
||||
name: "Auto Column",
|
||||
icon: "ri-magic-line",
|
||||
type: "auto",
|
||||
// no constraints for auto-columns
|
||||
// these are fully created serverside
|
||||
}
|
||||
}
|
||||
|
||||
export const AUTO_COLUMN_SUB_TYPES = {
|
||||
CREATED_BY: "createdBy",
|
||||
CREATED_AT: "createdAt",
|
||||
UPDATED_BY: "updatedBy",
|
||||
UPDATED_AT: "updatedAt",
|
||||
AUTO_ID: "autoID",
|
||||
}
|
||||
|
||||
export const FILE_TYPES = {
|
||||
|
@ -107,3 +108,43 @@ export const Roles = {
|
|||
PUBLIC: "PUBLIC",
|
||||
BUILDER: "BUILDER",
|
||||
}
|
||||
|
||||
export const USER_TABLE_ID = "ta_users"
|
||||
|
||||
export function isAutoColumnUserRelationship(subtype) {
|
||||
return subtype === AUTO_COLUMN_SUB_TYPES.CREATED_BY ||
|
||||
subtype === AUTO_COLUMN_SUB_TYPES.UPDATED_BY
|
||||
}
|
||||
|
||||
export function buildAutoColumn(tableName, name, subtype) {
|
||||
let type
|
||||
switch (subtype) {
|
||||
case AUTO_COLUMN_SUB_TYPES.UPDATED_BY:
|
||||
case AUTO_COLUMN_SUB_TYPES.CREATED_BY:
|
||||
type = FIELDS.LINK.type
|
||||
break
|
||||
case AUTO_COLUMN_SUB_TYPES.AUTO_ID:
|
||||
type = FIELDS.NUMBER.type
|
||||
break
|
||||
default:
|
||||
type = FIELDS.STRING.type
|
||||
break
|
||||
}
|
||||
if (Object.values(AUTO_COLUMN_SUB_TYPES).indexOf(subtype) === -1) {
|
||||
throw "Cannot build auto column with supplied subtype"
|
||||
}
|
||||
const base = {
|
||||
name,
|
||||
type,
|
||||
subtype,
|
||||
icon: "ri-magic-line",
|
||||
autocolumn: true,
|
||||
// no constraints, this should never have valid inputs
|
||||
constraints: {},
|
||||
}
|
||||
if (isAutoColumnUserRelationship(subtype)) {
|
||||
base.tableId = USER_TABLE_ID
|
||||
base.fieldName = `${tableName}-${name}`
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
|
|
@ -58,18 +58,17 @@ async function findRow(db, appId, tableId, rowId) {
|
|||
exports.patch = async function(ctx) {
|
||||
const appId = ctx.user.appId
|
||||
const db = new CouchDB(appId)
|
||||
let row = await db.get(ctx.params.rowId)
|
||||
const table = await db.get(row.tableId)
|
||||
let dbRow = await db.get(ctx.params.rowId)
|
||||
let dbTable = await db.get(dbRow.tableId)
|
||||
const patchfields = ctx.request.body
|
||||
|
||||
// need to build up full patch fields before coerce
|
||||
for (let key of Object.keys(patchfields)) {
|
||||
if (!table.schema[key]) continue
|
||||
row[key] = patchfields[key]
|
||||
if (!dbTable.schema[key]) continue
|
||||
dbRow[key] = patchfields[key]
|
||||
}
|
||||
|
||||
row = inputProcessing(ctx.user, table, row)
|
||||
|
||||
// this returns the table and row incase they have been updated
|
||||
let { table, row } = await inputProcessing(ctx.user, dbTable, dbRow)
|
||||
const validateResult = await validate({
|
||||
row,
|
||||
table,
|
||||
|
@ -114,32 +113,34 @@ exports.patch = async function(ctx) {
|
|||
exports.save = async function(ctx) {
|
||||
const appId = ctx.user.appId
|
||||
const db = new CouchDB(appId)
|
||||
let row = ctx.request.body
|
||||
row.tableId = ctx.params.tableId
|
||||
let inputs = ctx.request.body
|
||||
inputs.tableId = ctx.params.tableId
|
||||
|
||||
// TODO: find usage of this and break out into own endpoint
|
||||
if (ctx.request.body.type === "delete") {
|
||||
if (inputs.type === "delete") {
|
||||
await bulkDelete(ctx)
|
||||
ctx.body = ctx.request.body.rows
|
||||
ctx.body = inputs.rows
|
||||
return
|
||||
}
|
||||
|
||||
// if the row obj had an _id then it will have been retrieved
|
||||
const existingRow = ctx.preExisting
|
||||
if (existingRow) {
|
||||
ctx.params.rowId = row._id
|
||||
ctx.params.rowId = inputs._id
|
||||
await exports.patch(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
if (!row._rev && !row._id) {
|
||||
row._id = generateRowID(row.tableId)
|
||||
if (!inputs._rev && !inputs._id) {
|
||||
inputs._id = generateRowID(inputs.tableId)
|
||||
}
|
||||
|
||||
const table = await db.get(row.tableId)
|
||||
|
||||
row = inputProcessing(ctx.user, table, row)
|
||||
|
||||
// this returns the table and row incase they have been updated
|
||||
let { table, row } = await inputProcessing(
|
||||
ctx.user,
|
||||
await db.get(inputs.tableId),
|
||||
inputs
|
||||
)
|
||||
const validateResult = await validate({
|
||||
row,
|
||||
table,
|
||||
|
|
|
@ -51,6 +51,14 @@ exports.FieldTypes = {
|
|||
AUTO: "auto",
|
||||
}
|
||||
|
||||
exports.AutoFieldSubTypes = {
|
||||
CREATED_BY: "createdBy",
|
||||
CREATED_AT: "createdAt",
|
||||
UPDATED_BY: "updatedBy",
|
||||
UPDATED_AT: "updatedAt",
|
||||
AUTO_ID: "autoID",
|
||||
}
|
||||
|
||||
exports.AuthTypes = AuthTypes
|
||||
exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA
|
||||
exports.BUILDER_CONFIG_DB = "builder-config-db"
|
||||
|
|
|
@ -25,7 +25,14 @@ function LinkDocument(
|
|||
rowId2
|
||||
) {
|
||||
// build the ID out of unique references to this link document
|
||||
this._id = generateLinkID(tableId1, tableId2, rowId1, rowId2)
|
||||
this._id = generateLinkID(
|
||||
tableId1,
|
||||
tableId2,
|
||||
rowId1,
|
||||
rowId2,
|
||||
fieldName1,
|
||||
fieldName2
|
||||
)
|
||||
// required for referencing in view
|
||||
this.type = FieldTypes.LINK
|
||||
this.doc1 = {
|
||||
|
|
|
@ -5,7 +5,7 @@ const {
|
|||
createLinkView,
|
||||
getUniqueByProp,
|
||||
} = require("./linkUtils")
|
||||
const _ = require("lodash")
|
||||
const { flatten } = require("lodash")
|
||||
|
||||
/**
|
||||
* This functionality makes sure that when rows with links are created, updated or deleted they are processed
|
||||
|
@ -101,7 +101,7 @@ exports.attachLinkInfo = async (appId, rows) => {
|
|||
}
|
||||
let tableIds = [...new Set(rows.map(el => el.tableId))]
|
||||
// start by getting all the link values for performance reasons
|
||||
let responses = _.flatten(
|
||||
let responses = flatten(
|
||||
await Promise.all(
|
||||
tableIds.map(tableId =>
|
||||
getLinkDocuments({
|
||||
|
|
|
@ -138,10 +138,22 @@ exports.generateAutomationID = () => {
|
|||
* @param {string} tableId2 The ID of the linked table.
|
||||
* @param {string} rowId1 The ID of the linker row.
|
||||
* @param {string} rowId2 The ID of the linked row.
|
||||
* @param {string} fieldName1 The name of the field in the linker row.
|
||||
* @param {string} fieldName2 the name of the field in the linked row.
|
||||
* @returns {string} The new link doc ID which the automation doc can be stored under.
|
||||
*/
|
||||
exports.generateLinkID = (tableId1, tableId2, rowId1, rowId2) => {
|
||||
return `${DocumentTypes.LINK}${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}`
|
||||
exports.generateLinkID = (
|
||||
tableId1,
|
||||
tableId2,
|
||||
rowId1,
|
||||
rowId2,
|
||||
fieldName1,
|
||||
fieldName2
|
||||
) => {
|
||||
const tables = `${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}`
|
||||
const rows = `${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}`
|
||||
const fields = `${SEPARATOR}${fieldName1}${SEPARATOR}${fieldName2}`
|
||||
return `${DocumentTypes.LINK}${tables}${rows}${fields}`
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,13 +2,18 @@ const env = require("../environment")
|
|||
const { OBJ_STORE_DIRECTORY } = require("../constants")
|
||||
const linkRows = require("../db/linkedRows")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
const { FieldTypes } = require("../constants")
|
||||
const { FieldTypes, AutoFieldSubTypes } = require("../constants")
|
||||
const CouchDB = require("../db")
|
||||
const { ViewNames } = require("../db/utils")
|
||||
|
||||
const BASE_AUTO_ID = 1
|
||||
const USER_TABLE_ID = ViewNames.USERS
|
||||
|
||||
/**
|
||||
* A map of how we convert various properties in rows to each other based on the row type.
|
||||
*/
|
||||
const TYPE_TRANSFORM_MAP = {
|
||||
link: {
|
||||
[FieldTypes.LINK]: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
|
@ -19,44 +24,102 @@ const TYPE_TRANSFORM_MAP = {
|
|||
return link
|
||||
},
|
||||
},
|
||||
options: {
|
||||
[FieldTypes.OPTIONS]: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
string: {
|
||||
[FieldTypes.STRING]: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
longform: {
|
||||
[FieldTypes.LONGFORM]: {
|
||||
"": "",
|
||||
[null]: "",
|
||||
[undefined]: undefined,
|
||||
},
|
||||
number: {
|
||||
[FieldTypes.NUMBER]: {
|
||||
"": null,
|
||||
[null]: null,
|
||||
[undefined]: undefined,
|
||||
parse: n => parseFloat(n),
|
||||
},
|
||||
datetime: {
|
||||
[FieldTypes.DATETIME]: {
|
||||
"": null,
|
||||
[undefined]: undefined,
|
||||
[null]: null,
|
||||
},
|
||||
attachment: {
|
||||
[FieldTypes.ATTACHMENT]: {
|
||||
"": [],
|
||||
[null]: [],
|
||||
[undefined]: undefined,
|
||||
},
|
||||
boolean: {
|
||||
[FieldTypes.BOOLEAN]: {
|
||||
"": null,
|
||||
[null]: null,
|
||||
[undefined]: undefined,
|
||||
true: true,
|
||||
false: false,
|
||||
},
|
||||
[FieldTypes.AUTO]: {
|
||||
parse: () => undefined,
|
||||
},
|
||||
}
|
||||
|
||||
function getAutoRelationshipName(table, columnName) {
|
||||
return `${table.name}-${columnName}`
|
||||
}
|
||||
|
||||
/**
|
||||
* This will update any auto columns that are found on the row/table with the correct information based on
|
||||
* time now and the current logged in user making the request.
|
||||
* @param {Object} user The user to be used for an appId as well as the createdBy and createdAt fields.
|
||||
* @param {Object} table The table which is to be used for the schema, as well as handling auto IDs incrementing.
|
||||
* @param {Object} row The row which is to be updated with information for the auto columns.
|
||||
* @returns {Promise<{row: Object, table: Object}>} The updated row and table, the table may need to be updated
|
||||
* for automatic ID purposes.
|
||||
*/
|
||||
async function processAutoColumn(user, table, row) {
|
||||
let now = new Date().toISOString()
|
||||
// if a row doesn't have a revision then it doesn't exist yet
|
||||
const creating = !row._rev
|
||||
let tableUpdated = false
|
||||
for (let [key, schema] of Object.entries(table.schema)) {
|
||||
if (!schema.autocolumn) {
|
||||
continue
|
||||
}
|
||||
switch (schema.subtype) {
|
||||
case AutoFieldSubTypes.CREATED_BY:
|
||||
if (creating) {
|
||||
row[key] = [user.userId]
|
||||
}
|
||||
break
|
||||
case AutoFieldSubTypes.CREATED_AT:
|
||||
if (creating) {
|
||||
row[key] = now
|
||||
}
|
||||
break
|
||||
case AutoFieldSubTypes.UPDATED_BY:
|
||||
row[key] = [user.userId]
|
||||
break
|
||||
case AutoFieldSubTypes.UPDATED_AT:
|
||||
row[key] = now
|
||||
break
|
||||
case AutoFieldSubTypes.AUTO_ID:
|
||||
schema.lastID = !schema.lastID ? BASE_AUTO_ID : schema.lastID + 1
|
||||
row[key] = schema.lastID
|
||||
tableUpdated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (tableUpdated) {
|
||||
const db = new CouchDB(user.appId)
|
||||
const response = await db.put(table)
|
||||
// update the revision
|
||||
table._rev = response._rev
|
||||
}
|
||||
return { table, row }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,7 +128,7 @@ const TYPE_TRANSFORM_MAP = {
|
|||
* @param {object} type The type fo coerce to
|
||||
* @returns {object} The coerced value
|
||||
*/
|
||||
exports.coerceValue = (row, type) => {
|
||||
exports.coerce = (row, type) => {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
|
||||
return TYPE_TRANSFORM_MAP[type][row]
|
||||
|
@ -84,15 +147,17 @@ exports.coerceValue = (row, type) => {
|
|||
* @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)
|
||||
exports.inputProcessing = async (user, table, row) => {
|
||||
let clonedRow = cloneDeep(row)
|
||||
for (let [key, value] of Object.entries(clonedRow)) {
|
||||
const field = table.schema[key]
|
||||
if (!field) continue
|
||||
|
||||
if (!field) {
|
||||
continue
|
||||
}
|
||||
clonedRow[key] = exports.coerce(value, field.type)
|
||||
}
|
||||
return clonedRow
|
||||
// handle auto columns - this returns an object like {table, row}
|
||||
return processAutoColumn(user, table, clonedRow)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue