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 { 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue