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 { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen" import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen" import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
import { FIELDS } from "constants/backend" import { AUTO_COLUMN_SUB_TYPES, buildAutoColumn } from "constants/backend"
import { cloneDeep } from "lodash/fp"
const defaultScreens = [ const defaultScreens = [
NEW_ROW_TEMPLATE, NEW_ROW_TEMPLATE,
@ -23,33 +22,34 @@
ROW_LIST_TEMPLATE, ROW_LIST_TEMPLATE,
] ]
$: tableNames = $backendUiStore.tables.map(table => table.name)
let modal let modal
let name let name
let dataImport let dataImport
let error = "" let error = ""
let createAutoscreens = true let createAutoscreens = true
let autoColumns = { let autoColumns = {
createdBy: true, [AUTO_COLUMN_SUB_TYPES.AUTO_ID]: {enabled: true, name: "Auto ID"},
createdAt: true, [AUTO_COLUMN_SUB_TYPES.CREATED_BY]: {enabled: true, name: "Created By"},
updatedBy: true, [AUTO_COLUMN_SUB_TYPES.CREATED_AT]: {enabled: true, name: "Created At"},
updatedAt: true, [AUTO_COLUMN_SUB_TYPES.UPDATED_BY]: {enabled: true, name: "Updated By"},
autoID: true, [AUTO_COLUMN_SUB_TYPES.UPDATED_AT]: {enabled: true, name: "Updated At"},
} }
function addAutoColumns(schema) { function addAutoColumns(tableName, schema) {
for (let [property, enabled] of Object.entries(autoColumns)) { for (let [subtype, col] of Object.entries(autoColumns)) {
if (!enabled) { if (!col.enabled) {
continue continue
} }
const autoColDef = cloneDeep(FIELDS.AUTO) schema[col.name] = buildAutoColumn(tableName, col.name, subtype)
autoColDef.subtype = property
schema[property] = autoColDef
} }
return schema
} }
function checkValid(evt) { function checkValid(evt) {
const tableName = evt.target.value 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.` error = `Table with name ${tableName} already exists. Please choose another name.`
return return
} }
@ -59,7 +59,7 @@
async function saveTable() { async function saveTable() {
let newTable = { let newTable = {
name, name,
schema: addAutoColumns(dataImport.schema || {}), schema: addAutoColumns(name, dataImport.schema || {}),
dataImport, dataImport,
} }

View File

@ -80,13 +80,14 @@ export const FIELDS = {
presence: false, 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 = { export const FILE_TYPES = {
@ -107,3 +108,43 @@ export const Roles = {
PUBLIC: "PUBLIC", PUBLIC: "PUBLIC",
BUILDER: "BUILDER", 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) { exports.patch = async function(ctx) {
const appId = ctx.user.appId const appId = ctx.user.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
let row = await db.get(ctx.params.rowId) let dbRow = await db.get(ctx.params.rowId)
const table = await db.get(row.tableId) let dbTable = await db.get(dbRow.tableId)
const patchfields = ctx.request.body const patchfields = ctx.request.body
// need to build up full patch fields before coerce // need to build up full patch fields before coerce
for (let key of Object.keys(patchfields)) { for (let key of Object.keys(patchfields)) {
if (!table.schema[key]) continue if (!dbTable.schema[key]) continue
row[key] = patchfields[key] 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({ const validateResult = await validate({
row, row,
table, table,
@ -114,32 +113,34 @@ exports.patch = async function(ctx) {
exports.save = async function(ctx) { exports.save = async function(ctx) {
const appId = ctx.user.appId const appId = ctx.user.appId
const db = new CouchDB(appId) const db = new CouchDB(appId)
let row = ctx.request.body let inputs = ctx.request.body
row.tableId = ctx.params.tableId inputs.tableId = ctx.params.tableId
// TODO: find usage of this and break out into own endpoint // TODO: find usage of this and break out into own endpoint
if (ctx.request.body.type === "delete") { if (inputs.type === "delete") {
await bulkDelete(ctx) await bulkDelete(ctx)
ctx.body = ctx.request.body.rows ctx.body = inputs.rows
return return
} }
// if the row obj had an _id then it will have been retrieved // if the row obj had an _id then it will have been retrieved
const existingRow = ctx.preExisting const existingRow = ctx.preExisting
if (existingRow) { if (existingRow) {
ctx.params.rowId = row._id ctx.params.rowId = inputs._id
await exports.patch(ctx) await exports.patch(ctx)
return return
} }
if (!row._rev && !row._id) { if (!inputs._rev && !inputs._id) {
row._id = generateRowID(row.tableId) inputs._id = generateRowID(inputs.tableId)
} }
const table = await db.get(row.tableId) // this returns the table and row incase they have been updated
let { table, row } = await inputProcessing(
row = inputProcessing(ctx.user, table, row) ctx.user,
await db.get(inputs.tableId),
inputs
)
const validateResult = await validate({ const validateResult = await validate({
row, row,
table, table,

View File

@ -51,6 +51,14 @@ exports.FieldTypes = {
AUTO: "auto", AUTO: "auto",
} }
exports.AutoFieldSubTypes = {
CREATED_BY: "createdBy",
CREATED_AT: "createdAt",
UPDATED_BY: "updatedBy",
UPDATED_AT: "updatedAt",
AUTO_ID: "autoID",
}
exports.AuthTypes = AuthTypes exports.AuthTypes = AuthTypes
exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA exports.USERS_TABLE_SCHEMA = USERS_TABLE_SCHEMA
exports.BUILDER_CONFIG_DB = "builder-config-db" exports.BUILDER_CONFIG_DB = "builder-config-db"

View File

@ -25,7 +25,14 @@ function LinkDocument(
rowId2 rowId2
) { ) {
// build the ID out of unique references to this link document // 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 // required for referencing in view
this.type = FieldTypes.LINK this.type = FieldTypes.LINK
this.doc1 = { this.doc1 = {

View File

@ -5,7 +5,7 @@ const {
createLinkView, createLinkView,
getUniqueByProp, getUniqueByProp,
} = require("./linkUtils") } = 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 * 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))] let tableIds = [...new Set(rows.map(el => el.tableId))]
// start by getting all the link values for performance reasons // start by getting all the link values for performance reasons
let responses = _.flatten( let responses = flatten(
await Promise.all( await Promise.all(
tableIds.map(tableId => tableIds.map(tableId =>
getLinkDocuments({ getLinkDocuments({

View File

@ -138,10 +138,22 @@ exports.generateAutomationID = () => {
* @param {string} tableId2 The ID of the linked table. * @param {string} tableId2 The ID of the linked table.
* @param {string} rowId1 The ID of the linker row. * @param {string} rowId1 The ID of the linker row.
* @param {string} rowId2 The ID of the linked 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. * @returns {string} The new link doc ID which the automation doc can be stored under.
*/ */
exports.generateLinkID = (tableId1, tableId2, rowId1, rowId2) => { exports.generateLinkID = (
return `${DocumentTypes.LINK}${SEPARATOR}${tableId1}${SEPARATOR}${tableId2}${SEPARATOR}${rowId1}${SEPARATOR}${rowId2}` 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 { OBJ_STORE_DIRECTORY } = require("../constants")
const linkRows = require("../db/linkedRows") const linkRows = require("../db/linkedRows")
const { cloneDeep } = require("lodash/fp") 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. * A map of how we convert various properties in rows to each other based on the row type.
*/ */
const TYPE_TRANSFORM_MAP = { const TYPE_TRANSFORM_MAP = {
link: { [FieldTypes.LINK]: {
"": [], "": [],
[null]: [], [null]: [],
[undefined]: undefined, [undefined]: undefined,
@ -19,44 +24,102 @@ const TYPE_TRANSFORM_MAP = {
return link return link
}, },
}, },
options: { [FieldTypes.OPTIONS]: {
"": "", "": "",
[null]: "", [null]: "",
[undefined]: undefined, [undefined]: undefined,
}, },
string: { [FieldTypes.STRING]: {
"": "", "": "",
[null]: "", [null]: "",
[undefined]: undefined, [undefined]: undefined,
}, },
longform: { [FieldTypes.LONGFORM]: {
"": "", "": "",
[null]: "", [null]: "",
[undefined]: undefined, [undefined]: undefined,
}, },
number: { [FieldTypes.NUMBER]: {
"": null, "": null,
[null]: null, [null]: null,
[undefined]: undefined, [undefined]: undefined,
parse: n => parseFloat(n), parse: n => parseFloat(n),
}, },
datetime: { [FieldTypes.DATETIME]: {
"": null, "": null,
[undefined]: undefined, [undefined]: undefined,
[null]: null, [null]: null,
}, },
attachment: { [FieldTypes.ATTACHMENT]: {
"": [], "": [],
[null]: [], [null]: [],
[undefined]: undefined, [undefined]: undefined,
}, },
boolean: { [FieldTypes.BOOLEAN]: {
"": null, "": null,
[null]: null, [null]: null,
[undefined]: undefined, [undefined]: undefined,
true: true, true: true,
false: false, 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 * @param {object} type The type fo coerce to
* @returns {object} The coerced value * @returns {object} The coerced value
*/ */
exports.coerceValue = (row, type) => { exports.coerce = (row, type) => {
// eslint-disable-next-line no-prototype-builtins // eslint-disable-next-line no-prototype-builtins
if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) { if (TYPE_TRANSFORM_MAP[type].hasOwnProperty(row)) {
return TYPE_TRANSFORM_MAP[type][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. * @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. * @returns {object} the row which has been prepared to be written to the DB.
*/ */
exports.inputProcessing = (user, table, row) => { exports.inputProcessing = async (user, table, row) => {
const clonedRow = cloneDeep(row) let clonedRow = cloneDeep(row)
for (let [key, value] of Object.entries(clonedRow)) { for (let [key, value] of Object.entries(clonedRow)) {
const field = table.schema[key] const field = table.schema[key]
if (!field) continue if (!field) {
continue
}
clonedRow[key] = exports.coerce(value, field.type) clonedRow[key] = exports.coerce(value, field.type)
} }
return clonedRow // handle auto columns - this returns an object like {table, row}
return processAutoColumn(user, table, clonedRow)
} }
/** /**