Merge pull request #12659 from Budibase/BUDI-7656/add-multiple-relationships-dev-script
Add multiple relationships dev script
This commit is contained in:
commit
4b38f18cc2
|
@ -1 +1 @@
|
||||||
Subproject commit d6a1f89aa543bdce7acde5fbe4ce650a1344e2fe
|
Subproject commit c1a53bb2f4cafcb4c55ad7181146617b449907f2
|
|
@ -18,14 +18,15 @@ export enum TTL {
|
||||||
ONE_DAY = 86400,
|
ONE_DAY = 86400,
|
||||||
}
|
}
|
||||||
|
|
||||||
function performExport(funcName: string) {
|
export const keys = (...args: Parameters<typeof GENERIC.keys>) =>
|
||||||
// @ts-ignore
|
GENERIC.keys(...args)
|
||||||
return (...args: any) => GENERIC[funcName](...args)
|
export const get = (...args: Parameters<typeof GENERIC.get>) =>
|
||||||
}
|
GENERIC.get(...args)
|
||||||
|
export const store = (...args: Parameters<typeof GENERIC.store>) =>
|
||||||
export const keys = performExport("keys")
|
GENERIC.store(...args)
|
||||||
export const get = performExport("get")
|
export const destroy = (...args: Parameters<typeof GENERIC.delete>) =>
|
||||||
export const store = performExport("store")
|
GENERIC.delete(...args)
|
||||||
export const destroy = performExport("delete")
|
export const withCache = (...args: Parameters<typeof GENERIC.withCache>) =>
|
||||||
export const withCache = performExport("withCache")
|
GENERIC.withCache(...args)
|
||||||
export const bustCache = performExport("bustCache")
|
export const bustCache = (...args: Parameters<typeof GENERIC.bustCache>) =>
|
||||||
|
GENERIC.bustCache(...args)
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
#!/bin/node
|
||||||
|
const {
|
||||||
|
createApp,
|
||||||
|
getTable,
|
||||||
|
createRow,
|
||||||
|
createTable,
|
||||||
|
getApp,
|
||||||
|
getRows,
|
||||||
|
} = require("./utils")
|
||||||
|
|
||||||
|
const Chance = require("chance")
|
||||||
|
|
||||||
|
const generator = new Chance()
|
||||||
|
|
||||||
|
const STUDENT_COUNT = 500
|
||||||
|
const SUBJECT_COUNT = 10
|
||||||
|
|
||||||
|
let { apiKey, appId } = require("yargs")
|
||||||
|
.demandOption(["apiKey"])
|
||||||
|
.option("appId").argv
|
||||||
|
|
||||||
|
const start = Date.now()
|
||||||
|
async function batchCreate(apiKey, appId, table, items, batchSize = 100) {
|
||||||
|
let i = 0
|
||||||
|
let errors = 0
|
||||||
|
|
||||||
|
async function createSingleRow(item) {
|
||||||
|
try {
|
||||||
|
const row = await createRow(apiKey, appId, table, item)
|
||||||
|
console.log(
|
||||||
|
`${table.name} - ${++i} of ${items.length} created (${
|
||||||
|
(Date.now() - start) / 1000
|
||||||
|
}s)`
|
||||||
|
)
|
||||||
|
return row
|
||||||
|
} catch {
|
||||||
|
errors++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = []
|
||||||
|
const maxConcurrency = Math.min(batchSize, items.length)
|
||||||
|
const inFlight = {}
|
||||||
|
|
||||||
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
|
const item = items[itemIndex]
|
||||||
|
const promise = createSingleRow(item)
|
||||||
|
.then(result => {
|
||||||
|
rows.push(result)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
delete inFlight[itemIndex]
|
||||||
|
})
|
||||||
|
|
||||||
|
inFlight[itemIndex] = promise
|
||||||
|
|
||||||
|
if (Object.keys(inFlight).length >= maxConcurrency) {
|
||||||
|
await Promise.race(Object.values(inFlight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(Object.values(inFlight))
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error(
|
||||||
|
`${table.name} - ${errors} creation errored (${
|
||||||
|
(Date.now() - start) / 1000
|
||||||
|
}s)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
const useExistingApp = !!appId
|
||||||
|
|
||||||
|
async function upsertTable(appId, tableName, tableData) {
|
||||||
|
if (useExistingApp) {
|
||||||
|
return await getTable(apiKey, appId, tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = await createTable(apiKey, appId, {
|
||||||
|
...tableData,
|
||||||
|
name: tableName,
|
||||||
|
})
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (!appId) {
|
||||||
|
const app = appId ? await getApp(apiKey, appId) : await createApp(apiKey)
|
||||||
|
appId = app._id
|
||||||
|
|
||||||
|
console.log(`App created. Url: http://localhost:10000/builder/app/${appId}`)
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`App retrieved. Url: http://localhost:10000/builder/app/${appId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const studentsTable = await getTable(apiKey, appId, "Students")
|
||||||
|
|
||||||
|
let studentNumber = studentsTable.schema["Auto ID"].lastID
|
||||||
|
const students = await batchCreate(
|
||||||
|
apiKey,
|
||||||
|
appId,
|
||||||
|
studentsTable,
|
||||||
|
Array.from({ length: STUDENT_COUNT }).map(() => ({
|
||||||
|
"Student Number": (++studentNumber).toString(),
|
||||||
|
"First Name": generator.first(),
|
||||||
|
"Last Name": generator.last(),
|
||||||
|
Gender: generator.pickone(["M", "F"]),
|
||||||
|
Grade: generator.pickone(["8", "9", "10", "11"]),
|
||||||
|
"Tardiness (Days)": generator.integer({ min: 1, max: 100 }),
|
||||||
|
"Home Number": generator.phone(),
|
||||||
|
"Attendance_(%)": generator.integer({ min: 0, max: 100 }),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const subjectTable = await upsertTable(appId, "Subjects", {
|
||||||
|
schema: {
|
||||||
|
Name: {
|
||||||
|
name: "Name",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primaryDisplay: "Name",
|
||||||
|
})
|
||||||
|
|
||||||
|
const subjects = useExistingApp
|
||||||
|
? await getRows(apiKey, appId, subjectTable._id)
|
||||||
|
: await batchCreate(
|
||||||
|
apiKey,
|
||||||
|
appId,
|
||||||
|
subjectTable,
|
||||||
|
Array.from({ length: SUBJECT_COUNT }).map(() => ({
|
||||||
|
Name: generator.profession(),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const gradesTable = await upsertTable(appId, "Grades", {
|
||||||
|
schema: {
|
||||||
|
Score: {
|
||||||
|
name: "Score",
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Student: {
|
||||||
|
name: "Student",
|
||||||
|
tableId: studentsTable._id,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
fieldName: "Grades",
|
||||||
|
relationshipType: "one-to-many",
|
||||||
|
type: "link",
|
||||||
|
},
|
||||||
|
Subject: {
|
||||||
|
name: "Subject",
|
||||||
|
tableId: subjectTable._id,
|
||||||
|
constraints: {
|
||||||
|
presence: true,
|
||||||
|
type: "array",
|
||||||
|
},
|
||||||
|
fieldName: "Grades",
|
||||||
|
relationshipType: "one-to-many",
|
||||||
|
type: "link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await batchCreate(
|
||||||
|
apiKey,
|
||||||
|
appId,
|
||||||
|
gradesTable,
|
||||||
|
students.flatMap(student =>
|
||||||
|
subjects.map(subject => ({
|
||||||
|
Score: generator.integer({ min: 0, max: 100 }),
|
||||||
|
Student: [student],
|
||||||
|
Subject: [subject],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Access the app here: http://localhost:10000/builder/app/${appId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => {
|
||||||
|
console.log(`Done in ${(Date.now() - start) / 1000} seconds`)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
|
@ -0,0 +1,29 @@
|
||||||
|
#!/bin/node
|
||||||
|
const { searchApps, deleteApp } = require("./utils")
|
||||||
|
|
||||||
|
if (!process.argv[2]) {
|
||||||
|
console.error("Please specify an API key as script argument.")
|
||||||
|
process.exit(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const apiKey = process.argv[2]
|
||||||
|
const apps = await searchApps(apiKey)
|
||||||
|
console.log(`Deleting ${apps.length} apps`)
|
||||||
|
|
||||||
|
let deletedApps = 0
|
||||||
|
await Promise.all(
|
||||||
|
apps.map(async app => {
|
||||||
|
await deleteApp(apiKey, app._id)
|
||||||
|
console.log(`App ${++deletedApps} of ${apps.length} deleted`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Done!")
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
})
|
|
@ -2,7 +2,8 @@ const fetch = require("node-fetch")
|
||||||
const uuid = require("uuid/v4")
|
const uuid = require("uuid/v4")
|
||||||
|
|
||||||
const URL_APP = "http://localhost:10000/api/public/v1/applications"
|
const URL_APP = "http://localhost:10000/api/public/v1/applications"
|
||||||
const URL_TABLE = "http://localhost:10000/api/public/v1/tables/search"
|
const URL_TABLE = "http://localhost:10000/api/public/v1/tables"
|
||||||
|
const URL_SEARCH_TABLE = "http://localhost:10000/api/public/v1/tables/search"
|
||||||
|
|
||||||
async function request(apiKey, url, method, body, appId = undefined) {
|
async function request(apiKey, url, method, body, appId = undefined) {
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -37,30 +38,64 @@ exports.createApp = async apiKey => {
|
||||||
return json.data
|
return json.data
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.getTable = async (apiKey, appId) => {
|
exports.getApp = async (apiKey, appId) => {
|
||||||
const res = await request(apiKey, URL_TABLE, "POST", {}, appId)
|
const res = await request(apiKey, `${URL_APP}/${appId}`, "GET")
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
return json.data[0]
|
return json.data
|
||||||
|
}
|
||||||
|
exports.searchApps = async apiKey => {
|
||||||
|
const res = await request(apiKey, `${URL_APP}/search`, "POST", {})
|
||||||
|
const json = await res.json()
|
||||||
|
return json.data
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.createRow = async (apiKey, appId, table) => {
|
exports.deleteApp = async (apiKey, appId) => {
|
||||||
const body = {}
|
const res = await request(apiKey, `${URL_APP}/${appId}`, "DELETE")
|
||||||
for (let [key, schema] of Object.entries(table.schema)) {
|
return res
|
||||||
let fake
|
}
|
||||||
switch (schema.type) {
|
|
||||||
default:
|
exports.getTable = async (apiKey, appId, tableName) => {
|
||||||
case "string":
|
const res = await request(apiKey, URL_SEARCH_TABLE, "POST", {}, appId)
|
||||||
fake = schema.constraints.inclusion
|
const json = await res.json()
|
||||||
? schema.constraints.inclusion[0]
|
const table = json.data.find(t => t.name === tableName)
|
||||||
: "a"
|
if (!table) {
|
||||||
break
|
throw `Table '${tableName} not found`
|
||||||
case "number":
|
}
|
||||||
fake = 1
|
return table
|
||||||
break
|
}
|
||||||
|
|
||||||
|
exports.createRow = async (apiKey, appId, table, body) => {
|
||||||
|
if (!body) {
|
||||||
|
body = {}
|
||||||
|
for (let [key, schema] of Object.entries(table.schema)) {
|
||||||
|
let fake
|
||||||
|
switch (schema.type) {
|
||||||
|
default:
|
||||||
|
case "string":
|
||||||
|
fake = schema.constraints?.inclusion
|
||||||
|
? schema.constraints.inclusion[0]
|
||||||
|
: "a"
|
||||||
|
break
|
||||||
|
case "number":
|
||||||
|
fake = 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
body[key] = fake
|
||||||
}
|
}
|
||||||
body[key] = fake
|
|
||||||
}
|
}
|
||||||
const url = `http://localhost:10000/api/public/v1/tables/${table._id}/rows`
|
const url = `http://localhost:10000/api/public/v1/tables/${table._id}/rows`
|
||||||
const res = await request(apiKey, url, "POST", body, appId)
|
const res = await request(apiKey, url, "POST", body, appId)
|
||||||
return (await res.json()).data
|
return (await res.json()).data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getRows = async (apiKey, appId, tableId) => {
|
||||||
|
const url = `${URL_TABLE}/${tableId}/rows/search`
|
||||||
|
const res = await request(apiKey, url, "POST", {}, appId)
|
||||||
|
return (await res.json()).data
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.createTable = async (apiKey, appId, config) => {
|
||||||
|
const res = await request(apiKey, URL_TABLE, "POST", config, appId)
|
||||||
|
const json = await res.json()
|
||||||
|
return json.data
|
||||||
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
inputProcessing,
|
inputProcessing,
|
||||||
outputProcessing,
|
outputProcessing,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { cloneDeep, isEqual } from "lodash"
|
import { cloneDeep } from "lodash"
|
||||||
|
|
||||||
export async function handleRequest<T extends Operation>(
|
export async function handleRequest<T extends Operation>(
|
||||||
operation: T,
|
operation: T,
|
||||||
|
@ -86,50 +86,6 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: UserCtx) {
|
|
||||||
const inputs = ctx.request.body
|
|
||||||
const tableId = utils.getTableId(ctx)
|
|
||||||
|
|
||||||
const table = await sdk.tables.getTable(tableId)
|
|
||||||
const { table: updatedTable, row } = await inputProcessing(
|
|
||||||
ctx.user?._id,
|
|
||||||
cloneDeep(table),
|
|
||||||
inputs
|
|
||||||
)
|
|
||||||
|
|
||||||
const validateResult = await sdk.rows.utils.validate({
|
|
||||||
row,
|
|
||||||
tableId,
|
|
||||||
})
|
|
||||||
if (!validateResult.valid) {
|
|
||||||
throw { validation: validateResult.errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await handleRequest(Operation.CREATE, tableId, {
|
|
||||||
row,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isEqual(table, updatedTable)) {
|
|
||||||
await sdk.tables.saveTable(updatedTable)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowId = response.row._id
|
|
||||||
if (rowId) {
|
|
||||||
const row = await sdk.rows.external.getRow(tableId, rowId, {
|
|
||||||
relationships: true,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
...response,
|
|
||||||
row: await outputProcessing(table, row, {
|
|
||||||
preserveLinks: true,
|
|
||||||
squash: true,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function find(ctx: UserCtx): Promise<Row> {
|
export async function find(ctx: UserCtx): Promise<Row> {
|
||||||
const id = ctx.params.rowId
|
const id = ctx.params.rowId
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { Format } from "../view/exporters"
|
||||||
|
|
||||||
export * as views from "./views"
|
export * as views from "./views"
|
||||||
|
|
||||||
function pickApi(tableId: any) {
|
function pickApi(tableId: string) {
|
||||||
if (isExternalTableID(tableId)) {
|
if (isExternalTableID(tableId)) {
|
||||||
return external
|
return external
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,12 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
|
||||||
return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>)
|
return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>)
|
||||||
}
|
}
|
||||||
const { row, table, squashed } = await quotas.addRow(() =>
|
const { row, table, squashed } = await quotas.addRow(() =>
|
||||||
quotas.addQuery(() => pickApi(tableId).save(ctx), {
|
quotas.addQuery(
|
||||||
datasourceId: tableId,
|
() => sdk.rows.save(tableId, ctx.request.body, ctx.user?._id),
|
||||||
})
|
{
|
||||||
|
datasourceId: tableId,
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as linkRows from "../../../db/linkedRows"
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
import { generateRowID, InternalTables } from "../../../db/utils"
|
import { InternalTables } from "../../../db/utils"
|
||||||
import * as userController from "../user"
|
import * as userController from "../user"
|
||||||
import {
|
import {
|
||||||
AttachmentCleanup,
|
AttachmentCleanup,
|
||||||
|
@ -94,45 +94,6 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: UserCtx) {
|
|
||||||
let inputs = ctx.request.body
|
|
||||||
inputs.tableId = utils.getTableId(ctx)
|
|
||||||
|
|
||||||
if (!inputs._rev && !inputs._id) {
|
|
||||||
inputs._id = generateRowID(inputs.tableId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this returns the table and row incase they have been updated
|
|
||||||
const dbTable = await sdk.tables.getTable(inputs.tableId)
|
|
||||||
|
|
||||||
// need to copy the table so it can be differenced on way out
|
|
||||||
const tableClone = cloneDeep(dbTable)
|
|
||||||
|
|
||||||
let { table, row } = await inputProcessing(ctx.user?._id, tableClone, inputs)
|
|
||||||
|
|
||||||
const validateResult = await sdk.rows.utils.validate({
|
|
||||||
row,
|
|
||||||
table,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!validateResult.valid) {
|
|
||||||
throw { validation: validateResult.errors }
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure link rows are up-to-date
|
|
||||||
row = (await linkRows.updateLinks({
|
|
||||||
eventType: linkRows.EventType.ROW_SAVE,
|
|
||||||
row,
|
|
||||||
tableId: row.tableId,
|
|
||||||
table,
|
|
||||||
})) as Row
|
|
||||||
|
|
||||||
return finaliseRow(table, row, {
|
|
||||||
oldTable: dbTable,
|
|
||||||
updateFormula: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function find(ctx: UserCtx): Promise<Row> {
|
export async function find(ctx: UserCtx): Promise<Row> {
|
||||||
const tableId = utils.getTableId(ctx),
|
const tableId = utils.getTableId(ctx),
|
||||||
rowId = ctx.params.rowId
|
rowId = ctx.params.rowId
|
||||||
|
|
|
@ -5,8 +5,8 @@ import {
|
||||||
processFormulas,
|
processFormulas,
|
||||||
} from "../../../utilities/rowProcessor"
|
} from "../../../utilities/rowProcessor"
|
||||||
import { FieldTypes, FormulaTypes } from "../../../constants"
|
import { FieldTypes, FormulaTypes } from "../../../constants"
|
||||||
import { context } from "@budibase/backend-core"
|
import { context, locks } from "@budibase/backend-core"
|
||||||
import { Table, Row } from "@budibase/types"
|
import { Table, Row, LockType, LockName } from "@budibase/types"
|
||||||
import * as linkRows from "../../../db/linkedRows"
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import isEqual from "lodash/isEqual"
|
import isEqual from "lodash/isEqual"
|
||||||
|
@ -149,12 +149,22 @@ export async function finaliseRow(
|
||||||
await db.put(table)
|
await db.put(table)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.status === 409) {
|
if (err.status === 409) {
|
||||||
const updatedTable = await sdk.tables.getTable(table._id!)
|
// Some conflicts with the autocolumns occurred, we need to refetch the table and recalculate
|
||||||
let response = processAutoColumn(null, updatedTable, row, {
|
await locks.doWithLock(
|
||||||
reprocessing: true,
|
{
|
||||||
})
|
type: LockType.AUTO_EXTEND,
|
||||||
await db.put(response.table)
|
name: LockName.PROCESS_AUTO_COLUMNS,
|
||||||
row = response.row
|
resource: table._id,
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const latestTable = await sdk.tables.getTable(table._id!)
|
||||||
|
let response = processAutoColumn(null, latestTable, row, {
|
||||||
|
reprocessing: true,
|
||||||
|
})
|
||||||
|
await db.put(response.table)
|
||||||
|
row = response.row
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ const publicRouter = new Router({
|
||||||
prefix: PREFIX,
|
prefix: PREFIX,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (limiter) {
|
if (limiter && !env.isDev()) {
|
||||||
publicRouter.use(limiter)
|
publicRouter.use(limiter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import { IncludeRelationship, Operation } from "@budibase/types"
|
import { IncludeRelationship, Operation, Row } from "@budibase/types"
|
||||||
import { handleRequest } from "../../../api/controllers/row/external"
|
import { handleRequest } from "../../../api/controllers/row/external"
|
||||||
import { breakRowIdField } from "../../../integrations/utils"
|
import { breakRowIdField } from "../../../integrations/utils"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
import {
|
||||||
|
inputProcessing,
|
||||||
|
outputProcessing,
|
||||||
|
} from "../../../utilities/rowProcessor"
|
||||||
|
import cloneDeep from "lodash/fp/cloneDeep"
|
||||||
|
import isEqual from "lodash/fp/isEqual"
|
||||||
|
|
||||||
export async function getRow(
|
export async function getRow(
|
||||||
tableId: string,
|
tableId: string,
|
||||||
|
@ -15,3 +22,48 @@ export async function getRow(
|
||||||
})
|
})
|
||||||
return response ? response[0] : response
|
return response ? response[0] : response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function save(
|
||||||
|
tableId: string,
|
||||||
|
inputs: Row,
|
||||||
|
userId: string | undefined
|
||||||
|
) {
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
const { table: updatedTable, row } = await inputProcessing(
|
||||||
|
userId,
|
||||||
|
cloneDeep(table),
|
||||||
|
inputs
|
||||||
|
)
|
||||||
|
|
||||||
|
const validateResult = await sdk.rows.utils.validate({
|
||||||
|
row,
|
||||||
|
tableId,
|
||||||
|
})
|
||||||
|
if (!validateResult.valid) {
|
||||||
|
throw { validation: validateResult.errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await handleRequest(Operation.CREATE, tableId, {
|
||||||
|
row,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!isEqual(table, updatedTable)) {
|
||||||
|
await sdk.tables.saveTable(updatedTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowId = response.row._id
|
||||||
|
if (rowId) {
|
||||||
|
const row = await sdk.rows.external.getRow(tableId, rowId, {
|
||||||
|
relationships: true,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
row: await outputProcessing(table, row, {
|
||||||
|
preserveLinks: true,
|
||||||
|
squash: true,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { db } from "@budibase/backend-core"
|
||||||
|
import { Row } from "@budibase/types"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
import cloneDeep from "lodash/fp/cloneDeep"
|
||||||
|
import { finaliseRow } from "../../../api/controllers/row/staticFormula"
|
||||||
|
import { inputProcessing } from "../../../utilities/rowProcessor"
|
||||||
|
import * as linkRows from "../../../db/linkedRows"
|
||||||
|
|
||||||
|
export async function save(
|
||||||
|
tableId: string,
|
||||||
|
inputs: Row,
|
||||||
|
userId: string | undefined
|
||||||
|
) {
|
||||||
|
inputs.tableId = tableId
|
||||||
|
|
||||||
|
if (!inputs._rev && !inputs._id) {
|
||||||
|
inputs._id = db.generateRowID(inputs.tableId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this returns the table and row incase they have been updated
|
||||||
|
const dbTable = await sdk.tables.getTable(inputs.tableId)
|
||||||
|
|
||||||
|
// need to copy the table so it can be differenced on way out
|
||||||
|
const tableClone = cloneDeep(dbTable)
|
||||||
|
|
||||||
|
let { table, row } = await inputProcessing(userId, tableClone, inputs)
|
||||||
|
|
||||||
|
const validateResult = await sdk.rows.utils.validate({
|
||||||
|
row,
|
||||||
|
table,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!validateResult.valid) {
|
||||||
|
throw { validation: validateResult.errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure link rows are up-to-date
|
||||||
|
row = (await linkRows.updateLinks({
|
||||||
|
eventType: linkRows.EventType.ROW_SAVE,
|
||||||
|
row,
|
||||||
|
tableId: row.tableId,
|
||||||
|
table,
|
||||||
|
})) as Row
|
||||||
|
|
||||||
|
return finaliseRow(table, row, {
|
||||||
|
oldTable: dbTable,
|
||||||
|
updateFormula: true,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
import { db as dbCore, context } from "@budibase/backend-core"
|
import { db as dbCore, context } from "@budibase/backend-core"
|
||||||
import { Database, Row } from "@budibase/types"
|
import { Database, Row } from "@budibase/types"
|
||||||
import { getRowParams } from "../../../db/utils"
|
import { getRowParams } from "../../../db/utils"
|
||||||
|
import { isExternalTableID } from "../../../integrations/utils"
|
||||||
|
import * as internal from "./internal"
|
||||||
|
import * as external from "./external"
|
||||||
|
|
||||||
export async function getAllInternalRows(appId?: string) {
|
export async function getAllInternalRows(appId?: string) {
|
||||||
let db: Database
|
let db: Database
|
||||||
|
@ -16,3 +19,18 @@ export async function getAllInternalRows(appId?: string) {
|
||||||
)
|
)
|
||||||
return response.rows.map(row => row.doc) as Row[]
|
return response.rows.map(row => row.doc) as Row[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pickApi(tableId: any) {
|
||||||
|
if (isExternalTableID(tableId)) {
|
||||||
|
return external
|
||||||
|
}
|
||||||
|
return internal
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function save(
|
||||||
|
tableId: string,
|
||||||
|
row: Row,
|
||||||
|
userId: string | undefined
|
||||||
|
) {
|
||||||
|
return pickApi(tableId).save(tableId, row, userId)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
import tk from "timekeeper"
|
||||||
|
import * as internalSdk from "../internal"
|
||||||
|
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import {
|
||||||
|
INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
TableSourceType,
|
||||||
|
FieldType,
|
||||||
|
Table,
|
||||||
|
AutoFieldSubTypes,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||||
|
import { cache } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
tk.freeze(Date.now())
|
||||||
|
|
||||||
|
describe("sdk >> rows >> internal", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
function makeRow() {
|
||||||
|
return {
|
||||||
|
name: generator.first(),
|
||||||
|
surname: generator.last(),
|
||||||
|
age: generator.age(),
|
||||||
|
address: generator.address(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("save", () => {
|
||||||
|
const tableData: Table = {
|
||||||
|
name: generator.word(),
|
||||||
|
type: "table",
|
||||||
|
sourceId: INTERNAL_TABLE_SOURCE_ID,
|
||||||
|
sourceType: TableSourceType.INTERNAL,
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
surname: {
|
||||||
|
name: "surname",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
name: "age",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
name: "address",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("save will persist the row properly", async () => {
|
||||||
|
const table = await config.createTable(tableData)
|
||||||
|
const row = makeRow()
|
||||||
|
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
const response = await internalSdk.save(
|
||||||
|
table._id!,
|
||||||
|
row,
|
||||||
|
config.user._id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
table,
|
||||||
|
row: {
|
||||||
|
...row,
|
||||||
|
type: "row",
|
||||||
|
_rev: expect.stringMatching("1-.*"),
|
||||||
|
},
|
||||||
|
squashed: {
|
||||||
|
...row,
|
||||||
|
type: "row",
|
||||||
|
_rev: expect.stringMatching("1-.*"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const persistedRow = await config.getRow(table._id!, response.row._id!)
|
||||||
|
expect(persistedRow).toEqual({
|
||||||
|
...row,
|
||||||
|
type: "row",
|
||||||
|
_rev: expect.stringMatching("1-.*"),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("auto ids will update when creating new rows", async () => {
|
||||||
|
const table = await config.createTable({
|
||||||
|
...tableData,
|
||||||
|
schema: {
|
||||||
|
...tableData.schema,
|
||||||
|
id: {
|
||||||
|
name: "id",
|
||||||
|
type: FieldType.AUTO,
|
||||||
|
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||||
|
autocolumn: true,
|
||||||
|
lastID: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const row = makeRow()
|
||||||
|
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
const response = await internalSdk.save(
|
||||||
|
table._id!,
|
||||||
|
row,
|
||||||
|
config.user._id
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
table: {
|
||||||
|
...table,
|
||||||
|
schema: {
|
||||||
|
...table.schema,
|
||||||
|
id: {
|
||||||
|
...table.schema.id,
|
||||||
|
lastID: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
...row,
|
||||||
|
id: 1,
|
||||||
|
type: "row",
|
||||||
|
_rev: expect.stringMatching("1-.*"),
|
||||||
|
},
|
||||||
|
squashed: {
|
||||||
|
...row,
|
||||||
|
id: 1,
|
||||||
|
type: "row",
|
||||||
|
_rev: expect.stringMatching("1-.*"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const persistedRow = await config.getRow(table._id!, response.row._id!)
|
||||||
|
expect(persistedRow).toEqual({
|
||||||
|
...row,
|
||||||
|
type: "row",
|
||||||
|
id: 1,
|
||||||
|
_rev: expect.stringMatching("1-.*"),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("auto ids will update when creating new rows in parallel", async () => {
|
||||||
|
function makeRows(count: number) {
|
||||||
|
return Array.from({ length: count }, () => makeRow())
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = await config.createTable({
|
||||||
|
...tableData,
|
||||||
|
schema: {
|
||||||
|
...tableData.schema,
|
||||||
|
id: {
|
||||||
|
name: "id",
|
||||||
|
type: FieldType.AUTO,
|
||||||
|
subtype: AutoFieldSubTypes.AUTO_ID,
|
||||||
|
autocolumn: true,
|
||||||
|
lastID: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await config.doInContext(config.appId, async () => {
|
||||||
|
for (const row of makeRows(5)) {
|
||||||
|
await internalSdk.save(table._id!, row, config.user._id)
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
makeRows(10).map(row =>
|
||||||
|
internalSdk.save(table._id!, row, config.user._id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for (const row of makeRows(5)) {
|
||||||
|
await internalSdk.save(table._id!, row, config.user._id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const persistedRows = await config.getRows(table._id!)
|
||||||
|
expect(persistedRows).toHaveLength(20)
|
||||||
|
expect(persistedRows).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
Array.from({ length: 20 }).map((_, i) =>
|
||||||
|
expect.objectContaining({ id: i + 1 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const persistedTable = await config.getTable(table._id)
|
||||||
|
expect((table as any).schema.id.lastID).toBe(0)
|
||||||
|
expect(persistedTable.schema.id.lastID).toBe(20)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -21,6 +21,7 @@ export enum LockName {
|
||||||
PERSIST_WRITETHROUGH = "persist_writethrough",
|
PERSIST_WRITETHROUGH = "persist_writethrough",
|
||||||
QUOTA_USAGE_EVENT = "quota_usage_event",
|
QUOTA_USAGE_EVENT = "quota_usage_event",
|
||||||
APP_MIGRATION = "app_migrations",
|
APP_MIGRATION = "app_migrations",
|
||||||
|
PROCESS_AUTO_COLUMNS = "process_auto_columns",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LockOptions = {
|
export type LockOptions = {
|
||||||
|
|
Loading…
Reference in New Issue