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,
|
||||
}
|
||||
|
||||
function performExport(funcName: string) {
|
||||
// @ts-ignore
|
||||
return (...args: any) => GENERIC[funcName](...args)
|
||||
}
|
||||
|
||||
export const keys = performExport("keys")
|
||||
export const get = performExport("get")
|
||||
export const store = performExport("store")
|
||||
export const destroy = performExport("delete")
|
||||
export const withCache = performExport("withCache")
|
||||
export const bustCache = performExport("bustCache")
|
||||
export const keys = (...args: Parameters<typeof GENERIC.keys>) =>
|
||||
GENERIC.keys(...args)
|
||||
export const get = (...args: Parameters<typeof GENERIC.get>) =>
|
||||
GENERIC.get(...args)
|
||||
export const store = (...args: Parameters<typeof GENERIC.store>) =>
|
||||
GENERIC.store(...args)
|
||||
export const destroy = (...args: Parameters<typeof GENERIC.delete>) =>
|
||||
GENERIC.delete(...args)
|
||||
export const withCache = (...args: Parameters<typeof GENERIC.withCache>) =>
|
||||
GENERIC.withCache(...args)
|
||||
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 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) {
|
||||
const headers = {
|
||||
|
@ -37,20 +38,41 @@ exports.createApp = async apiKey => {
|
|||
return json.data
|
||||
}
|
||||
|
||||
exports.getTable = async (apiKey, appId) => {
|
||||
const res = await request(apiKey, URL_TABLE, "POST", {}, appId)
|
||||
exports.getApp = async (apiKey, appId) => {
|
||||
const res = await request(apiKey, `${URL_APP}/${appId}`, "GET")
|
||||
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) => {
|
||||
const body = {}
|
||||
exports.deleteApp = async (apiKey, appId) => {
|
||||
const res = await request(apiKey, `${URL_APP}/${appId}`, "DELETE")
|
||||
return res
|
||||
}
|
||||
|
||||
exports.getTable = async (apiKey, appId, tableName) => {
|
||||
const res = await request(apiKey, URL_SEARCH_TABLE, "POST", {}, appId)
|
||||
const json = await res.json()
|
||||
const table = json.data.find(t => t.name === tableName)
|
||||
if (!table) {
|
||||
throw `Table '${tableName} not found`
|
||||
}
|
||||
return table
|
||||
}
|
||||
|
||||
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
|
||||
fake = schema.constraints?.inclusion
|
||||
? schema.constraints.inclusion[0]
|
||||
: "a"
|
||||
break
|
||||
|
@ -60,7 +82,20 @@ exports.createRow = async (apiKey, appId, table) => {
|
|||
}
|
||||
body[key] = fake
|
||||
}
|
||||
}
|
||||
const url = `http://localhost:10000/api/public/v1/tables/${table._id}/rows`
|
||||
const res = await request(apiKey, url, "POST", body, appId)
|
||||
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,
|
||||
outputProcessing,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { cloneDeep, isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash"
|
||||
|
||||
export async function handleRequest<T extends Operation>(
|
||||
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> {
|
||||
const id = ctx.params.rowId
|
||||
const tableId = utils.getTableId(ctx)
|
||||
|
|
|
@ -30,7 +30,7 @@ import { Format } from "../view/exporters"
|
|||
|
||||
export * as views from "./views"
|
||||
|
||||
function pickApi(tableId: any) {
|
||||
function pickApi(tableId: string) {
|
||||
if (isExternalTableID(tableId)) {
|
||||
return external
|
||||
}
|
||||
|
@ -84,9 +84,12 @@ export const save = async (ctx: UserCtx<Row, Row>) => {
|
|||
return patch(ctx as UserCtx<PatchRowRequest, PatchRowResponse>)
|
||||
}
|
||||
const { row, table, squashed } = await quotas.addRow(() =>
|
||||
quotas.addQuery(() => pickApi(tableId).save(ctx), {
|
||||
quotas.addQuery(
|
||||
() => sdk.rows.save(tableId, ctx.request.body, ctx.user?._id),
|
||||
{
|
||||
datasourceId: tableId,
|
||||
})
|
||||
}
|
||||
)
|
||||
)
|
||||
ctx.status = 200
|
||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:save`, appId, row, table)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as linkRows from "../../../db/linkedRows"
|
||||
import { generateRowID, InternalTables } from "../../../db/utils"
|
||||
import { InternalTables } from "../../../db/utils"
|
||||
import * as userController from "../user"
|
||||
import {
|
||||
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> {
|
||||
const tableId = utils.getTableId(ctx),
|
||||
rowId = ctx.params.rowId
|
||||
|
|
|
@ -5,8 +5,8 @@ import {
|
|||
processFormulas,
|
||||
} from "../../../utilities/rowProcessor"
|
||||
import { FieldTypes, FormulaTypes } from "../../../constants"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import { Table, Row } from "@budibase/types"
|
||||
import { context, locks } from "@budibase/backend-core"
|
||||
import { Table, Row, LockType, LockName } from "@budibase/types"
|
||||
import * as linkRows from "../../../db/linkedRows"
|
||||
import sdk from "../../../sdk"
|
||||
import isEqual from "lodash/isEqual"
|
||||
|
@ -149,12 +149,22 @@ export async function finaliseRow(
|
|||
await db.put(table)
|
||||
} catch (err: any) {
|
||||
if (err.status === 409) {
|
||||
const updatedTable = await sdk.tables.getTable(table._id!)
|
||||
let response = processAutoColumn(null, updatedTable, row, {
|
||||
// Some conflicts with the autocolumns occurred, we need to refetch the table and recalculate
|
||||
await locks.doWithLock(
|
||||
{
|
||||
type: LockType.AUTO_EXTEND,
|
||||
name: LockName.PROCESS_AUTO_COLUMNS,
|
||||
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 {
|
||||
throw err
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ const publicRouter = new Router({
|
|||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
if (limiter) {
|
||||
if (limiter && !env.isDev()) {
|
||||
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 { 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(
|
||||
tableId: string,
|
||||
|
@ -15,3 +22,48 @@ export async function getRow(
|
|||
})
|
||||
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 { Database, Row } from "@budibase/types"
|
||||
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) {
|
||||
let db: Database
|
||||
|
@ -16,3 +19,18 @@ export async function getAllInternalRows(appId?: string) {
|
|||
)
|
||||
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",
|
||||
QUOTA_USAGE_EVENT = "quota_usage_event",
|
||||
APP_MIGRATION = "app_migrations",
|
||||
PROCESS_AUTO_COLUMNS = "process_auto_columns",
|
||||
}
|
||||
|
||||
export type LockOptions = {
|
||||
|
|
Loading…
Reference in New Issue