diff --git a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte index b9a2307433..5ae060254b 100644 --- a/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte +++ b/packages/builder/src/components/backend/TableNavigator/modals/CreateTableModal.svelte @@ -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, } diff --git a/packages/builder/src/constants/backend/index.js b/packages/builder/src/constants/backend/index.js index a3076b18d5..62fe0cb5ab 100644 --- a/packages/builder/src/constants/backend/index.js +++ b/packages/builder/src/constants/backend/index.js @@ -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 +} diff --git a/packages/server/src/api/controllers/row.js b/packages/server/src/api/controllers/row.js index 34f027002c..078e4b722f 100644 --- a/packages/server/src/api/controllers/row.js +++ b/packages/server/src/api/controllers/row.js @@ -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, diff --git a/packages/server/src/constants/index.js b/packages/server/src/constants/index.js index d8038e2a9e..314e220a97 100644 --- a/packages/server/src/constants/index.js +++ b/packages/server/src/constants/index.js @@ -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" diff --git a/packages/server/src/db/linkedRows/LinkController.js b/packages/server/src/db/linkedRows/LinkController.js index d69566d67c..54a0bfdd58 100644 --- a/packages/server/src/db/linkedRows/LinkController.js +++ b/packages/server/src/db/linkedRows/LinkController.js @@ -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 = { diff --git a/packages/server/src/db/linkedRows/index.js b/packages/server/src/db/linkedRows/index.js index 02af98eef9..fb536c5c44 100644 --- a/packages/server/src/db/linkedRows/index.js +++ b/packages/server/src/db/linkedRows/index.js @@ -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({ diff --git a/packages/server/src/db/utils.js b/packages/server/src/db/utils.js index 5e7e74d711..6ca55b6336 100644 --- a/packages/server/src/db/utils.js +++ b/packages/server/src/db/utils.js @@ -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}` } /** diff --git a/packages/server/src/utilities/linkedRows.js b/packages/server/src/utilities/linkedRows.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/utilities/rowProcessor.js b/packages/server/src/utilities/rowProcessor.js index fe18eefdc6..c2885d8440 100644 --- a/packages/server/src/utilities/rowProcessor.js +++ b/packages/server/src/utilities/rowProcessor.js @@ -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) } /**