Work in progress, getting the server backend mostly ready for this work.

This commit is contained in:
mike12345567 2021-02-15 17:47:14 +00:00
parent 6bc9123a86
commit 4b1855974c
9 changed files with 194 additions and 60 deletions

View File

@ -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,
}

View File

@ -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
}

View File

@ -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,

View File

@ -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"

View File

@ -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 = {

View File

@ -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({

View File

@ -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}`
}
/**

View File

@ -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)
}
/**