Merge branch 'develop' into account-portal-auth-api-testing-2
This commit is contained in:
commit
0380068473
|
@ -201,25 +201,24 @@ spec:
|
||||||
|
|
||||||
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/apps:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
|
{{- if .Values.services.apps.startupProbe }}
|
||||||
|
{{- with .Values.services.apps.startupProbe }}
|
||||||
|
startupProbe:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.services.apps.livenessProbe }}
|
||||||
|
{{- with .Values.services.apps.livenessProbe }}
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
{{- toYaml . | nindent 10 }}
|
||||||
path: /health
|
{{- end }}
|
||||||
port: {{ .Values.services.apps.port }}
|
{{- end }}
|
||||||
initialDelaySeconds: 10
|
{{- if .Values.services.apps.readinessProbe }}
|
||||||
periodSeconds: 5
|
{{- with .Values.services.apps.readinessProbe }}
|
||||||
successThreshold: 1
|
|
||||||
failureThreshold: 3
|
|
||||||
timeoutSeconds: 3
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
{{- toYaml . | nindent 10 }}
|
||||||
path: /health
|
{{- end }}
|
||||||
port: {{ .Values.services.apps.port }}
|
{{- end }}
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
successThreshold: 1
|
|
||||||
failureThreshold: 3
|
|
||||||
timeoutSeconds: 3
|
|
||||||
|
|
||||||
name: bbapps
|
name: bbapps
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.apps.port }}
|
- containerPort: {{ .Values.services.apps.port }}
|
||||||
|
|
|
@ -40,24 +40,24 @@ spec:
|
||||||
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
- image: budibase/proxy:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: proxy-service
|
name: proxy-service
|
||||||
|
{{- if .Values.services.proxy.startupProbe }}
|
||||||
|
{{- with .Values.services.proxy.startupProbe }}
|
||||||
|
startupProbe:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.services.proxy.livenessProbe }}
|
||||||
|
{{- with .Values.services.proxy.livenessProbe }}
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
{{- toYaml . | nindent 10 }}
|
||||||
path: /health
|
{{- end }}
|
||||||
port: {{ .Values.services.proxy.port }}
|
{{- end }}
|
||||||
initialDelaySeconds: 0
|
{{- if .Values.services.proxy.readinessProbe }}
|
||||||
periodSeconds: 5
|
{{- with .Values.services.proxy.readinessProbe }}
|
||||||
successThreshold: 1
|
|
||||||
failureThreshold: 2
|
|
||||||
timeoutSeconds: 3
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
{{- toYaml . | nindent 10 }}
|
||||||
path: /health
|
{{- end }}
|
||||||
port: {{ .Values.services.proxy.port }}
|
{{- end }}
|
||||||
initialDelaySeconds: 0
|
|
||||||
periodSeconds: 5
|
|
||||||
successThreshold: 1
|
|
||||||
failureThreshold: 2
|
|
||||||
timeoutSeconds: 3
|
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.proxy.port }}
|
- containerPort: {{ .Values.services.proxy.port }}
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -190,24 +190,24 @@ spec:
|
||||||
{{ end }}
|
{{ end }}
|
||||||
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
image: budibase/worker:{{ .Values.globals.appVersion | default .Chart.AppVersion }}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
|
{{- if .Values.services.worker.startupProbe }}
|
||||||
|
{{- with .Values.services.worker.startupProbe }}
|
||||||
|
startupProbe:
|
||||||
|
{{- toYaml . | nindent 10 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.services.worker.livenessProbe }}
|
||||||
|
{{- with .Values.services.worker.livenessProbe }}
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
{{- toYaml . | nindent 10 }}
|
||||||
path: /health
|
{{- end }}
|
||||||
port: {{ .Values.services.worker.port }}
|
{{- end }}
|
||||||
initialDelaySeconds: 10
|
{{- if .Values.services.worker.readinessProbe }}
|
||||||
periodSeconds: 5
|
{{- with .Values.services.worker.readinessProbe }}
|
||||||
successThreshold: 1
|
|
||||||
failureThreshold: 3
|
|
||||||
timeoutSeconds: 3
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
{{- toYaml . | nindent 10 }}
|
||||||
path: /health
|
{{- end }}
|
||||||
port: {{ .Values.services.worker.port }}
|
{{- end }}
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 5
|
|
||||||
successThreshold: 1
|
|
||||||
failureThreshold: 3
|
|
||||||
timeoutSeconds: 3
|
|
||||||
name: bbworker
|
name: bbworker
|
||||||
ports:
|
ports:
|
||||||
- containerPort: {{ .Values.services.worker.port }}
|
- containerPort: {{ .Values.services.worker.port }}
|
||||||
|
|
|
@ -119,11 +119,33 @@ services:
|
||||||
port: 10000
|
port: 10000
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
upstreams:
|
upstreams:
|
||||||
apps: 'http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}'
|
apps: "http://app-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.apps.port }}"
|
||||||
worker: 'http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}'
|
worker: "http://worker-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.worker.port }}"
|
||||||
minio: 'http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}'
|
minio: "http://minio-service.{{ .Release.Namespace }}.svc.{{ .Values.services.dns }}:{{ .Values.services.objectStore.port }}"
|
||||||
couchdb: 'http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}'
|
couchdb: "http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }}"
|
||||||
resources: {}
|
resources: {}
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 10000
|
||||||
|
scheme: HTTP
|
||||||
|
failureThreshold: 30
|
||||||
|
periodSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 10000
|
||||||
|
scheme: HTTP
|
||||||
|
enabled: true
|
||||||
|
periodSeconds: 3
|
||||||
|
failureThreshold: 1
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 10000
|
||||||
|
scheme: HTTP
|
||||||
|
failureThreshold: 3
|
||||||
|
periodSeconds: 5
|
||||||
# annotations:
|
# annotations:
|
||||||
# co.elastic.logs/module: nginx
|
# co.elastic.logs/module: nginx
|
||||||
# co.elastic.logs/fileset.stdout: access
|
# co.elastic.logs/fileset.stdout: access
|
||||||
|
@ -135,6 +157,28 @@ services:
|
||||||
logLevel: info
|
logLevel: info
|
||||||
httpLogging: 1
|
httpLogging: 1
|
||||||
resources: {}
|
resources: {}
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4002
|
||||||
|
scheme: HTTP
|
||||||
|
failureThreshold: 30
|
||||||
|
periodSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4002
|
||||||
|
scheme: HTTP
|
||||||
|
enabled: true
|
||||||
|
periodSeconds: 3
|
||||||
|
failureThreshold: 1
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4002
|
||||||
|
scheme: HTTP
|
||||||
|
failureThreshold: 3
|
||||||
|
periodSeconds: 5
|
||||||
# nodeDebug: "" # set the value of NODE_DEBUG
|
# nodeDebug: "" # set the value of NODE_DEBUG
|
||||||
# annotations:
|
# annotations:
|
||||||
# co.elastic.logs/multiline.type: pattern
|
# co.elastic.logs/multiline.type: pattern
|
||||||
|
@ -147,6 +191,28 @@ services:
|
||||||
logLevel: info
|
logLevel: info
|
||||||
httpLogging: 1
|
httpLogging: 1
|
||||||
resources: {}
|
resources: {}
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4003
|
||||||
|
scheme: HTTP
|
||||||
|
failureThreshold: 30
|
||||||
|
periodSeconds: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4003
|
||||||
|
scheme: HTTP
|
||||||
|
enabled: true
|
||||||
|
periodSeconds: 3
|
||||||
|
failureThreshold: 1
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 4003
|
||||||
|
scheme: HTTP
|
||||||
|
failureThreshold: 3
|
||||||
|
periodSeconds: 5
|
||||||
# annotations:
|
# annotations:
|
||||||
# co.elastic.logs/multiline.type: pattern
|
# co.elastic.logs/multiline.type: pattern
|
||||||
# co.elastic.logs/multiline.pattern: '^[[:space:]]'
|
# co.elastic.logs/multiline.pattern: '^[[:space:]]'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.8.29-alpha.6",
|
"version": "2.8.29-alpha.9",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -11,10 +11,6 @@ import {
|
||||||
Row,
|
Row,
|
||||||
PatchRowRequest,
|
PatchRowRequest,
|
||||||
PatchRowResponse,
|
PatchRowResponse,
|
||||||
SearchResponse,
|
|
||||||
SortOrder,
|
|
||||||
SortType,
|
|
||||||
ViewV2,
|
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { gridSocket } from "../../../websockets"
|
import { gridSocket } from "../../../websockets"
|
||||||
|
@ -23,6 +19,7 @@ import { fixRow } from "../public/rows"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as exporters from "../view/exporters"
|
import * as exporters from "../view/exporters"
|
||||||
import { apiFileReturn } from "../../../utilities/fileSystem"
|
import { apiFileReturn } from "../../../utilities/fileSystem"
|
||||||
|
export * as views from "./views"
|
||||||
|
|
||||||
function pickApi(tableId: any) {
|
function pickApi(tableId: any) {
|
||||||
if (isExternalTable(tableId)) {
|
if (isExternalTable(tableId)) {
|
||||||
|
@ -37,6 +34,7 @@ export async function patch(
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
|
|
||||||
// if it doesn't have an _id then its save
|
// if it doesn't have an _id then its save
|
||||||
if (body && !body._id) {
|
if (body && !body._id) {
|
||||||
return save(ctx)
|
return save(ctx)
|
||||||
|
@ -62,13 +60,14 @@ export async function patch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const save = async (ctx: any) => {
|
export const save = async (ctx: UserCtx<Row, Row>) => {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
|
|
||||||
// if it has an ID already then its a patch
|
// if it has an ID already then its a patch
|
||||||
if (body && body._id) {
|
if (body && body._id) {
|
||||||
return patch(ctx)
|
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(() => pickApi(tableId).save(ctx), {
|
||||||
|
@ -147,7 +146,7 @@ async function deleteRows(ctx: UserCtx<DeleteRowRequest>) {
|
||||||
const rowDeletes: Row[] = await processDeleteRowsRequest(ctx)
|
const rowDeletes: Row[] = await processDeleteRowsRequest(ctx)
|
||||||
deleteRequest.rows = rowDeletes
|
deleteRequest.rows = rowDeletes
|
||||||
|
|
||||||
let { rows } = await quotas.addQuery<any>(
|
const { rows } = await quotas.addQuery(
|
||||||
() => pickApi(tableId).bulkDestroy(ctx),
|
() => pickApi(tableId).bulkDestroy(ctx),
|
||||||
{
|
{
|
||||||
datasourceId: tableId,
|
datasourceId: tableId,
|
||||||
|
@ -167,13 +166,13 @@ async function deleteRow(ctx: UserCtx<DeleteRowRequest>) {
|
||||||
const appId = ctx.appId
|
const appId = ctx.appId
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
|
|
||||||
let resp = await quotas.addQuery<any>(() => pickApi(tableId).destroy(ctx), {
|
const resp = await quotas.addQuery(() => pickApi(tableId).destroy(ctx), {
|
||||||
datasourceId: tableId,
|
datasourceId: tableId,
|
||||||
})
|
})
|
||||||
await quotas.removeRow()
|
await quotas.removeRow()
|
||||||
|
|
||||||
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row)
|
ctx.eventEmitter && ctx.eventEmitter.emitRow(`row:delete`, appId, resp.row)
|
||||||
gridSocket?.emitRowDeletion(ctx, resp.row._id)
|
gridSocket?.emitRowDeletion(ctx, resp.row._id!)
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
@ -212,83 +211,6 @@ export async function search(ctx: any) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortOptions(
|
|
||||||
ctx: Ctx,
|
|
||||||
view: ViewV2
|
|
||||||
):
|
|
||||||
| {
|
|
||||||
sort: string
|
|
||||||
sortOrder?: SortOrder
|
|
||||||
sortType?: SortType
|
|
||||||
}
|
|
||||||
| undefined {
|
|
||||||
const { sort_column, sort_order, sort_type } = ctx.query
|
|
||||||
if (Array.isArray(sort_column)) {
|
|
||||||
ctx.throw(400, "sort_column cannot be an array")
|
|
||||||
}
|
|
||||||
if (Array.isArray(sort_order)) {
|
|
||||||
ctx.throw(400, "sort_order cannot be an array")
|
|
||||||
}
|
|
||||||
if (Array.isArray(sort_type)) {
|
|
||||||
ctx.throw(400, "sort_type cannot be an array")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort_column) {
|
|
||||||
return {
|
|
||||||
sort: sort_column,
|
|
||||||
sortOrder: sort_order as SortOrder,
|
|
||||||
sortType: sort_type as SortType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (view.sort) {
|
|
||||||
return {
|
|
||||||
sort: view.sort.field,
|
|
||||||
sortOrder: view.sort.order,
|
|
||||||
sortType: view.sort.type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function searchView(ctx: Ctx<void, SearchResponse>) {
|
|
||||||
const { viewId } = ctx.params
|
|
||||||
|
|
||||||
const view = await sdk.views.get(viewId)
|
|
||||||
if (!view) {
|
|
||||||
ctx.throw(404, `View ${viewId} not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (view.version !== 2) {
|
|
||||||
ctx.throw(400, `This method only supports viewsV2`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const table = await sdk.tables.getTable(view?.tableId)
|
|
||||||
|
|
||||||
const viewFields =
|
|
||||||
(view.columns &&
|
|
||||||
Object.entries(view.columns).length &&
|
|
||||||
Object.keys(sdk.views.enrichSchema(view, table.schema).schema)) ||
|
|
||||||
undefined
|
|
||||||
|
|
||||||
ctx.status = 200
|
|
||||||
const result = await quotas.addQuery(
|
|
||||||
() =>
|
|
||||||
sdk.rows.search({
|
|
||||||
tableId: view.tableId,
|
|
||||||
query: view.query || {},
|
|
||||||
fields: viewFields,
|
|
||||||
...getSortOptions(ctx, view),
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
datasourceId: view.tableId,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
result.rows.forEach(r => (r._viewId = view.id))
|
|
||||||
ctx.body = result
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validate(ctx: Ctx) {
|
export async function validate(ctx: Ctx) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
// external tables are hard to validate currently
|
// external tables are hard to validate currently
|
||||||
|
|
|
@ -93,7 +93,6 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function save(ctx: UserCtx) {
|
export async function save(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
|
||||||
let inputs = ctx.request.body
|
let inputs = ctx.request.body
|
||||||
inputs.tableId = ctx.params.tableId
|
inputs.tableId = ctx.params.tableId
|
||||||
|
|
||||||
|
@ -177,7 +176,6 @@ export async function destroy(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkDestroy(ctx: UserCtx) {
|
export async function bulkDestroy(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
|
||||||
const tableId = ctx.params.tableId
|
const tableId = ctx.params.tableId
|
||||||
const table = await sdk.tables.getTable(tableId)
|
const table = await sdk.tables.getTable(tableId)
|
||||||
let { rows } = ctx.request.body
|
let { rows } = ctx.request.body
|
||||||
|
@ -206,6 +204,7 @@ export async function bulkDestroy(ctx: UserCtx) {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
const db = context.getAppDB()
|
||||||
await db.bulkDocs(processedRows.map(row => ({ ...row, _deleted: true })))
|
await db.bulkDocs(processedRows.map(row => ({ ...row, _deleted: true })))
|
||||||
}
|
}
|
||||||
// remove any attachments that were on the rows from object storage
|
// remove any attachments that were on the rows from object storage
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { quotas } from "@budibase/pro"
|
||||||
|
import {
|
||||||
|
UserCtx,
|
||||||
|
SearchResponse,
|
||||||
|
SortOrder,
|
||||||
|
SortType,
|
||||||
|
ViewV2,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
export async function searchView(ctx: UserCtx<void, SearchResponse>) {
|
||||||
|
const { viewId } = ctx.params
|
||||||
|
|
||||||
|
const view = await sdk.views.get(viewId)
|
||||||
|
if (!view) {
|
||||||
|
ctx.throw(404, `View ${viewId} not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.version !== 2) {
|
||||||
|
ctx.throw(400, `This method only supports viewsV2`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = await sdk.tables.getTable(view?.tableId)
|
||||||
|
|
||||||
|
const viewFields =
|
||||||
|
(view.columns &&
|
||||||
|
Object.entries(view.columns).length &&
|
||||||
|
Object.keys(sdk.views.enrichSchema(view, table.schema).schema)) ||
|
||||||
|
undefined
|
||||||
|
|
||||||
|
ctx.status = 200
|
||||||
|
const result = await quotas.addQuery(
|
||||||
|
() =>
|
||||||
|
sdk.rows.search({
|
||||||
|
tableId: view.tableId,
|
||||||
|
query: view.query || {},
|
||||||
|
fields: viewFields,
|
||||||
|
...getSortOptions(ctx, view),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
datasourceId: view.tableId,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result.rows.forEach(r => (r._viewId = view.id))
|
||||||
|
ctx.body = result
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSortOptions(
|
||||||
|
ctx: UserCtx,
|
||||||
|
view: ViewV2
|
||||||
|
):
|
||||||
|
| {
|
||||||
|
sort: string
|
||||||
|
sortOrder?: SortOrder
|
||||||
|
sortType?: SortType
|
||||||
|
}
|
||||||
|
| undefined {
|
||||||
|
const { sort_column, sort_order, sort_type } = ctx.query
|
||||||
|
if (Array.isArray(sort_column)) {
|
||||||
|
ctx.throw(400, "sort_column cannot be an array")
|
||||||
|
}
|
||||||
|
if (Array.isArray(sort_order)) {
|
||||||
|
ctx.throw(400, "sort_order cannot be an array")
|
||||||
|
}
|
||||||
|
if (Array.isArray(sort_type)) {
|
||||||
|
ctx.throw(400, "sort_type cannot be an array")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sort_column) {
|
||||||
|
return {
|
||||||
|
sort: sort_column,
|
||||||
|
sortOrder: sort_order as SortOrder,
|
||||||
|
sortType: sort_type as SortType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (view.sort) {
|
||||||
|
return {
|
||||||
|
sort: view.sort.field,
|
||||||
|
sortOrder: view.sort.order,
|
||||||
|
sortType: view.sort.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -2,15 +2,79 @@ import sdk from "../../../sdk"
|
||||||
import {
|
import {
|
||||||
CreateViewRequest,
|
CreateViewRequest,
|
||||||
Ctx,
|
Ctx,
|
||||||
|
UIFieldMetadata,
|
||||||
UpdateViewRequest,
|
UpdateViewRequest,
|
||||||
ViewResponse,
|
ViewResponse,
|
||||||
|
ViewV2,
|
||||||
|
RequiredKeys,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
|
||||||
|
async function parseSchemaUI(ctx: Ctx, view: CreateViewRequest) {
|
||||||
|
if (!view.schema) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOverrides(
|
||||||
|
newObj: Record<string, any>,
|
||||||
|
existingObj: Record<string, any>
|
||||||
|
) {
|
||||||
|
const result = Object.entries(newObj).some(([key, value]) => {
|
||||||
|
const isObject = typeof value === "object"
|
||||||
|
const existing = existingObj[key]
|
||||||
|
if (isObject && hasOverrides(value, existing || {})) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!isObject && value !== existing) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = await sdk.tables.getTable(view.tableId)
|
||||||
|
for (const [
|
||||||
|
fieldName,
|
||||||
|
{ order, width, visible, icon, ...schemaNonUI },
|
||||||
|
] of Object.entries(view.schema)) {
|
||||||
|
const overrides = hasOverrides(schemaNonUI, table.schema[fieldName])
|
||||||
|
if (overrides) {
|
||||||
|
ctx.throw(
|
||||||
|
400,
|
||||||
|
"This endpoint does not support overriding non UI fields in the schema"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaUI =
|
||||||
|
view.schema &&
|
||||||
|
Object.entries(view.schema).reduce((p, [fieldName, schemaValue]) => {
|
||||||
|
p[fieldName] = {
|
||||||
|
order: schemaValue.order,
|
||||||
|
width: schemaValue.width,
|
||||||
|
visible: schemaValue.visible,
|
||||||
|
icon: schemaValue.icon,
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}, {} as Record<string, RequiredKeys<UIFieldMetadata>>)
|
||||||
|
return schemaUI
|
||||||
|
}
|
||||||
|
|
||||||
export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
|
export async function create(ctx: Ctx<CreateViewRequest, ViewResponse>) {
|
||||||
const view = ctx.request.body
|
const view = ctx.request.body
|
||||||
const { tableId } = view
|
const { tableId } = view
|
||||||
|
|
||||||
const result = await sdk.views.create(tableId, view)
|
const schemaUI = await parseSchemaUI(ctx, view)
|
||||||
|
|
||||||
|
const parsedView: Omit<ViewV2, "id" | "version"> = {
|
||||||
|
name: view.name,
|
||||||
|
tableId: view.tableId,
|
||||||
|
query: view.query,
|
||||||
|
sort: view.sort,
|
||||||
|
columns: view.schema && Object.keys(view.schema),
|
||||||
|
schemaUI,
|
||||||
|
}
|
||||||
|
const result = await sdk.views.create(tableId, parsedView)
|
||||||
ctx.status = 201
|
ctx.status = 201
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: result,
|
data: result,
|
||||||
|
@ -30,7 +94,19 @@ export async function update(ctx: Ctx<UpdateViewRequest, ViewResponse>) {
|
||||||
|
|
||||||
const { tableId } = view
|
const { tableId } = view
|
||||||
|
|
||||||
const result = await sdk.views.update(tableId, view)
|
const schemaUI = await parseSchemaUI(ctx, view)
|
||||||
|
const parsedView: ViewV2 = {
|
||||||
|
id: view.id,
|
||||||
|
name: view.name,
|
||||||
|
version: view.version,
|
||||||
|
tableId: view.tableId,
|
||||||
|
query: view.query,
|
||||||
|
sort: view.sort,
|
||||||
|
columns: view.schema && Object.keys(view.schema),
|
||||||
|
schemaUI,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await sdk.views.update(tableId, parsedView)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
data: result,
|
data: result,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import authorized from "../../middleware/authorized"
|
||||||
import { paramResource, paramSubResource } from "../../middleware/resourceId"
|
import { paramResource, paramSubResource } from "../../middleware/resourceId"
|
||||||
import { permissions } from "@budibase/backend-core"
|
import { permissions } from "@budibase/backend-core"
|
||||||
import { internalSearchValidator } from "./utils/validators"
|
import { internalSearchValidator } from "./utils/validators"
|
||||||
|
import noViewData from "../../middleware/noViewData"
|
||||||
|
import trimViewRowInfo from "../../middleware/trimViewRowInfo"
|
||||||
|
import * as utils from "../../db/utils"
|
||||||
const { PermissionType, PermissionLevel } = permissions
|
const { PermissionType, PermissionLevel } = permissions
|
||||||
|
|
||||||
const router: Router = new Router()
|
const router: Router = new Router()
|
||||||
|
@ -146,11 +149,6 @@ router
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
authorized(PermissionType.TABLE, PermissionLevel.READ),
|
||||||
rowController.search
|
rowController.search
|
||||||
)
|
)
|
||||||
.get(
|
|
||||||
"/api/v2/views/:viewId/search",
|
|
||||||
authorized(PermissionType.VIEW, PermissionLevel.READ),
|
|
||||||
rowController.searchView
|
|
||||||
)
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/:tableId/rows Creates a new row
|
* @api {post} /api/:tableId/rows Creates a new row
|
||||||
* @apiName Creates a new row
|
* @apiName Creates a new row
|
||||||
|
@ -179,6 +177,7 @@ router
|
||||||
"/api/:tableId/rows",
|
"/api/:tableId/rows",
|
||||||
paramResource("tableId"),
|
paramResource("tableId"),
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
|
noViewData,
|
||||||
rowController.save
|
rowController.save
|
||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
|
@ -193,6 +192,7 @@ router
|
||||||
"/api/:tableId/rows",
|
"/api/:tableId/rows",
|
||||||
paramResource("tableId"),
|
paramResource("tableId"),
|
||||||
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
authorized(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||||
|
noViewData,
|
||||||
rowController.patch
|
rowController.patch
|
||||||
)
|
)
|
||||||
/**
|
/**
|
||||||
|
@ -268,4 +268,91 @@ router
|
||||||
rowController.exportRows
|
rowController.exportRows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router
|
||||||
|
.get(
|
||||||
|
"/api/v2/views/:viewId/search",
|
||||||
|
authorized(PermissionType.VIEW, PermissionLevel.READ),
|
||||||
|
rowController.views.searchView
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* @api {post} /api/:tableId/rows Creates a new row
|
||||||
|
* @apiName Creates a new row
|
||||||
|
* @apiGroup rows
|
||||||
|
* @apiPermission table write access
|
||||||
|
* @apiDescription This API will create a new row based on the supplied body. If the
|
||||||
|
* body includes an "_id" field then it will update an existing row if the field
|
||||||
|
* links to one. Please note that "_id", "_rev" and "tableId" are fields that are
|
||||||
|
* already used by Budibase tables and cannot be used for columns.
|
||||||
|
*
|
||||||
|
* @apiParam {string} tableId The ID of the table to save a row to.
|
||||||
|
*
|
||||||
|
* @apiParam (Body) {string} [_id] If the row exists already then an ID for the row must be provided.
|
||||||
|
* @apiParam (Body) {string} [_rev] If working with an existing row for an internal table its revision
|
||||||
|
* must also be provided.
|
||||||
|
* @apiParam (Body) {string} _viewId The ID of the view should be specified in the row body itself.
|
||||||
|
* @apiParam (Body) {string} tableId The ID of the table should also be specified in the row body itself.
|
||||||
|
* @apiParam (Body) {any} [any] Any field supplied in the body will be assessed to see if it matches
|
||||||
|
* a column in the specified table. All other fields will be dropped and not stored.
|
||||||
|
*
|
||||||
|
* @apiSuccess {string} _id The ID of the row that was just saved, if it was just created this
|
||||||
|
* is the rows new ID.
|
||||||
|
* @apiSuccess {string} [_rev] If saving to an internal table a revision will also be returned.
|
||||||
|
* @apiSuccess {object} body The contents of the row that was saved will be returned as well.
|
||||||
|
*/
|
||||||
|
.post(
|
||||||
|
"/api/v2/views/:viewId/rows",
|
||||||
|
paramResource("viewId"),
|
||||||
|
authorized(PermissionType.VIEW, PermissionLevel.WRITE),
|
||||||
|
trimViewRowInfo,
|
||||||
|
rowController.save
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* @api {patch} /api/v2/views/:viewId/rows/:rowId Updates a row
|
||||||
|
* @apiName Update a row
|
||||||
|
* @apiGroup rows
|
||||||
|
* @apiPermission table write access
|
||||||
|
* @apiDescription This endpoint is identical to the row creation endpoint but instead it will
|
||||||
|
* error if an _id isn't provided, it will only function for existing rows.
|
||||||
|
*/
|
||||||
|
.patch(
|
||||||
|
"/api/v2/views/:viewId/rows/:rowId",
|
||||||
|
paramResource("viewId"),
|
||||||
|
authorized(PermissionType.VIEW, PermissionLevel.WRITE),
|
||||||
|
trimViewRowInfo,
|
||||||
|
rowController.patch
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* @api {delete} /api/v2/views/:viewId/rows Delete rows for a view
|
||||||
|
* @apiName Delete rows for a view
|
||||||
|
* @apiGroup rows
|
||||||
|
* @apiPermission table write access
|
||||||
|
* @apiDescription This endpoint can delete a single row, or delete them in a bulk
|
||||||
|
* fashion.
|
||||||
|
*
|
||||||
|
* @apiParam {string} tableId The ID of the table the row is to be deleted from.
|
||||||
|
*
|
||||||
|
* @apiParam (Body) {object[]} [rows] If bulk deletion is desired then provide the rows in this
|
||||||
|
* key of the request body that are to be deleted.
|
||||||
|
* @apiParam (Body) {string} [_id] If deleting a single row then provide its ID in this field.
|
||||||
|
* @apiParam (Body) {string} [_rev] If deleting a single row from an internal table then provide its
|
||||||
|
* revision here.
|
||||||
|
*
|
||||||
|
* @apiSuccess {object[]|object} body If deleting bulk then the response body will be an array
|
||||||
|
* of the deleted rows, if deleting a single row then the body will contain a "row" property which
|
||||||
|
* is the deleted row.
|
||||||
|
*/
|
||||||
|
.delete(
|
||||||
|
"/api/v2/views/:viewId/rows",
|
||||||
|
paramResource("viewId"),
|
||||||
|
authorized(PermissionType.VIEW, PermissionLevel.WRITE),
|
||||||
|
// This is required as the implementation relies on the table id
|
||||||
|
(ctx, next) => {
|
||||||
|
ctx.params.tableId = utils.extractViewInfoFromID(
|
||||||
|
ctx.params.viewId
|
||||||
|
).tableId
|
||||||
|
return next()
|
||||||
|
},
|
||||||
|
rowController.destroy
|
||||||
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -16,13 +16,16 @@ import {
|
||||||
FieldType,
|
FieldType,
|
||||||
SortType,
|
SortType,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
PatchRowRequest,
|
DeleteRow,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import {
|
import {
|
||||||
expectAnyInternalColsAttributes,
|
expectAnyInternalColsAttributes,
|
||||||
generator,
|
generator,
|
||||||
structures,
|
structures,
|
||||||
} from "@budibase/backend-core/tests"
|
} from "@budibase/backend-core/tests"
|
||||||
|
import trimViewRowInfoMiddleware from "../../../middleware/trimViewRowInfo"
|
||||||
|
import noViewDataMiddleware from "../../../middleware/noViewData"
|
||||||
|
import router from "../row"
|
||||||
|
|
||||||
describe("/rows", () => {
|
describe("/rows", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
|
@ -391,6 +394,26 @@ describe("/rows", () => {
|
||||||
expect(saved.arrayFieldArrayStrKnown).toEqual(["One"])
|
expect(saved.arrayFieldArrayStrKnown).toEqual(["One"])
|
||||||
expect(saved.optsFieldStrKnown).toEqual("Alpha")
|
expect(saved.optsFieldStrKnown).toEqual("Alpha")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should throw an error when creating a table row with view id data", async () => {
|
||||||
|
const res = await request
|
||||||
|
.post(`/api/${row.tableId}/rows`)
|
||||||
|
.send({ ...row, _viewId: generator.guid() })
|
||||||
|
.set(config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(400)
|
||||||
|
expect(res.body.message).toEqual(
|
||||||
|
"Table row endpoints cannot contain view info"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should setup the noViewData middleware", async () => {
|
||||||
|
const route = router.stack.find(
|
||||||
|
r => r.methods.includes("POST") && r.path === "/api/:tableId/rows"
|
||||||
|
)
|
||||||
|
expect(route).toBeDefined()
|
||||||
|
expect(route?.stack).toContainEqual(noViewDataMiddleware)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("patch", () => {
|
describe("patch", () => {
|
||||||
|
@ -440,6 +463,33 @@ describe("/rows", () => {
|
||||||
await assertRowUsage(rowUsage)
|
await assertRowUsage(rowUsage)
|
||||||
await assertQueryUsage(queryUsage)
|
await assertQueryUsage(queryUsage)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should throw an error when creating a table row with view id data", async () => {
|
||||||
|
const existing = await config.createRow()
|
||||||
|
|
||||||
|
const res = await config.api.row.patch(
|
||||||
|
table._id!,
|
||||||
|
{
|
||||||
|
...existing,
|
||||||
|
_id: existing._id!,
|
||||||
|
_rev: existing._rev!,
|
||||||
|
tableId: table._id!,
|
||||||
|
_viewId: generator.guid(),
|
||||||
|
},
|
||||||
|
{ expectStatus: 400 }
|
||||||
|
)
|
||||||
|
expect(res.body.message).toEqual(
|
||||||
|
"Table row endpoints cannot contain view info"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should setup the noViewData middleware", async () => {
|
||||||
|
const route = router.stack.find(
|
||||||
|
r => r.methods.includes("PATCH") && r.path === "/api/:tableId/rows"
|
||||||
|
)
|
||||||
|
expect(route).toBeDefined()
|
||||||
|
expect(route?.stack).toContainEqual(noViewDataMiddleware)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("destroy", () => {
|
describe("destroy", () => {
|
||||||
|
@ -983,7 +1033,7 @@ describe("/rows", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const view = await config.api.viewV2.create({
|
const view = await config.api.viewV2.create({
|
||||||
columns: { name: { visible: true } },
|
schema: { name: {} },
|
||||||
})
|
})
|
||||||
const response = await config.api.viewV2.search(view.id)
|
const response = await config.api.viewV2.search(view.id)
|
||||||
|
|
||||||
|
@ -1008,4 +1058,208 @@ describe("/rows", () => {
|
||||||
expect(response.body.rows).toHaveLength(0)
|
expect(response.body.rows).toHaveLength(0)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("view 2.0", () => {
|
||||||
|
function userTable(): Table {
|
||||||
|
return {
|
||||||
|
name: "user",
|
||||||
|
type: "user",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
},
|
||||||
|
surname: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "age",
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "address",
|
||||||
|
},
|
||||||
|
jobTitle: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "jobTitle",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomRowData = () => ({
|
||||||
|
name: generator.first(),
|
||||||
|
surname: generator.last(),
|
||||||
|
age: generator.age(),
|
||||||
|
address: generator.address(),
|
||||||
|
jobTitle: generator.word(),
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("should persist a new row with only the provided view fields", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId: table._id!,
|
||||||
|
schema: {
|
||||||
|
name: { visible: true },
|
||||||
|
surname: { visible: true },
|
||||||
|
address: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = randomRowData()
|
||||||
|
const newRow = await config.api.viewV2.row.create(view.id, {
|
||||||
|
tableId: config.table!._id,
|
||||||
|
_viewId: view.id,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
const row = await config.api.row.get(table._id!, newRow._id!)
|
||||||
|
expect(row.body).toEqual({
|
||||||
|
name: data.name,
|
||||||
|
surname: data.surname,
|
||||||
|
address: data.address,
|
||||||
|
tableId: config.table!._id,
|
||||||
|
type: "row",
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
})
|
||||||
|
expect(row.body._viewId).toBeUndefined()
|
||||||
|
expect(row.body.age).toBeUndefined()
|
||||||
|
expect(row.body.jobTitle).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should setup the trimViewRowInfo middleware", async () => {
|
||||||
|
const route = router.stack.find(
|
||||||
|
r =>
|
||||||
|
r.methods.includes("POST") &&
|
||||||
|
r.path === "/api/v2/views/:viewId/rows"
|
||||||
|
)
|
||||||
|
expect(route).toBeDefined()
|
||||||
|
expect(route?.stack).toContainEqual(trimViewRowInfoMiddleware)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("patch", () => {
|
||||||
|
it("should update only the view fields for a row", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const tableId = table._id!
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId,
|
||||||
|
schema: {
|
||||||
|
name: { visible: true },
|
||||||
|
address: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const newRow = await config.api.viewV2.row.create(view.id, {
|
||||||
|
tableId,
|
||||||
|
_viewId: view.id,
|
||||||
|
...randomRowData(),
|
||||||
|
})
|
||||||
|
const newData = randomRowData()
|
||||||
|
await config.api.viewV2.row.update(view.id, newRow._id!, {
|
||||||
|
tableId,
|
||||||
|
_viewId: view.id,
|
||||||
|
_id: newRow._id!,
|
||||||
|
_rev: newRow._rev!,
|
||||||
|
...newData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const row = await config.api.row.get(tableId, newRow._id!)
|
||||||
|
expect(row.body).toEqual({
|
||||||
|
...newRow,
|
||||||
|
type: "row",
|
||||||
|
name: newData.name,
|
||||||
|
address: newData.address,
|
||||||
|
_id: expect.any(String),
|
||||||
|
_rev: expect.any(String),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
})
|
||||||
|
expect(row.body._viewId).toBeUndefined()
|
||||||
|
expect(row.body.age).toBeUndefined()
|
||||||
|
expect(row.body.jobTitle).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should setup the trimViewRowInfo middleware", async () => {
|
||||||
|
const route = router.stack.find(
|
||||||
|
r =>
|
||||||
|
r.methods.includes("PATCH") &&
|
||||||
|
r.path === "/api/v2/views/:viewId/rows/:rowId"
|
||||||
|
)
|
||||||
|
expect(route).toBeDefined()
|
||||||
|
expect(route?.stack).toContainEqual(trimViewRowInfoMiddleware)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("destroy", () => {
|
||||||
|
it("should be able to delete a row", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const tableId = table._id!
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId,
|
||||||
|
schema: {
|
||||||
|
name: { visible: true },
|
||||||
|
address: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const createdRow = await config.createRow()
|
||||||
|
const rowUsage = await getRowUsage()
|
||||||
|
const queryUsage = await getQueryUsage()
|
||||||
|
|
||||||
|
const body: DeleteRow = {
|
||||||
|
_id: createdRow._id!,
|
||||||
|
}
|
||||||
|
await config.api.viewV2.row.delete(view.id, body)
|
||||||
|
|
||||||
|
await assertRowUsage(rowUsage - 1)
|
||||||
|
await assertQueryUsage(queryUsage + 1)
|
||||||
|
|
||||||
|
await config.api.row.get(tableId, createdRow._id!, {
|
||||||
|
expectStatus: 404,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should be able to delete multiple rows", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const tableId = table._id!
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
tableId,
|
||||||
|
schema: {
|
||||||
|
name: { visible: true },
|
||||||
|
address: { visible: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const rows = [
|
||||||
|
await config.createRow(),
|
||||||
|
await config.createRow(),
|
||||||
|
await config.createRow(),
|
||||||
|
]
|
||||||
|
const rowUsage = await getRowUsage()
|
||||||
|
const queryUsage = await getQueryUsage()
|
||||||
|
|
||||||
|
await config.api.viewV2.row.delete(view.id, {
|
||||||
|
rows: [rows[0], rows[2]],
|
||||||
|
})
|
||||||
|
|
||||||
|
await assertRowUsage(rowUsage - 2)
|
||||||
|
await assertQueryUsage(queryUsage + 1)
|
||||||
|
|
||||||
|
await config.api.row.get(tableId, rows[0]._id!, {
|
||||||
|
expectStatus: 404,
|
||||||
|
})
|
||||||
|
await config.api.row.get(tableId, rows[2]._id!, {
|
||||||
|
expectStatus: 404,
|
||||||
|
})
|
||||||
|
await config.api.row.get(tableId, rows[1]._id!, { expectStatus: 200 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as setup from "./utilities"
|
import * as setup from "./utilities"
|
||||||
import {
|
import {
|
||||||
CreateViewRequest,
|
CreateViewRequest,
|
||||||
|
FieldSchema,
|
||||||
FieldType,
|
FieldType,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
SortType,
|
SortType,
|
||||||
|
@ -40,7 +41,7 @@ describe("/v2/views", () => {
|
||||||
order: SortOrder.DESCENDING,
|
order: SortOrder.DESCENDING,
|
||||||
type: SortType.STRING,
|
type: SortType.STRING,
|
||||||
},
|
},
|
||||||
columns: {
|
schema: {
|
||||||
name: {
|
name: {
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
|
@ -73,17 +74,111 @@ describe("/v2/views", () => {
|
||||||
const newView: CreateViewRequest = {
|
const newView: CreateViewRequest = {
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
tableId: config.table!._id!,
|
tableId: config.table!._id!,
|
||||||
...viewFilters,
|
query: viewFilters.query,
|
||||||
|
sort: viewFilters.sort,
|
||||||
}
|
}
|
||||||
|
delete newView.schema
|
||||||
const res = await config.api.viewV2.create(newView)
|
const res = await config.api.viewV2.create(newView)
|
||||||
|
|
||||||
expect(res).toEqual({
|
expect(res).toEqual({
|
||||||
...newView,
|
...newView,
|
||||||
...viewFilters,
|
query: viewFilters.query,
|
||||||
|
sort: viewFilters.sort,
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
version: 2,
|
version: 2,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("persist only UI schema overrides", async () => {
|
||||||
|
const newView: CreateViewRequest = {
|
||||||
|
name: generator.name(),
|
||||||
|
tableId: config.table!._id!,
|
||||||
|
schema: {
|
||||||
|
Price: {
|
||||||
|
name: "Price",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
visible: false,
|
||||||
|
icon: "ic",
|
||||||
|
},
|
||||||
|
} as Record<string, FieldSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdView = await config.api.viewV2.create(newView)
|
||||||
|
|
||||||
|
expect(await config.api.viewV2.get(createdView.id)).toEqual({
|
||||||
|
...newView,
|
||||||
|
schema: undefined,
|
||||||
|
columns: ["Price", "Category"],
|
||||||
|
schemaUI: {
|
||||||
|
Price: {
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
visible: false,
|
||||||
|
icon: "ic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: createdView.id,
|
||||||
|
version: 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throw an exception if the schema overrides a non UI field", async () => {
|
||||||
|
const newView: CreateViewRequest = {
|
||||||
|
name: generator.name(),
|
||||||
|
tableId: config.table!._id!,
|
||||||
|
schema: {
|
||||||
|
Price: {
|
||||||
|
name: "Price",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Record<string, FieldSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.api.viewV2.create(newView, {
|
||||||
|
expectStatus: 400,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("will not throw an exception if the schema is 'deleting' non UI fields", async () => {
|
||||||
|
const newView: CreateViewRequest = {
|
||||||
|
name: generator.name(),
|
||||||
|
tableId: config.table!._id!,
|
||||||
|
schema: {
|
||||||
|
Price: {
|
||||||
|
name: "Price",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
} as Record<string, FieldSchema>,
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.api.viewV2.create(newView, {
|
||||||
|
expectStatus: 201,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
|
@ -202,6 +297,94 @@ describe("/v2/views", () => {
|
||||||
status: 400,
|
status: 400,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("updates only UI schema overrides", async () => {
|
||||||
|
await config.api.viewV2.update({
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
Price: {
|
||||||
|
name: "Price",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
visible: false,
|
||||||
|
icon: "ic",
|
||||||
|
},
|
||||||
|
} as Record<string, FieldSchema>,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await config.api.viewV2.get(view.id)).toEqual({
|
||||||
|
...view,
|
||||||
|
schema: undefined,
|
||||||
|
columns: ["Price", "Category"],
|
||||||
|
schemaUI: {
|
||||||
|
Price: {
|
||||||
|
visible: true,
|
||||||
|
order: 1,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
visible: false,
|
||||||
|
icon: "ic",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: view.id,
|
||||||
|
version: 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throw an exception if the schema overrides a non UI field", async () => {
|
||||||
|
await config.api.viewV2.update(
|
||||||
|
{
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
Price: {
|
||||||
|
name: "Price",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
presence: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Record<string, FieldSchema>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectStatus: 400,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("will not throw an exception if the schema is 'deleting' non UI fields", async () => {
|
||||||
|
await config.api.viewV2.update(
|
||||||
|
{
|
||||||
|
...view,
|
||||||
|
schema: {
|
||||||
|
Price: {
|
||||||
|
name: "Price",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
Category: {
|
||||||
|
name: "Category",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
} as Record<string, FieldSchema>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
expectStatus: 200,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Ctx, Row } from "@budibase/types"
|
||||||
|
|
||||||
|
export default async (ctx: Ctx<Row>, next: any) => {
|
||||||
|
if (ctx.request.body._viewId) {
|
||||||
|
return ctx.throw(400, "Table row endpoints cannot contain view info")
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import { BBRequest, FieldType, Row, Table } from "@budibase/types"
|
||||||
|
import { Next } from "koa"
|
||||||
|
import * as utils from "../../db/utils"
|
||||||
|
import noViewDataMiddleware from "../noViewData"
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
next: Next
|
||||||
|
throw: jest.Mock<(status: number, message: string) => never>
|
||||||
|
middleware: typeof noViewDataMiddleware
|
||||||
|
params: Record<string, any>
|
||||||
|
request?: Pick<BBRequest<Row>, "body">
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.next = jest.fn()
|
||||||
|
this.throw = jest.fn()
|
||||||
|
this.params = {}
|
||||||
|
|
||||||
|
this.middleware = noViewDataMiddleware
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware(ctxRequestBody: Row) {
|
||||||
|
this.request = {
|
||||||
|
body: ctxRequestBody,
|
||||||
|
}
|
||||||
|
return this.middleware(
|
||||||
|
{
|
||||||
|
request: this.request as any,
|
||||||
|
throw: this.throw as any,
|
||||||
|
params: this.params,
|
||||||
|
} as any,
|
||||||
|
this.next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach() {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("noViewData middleware", () => {
|
||||||
|
let config: TestConfiguration
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getRandomData = () => ({
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: generator.name(),
|
||||||
|
age: generator.age(),
|
||||||
|
address: generator.address(),
|
||||||
|
})
|
||||||
|
|
||||||
|
it("it should pass without view id data", async () => {
|
||||||
|
const data = getRandomData()
|
||||||
|
await config.executeMiddleware({
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.next).toBeCalledTimes(1)
|
||||||
|
expect(config.throw).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("it should throw an error if _viewid is provided", async () => {
|
||||||
|
const data = getRandomData()
|
||||||
|
await config.executeMiddleware({
|
||||||
|
_viewId: generator.guid(),
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.throw).toBeCalledTimes(1)
|
||||||
|
expect(config.throw).toBeCalledWith(
|
||||||
|
400,
|
||||||
|
"Table row endpoints cannot contain view info"
|
||||||
|
)
|
||||||
|
expect(config.next).not.toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
|
import { BBRequest, FieldType, Row, Table } from "@budibase/types"
|
||||||
|
import * as utils from "../../db/utils"
|
||||||
|
import trimViewRowInfoMiddleware from "../trimViewRowInfo"
|
||||||
|
|
||||||
|
jest.mock("../../sdk", () => ({
|
||||||
|
views: {
|
||||||
|
...jest.requireActual("../../sdk/app/views"),
|
||||||
|
get: jest.fn(),
|
||||||
|
},
|
||||||
|
tables: {
|
||||||
|
getTable: jest.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import sdk from "../../sdk"
|
||||||
|
import { Next } from "koa"
|
||||||
|
|
||||||
|
const mockGetView = sdk.views.get as jest.MockedFunction<typeof sdk.views.get>
|
||||||
|
const mockGetTable = sdk.tables.getTable as jest.MockedFunction<
|
||||||
|
typeof sdk.tables.getTable
|
||||||
|
>
|
||||||
|
|
||||||
|
class TestConfiguration {
|
||||||
|
next: Next
|
||||||
|
throw: jest.Mock<(status: number, message: string) => never>
|
||||||
|
middleware: typeof trimViewRowInfoMiddleware
|
||||||
|
params: Record<string, any>
|
||||||
|
request?: Pick<BBRequest<Row>, "body">
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.next = jest.fn()
|
||||||
|
this.throw = jest.fn()
|
||||||
|
this.params = {}
|
||||||
|
|
||||||
|
this.middleware = trimViewRowInfoMiddleware
|
||||||
|
}
|
||||||
|
|
||||||
|
executeMiddleware(viewId: string, ctxRequestBody: Row) {
|
||||||
|
this.request = {
|
||||||
|
body: ctxRequestBody,
|
||||||
|
}
|
||||||
|
this.params.viewId = viewId
|
||||||
|
return this.middleware(
|
||||||
|
{
|
||||||
|
request: this.request as any,
|
||||||
|
next: this.next,
|
||||||
|
throw: this.throw as any,
|
||||||
|
params: this.params,
|
||||||
|
} as any,
|
||||||
|
this.next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach() {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("trimViewRowInfo middleware", () => {
|
||||||
|
let config: TestConfiguration
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = new TestConfiguration()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
config.afterEach()
|
||||||
|
})
|
||||||
|
|
||||||
|
const table: Table = {
|
||||||
|
_id: utils.generateTableID(),
|
||||||
|
name: generator.word(),
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
name: "name",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
name: "age",
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
},
|
||||||
|
address: {
|
||||||
|
name: "address",
|
||||||
|
type: FieldType.STRING,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
let viewId: string = undefined!
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
mockGetTable.mockResolvedValue(table)
|
||||||
|
viewId = utils.generateViewID(table._id!)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getRandomData = () => ({
|
||||||
|
_id: generator.guid(),
|
||||||
|
name: generator.name(),
|
||||||
|
age: generator.age(),
|
||||||
|
address: generator.address(),
|
||||||
|
})
|
||||||
|
|
||||||
|
it("when no columns are defined, same data is returned", async () => {
|
||||||
|
mockGetView.mockResolvedValue({
|
||||||
|
version: 2,
|
||||||
|
id: viewId,
|
||||||
|
name: generator.guid(),
|
||||||
|
tableId: table._id!,
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = getRandomData()
|
||||||
|
await config.executeMiddleware(viewId, {
|
||||||
|
_viewId: viewId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.request?.body).toEqual(data)
|
||||||
|
expect(config.params.tableId).toEqual(table._id)
|
||||||
|
|
||||||
|
expect(config.next).toBeCalledTimes(1)
|
||||||
|
expect(config.throw).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("when columns are defined, trimmed data is returned", async () => {
|
||||||
|
mockGetView.mockResolvedValue({
|
||||||
|
version: 2,
|
||||||
|
id: viewId,
|
||||||
|
name: generator.guid(),
|
||||||
|
tableId: table._id!,
|
||||||
|
columns: ["name", "address"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = getRandomData()
|
||||||
|
await config.executeMiddleware(viewId, {
|
||||||
|
_viewId: viewId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.request?.body).toEqual({
|
||||||
|
_id: data._id,
|
||||||
|
name: data.name,
|
||||||
|
address: data.address,
|
||||||
|
})
|
||||||
|
expect(config.params.tableId).toEqual(table._id)
|
||||||
|
|
||||||
|
expect(config.next).toBeCalledTimes(1)
|
||||||
|
expect(config.throw).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("it should throw an error if no viewid is provided on the body", async () => {
|
||||||
|
const data = getRandomData()
|
||||||
|
await config.executeMiddleware(viewId, {
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.throw).toBeCalledTimes(1)
|
||||||
|
expect(config.throw).toBeCalledWith(400, "_viewId is required")
|
||||||
|
expect(config.next).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("it should throw an error if no viewid is provided on the parameters", async () => {
|
||||||
|
const data = getRandomData()
|
||||||
|
await config.executeMiddleware(undefined as any, {
|
||||||
|
_viewId: viewId,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(config.throw).toBeCalledTimes(1)
|
||||||
|
expect(config.throw).toBeCalledWith(400, "viewId path is required")
|
||||||
|
expect(config.next).not.toBeCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { Ctx, Row } from "@budibase/types"
|
||||||
|
import * as utils from "../db/utils"
|
||||||
|
import sdk from "../sdk"
|
||||||
|
import { db } from "@budibase/backend-core"
|
||||||
|
import { Next } from "koa"
|
||||||
|
|
||||||
|
export default async (ctx: Ctx<Row>, next: Next) => {
|
||||||
|
const { body } = ctx.request
|
||||||
|
const { _viewId: viewId } = body
|
||||||
|
if (!viewId) {
|
||||||
|
return ctx.throw(400, "_viewId is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.params.viewId) {
|
||||||
|
return ctx.throw(400, "viewId path is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tableId } = utils.extractViewInfoFromID(ctx.params.viewId)
|
||||||
|
const { _viewId, ...trimmedView } = await trimViewFields(
|
||||||
|
viewId,
|
||||||
|
tableId,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
ctx.request.body = trimmedView
|
||||||
|
ctx.params.tableId = tableId
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function trimViewFields<T extends Row>(
|
||||||
|
viewId: string,
|
||||||
|
tableId: string,
|
||||||
|
data: T
|
||||||
|
): Promise<T> {
|
||||||
|
const view = await sdk.views.get(viewId)
|
||||||
|
if (!view?.columns || !Object.keys(view.columns).length) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = await sdk.tables.getTable(tableId)
|
||||||
|
const { schema } = sdk.views.enrichSchema(view!, table.schema)
|
||||||
|
const result: Record<string, any> = {}
|
||||||
|
for (const key of [
|
||||||
|
...Object.keys(schema),
|
||||||
|
...db.CONSTANT_EXTERNAL_ROW_COLS,
|
||||||
|
...db.CONSTANT_INTERNAL_ROW_COLS,
|
||||||
|
]) {
|
||||||
|
result[key] = data[key] !== null ? data[key] : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return result as T
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ jest.unmock("mysql2/promise")
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
describe("external", () => {
|
describe.skip("external", () => {
|
||||||
const config = new TestConfiguration()
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
let externalDatasource: Datasource
|
let externalDatasource: Datasource
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { HTTPError, context } from "@budibase/backend-core"
|
import { HTTPError, context } from "@budibase/backend-core"
|
||||||
import { TableSchema, UIFieldMetadata, View, ViewV2 } from "@budibase/types"
|
import { FieldSchema, TableSchema, View, ViewV2 } from "@budibase/types"
|
||||||
|
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import * as utils from "../../../db/utils"
|
import * as utils from "../../../db/utils"
|
||||||
|
@ -73,37 +73,34 @@ export function enrichSchema(view: View | ViewV2, tableSchema: TableSchema) {
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let schema = { ...tableSchema }
|
||||||
|
if (view.schemaUI) {
|
||||||
|
const viewOverridesEntries = Object.entries(view.schemaUI)
|
||||||
|
const viewSetsOrder = viewOverridesEntries.some(([_, v]) => v.order)
|
||||||
|
for (const [fieldName, schemaUI] of viewOverridesEntries) {
|
||||||
|
schema[fieldName] = {
|
||||||
|
...schema[fieldName],
|
||||||
|
...schemaUI,
|
||||||
|
order: viewSetsOrder
|
||||||
|
? schemaUI.order || undefined
|
||||||
|
: schema[fieldName].order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view?.columns?.length) {
|
||||||
|
const pickedSchema: Record<string, FieldSchema> = {}
|
||||||
|
for (const fieldName of view.columns) {
|
||||||
|
if (!schema[fieldName]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pickedSchema[fieldName] = { ...schema[fieldName] }
|
||||||
|
}
|
||||||
|
schema = pickedSchema
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...view,
|
...view,
|
||||||
schema:
|
schema: schema,
|
||||||
!view?.columns || !Object.entries(view?.columns).length
|
|
||||||
? tableSchema
|
|
||||||
: enrichViewV2Schema(tableSchema, view.columns),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function enrichViewV2Schema(
|
|
||||||
tableSchema: TableSchema,
|
|
||||||
viewOverrides: Record<string, UIFieldMetadata>
|
|
||||||
) {
|
|
||||||
const result: TableSchema = {}
|
|
||||||
const viewOverridesEntries = Object.entries(viewOverrides)
|
|
||||||
const viewSetsOrder = viewOverridesEntries.some(([_, v]) => v.order)
|
|
||||||
for (const [columnName, columnUIMetadata] of viewOverridesEntries) {
|
|
||||||
if (!columnUIMetadata.visible) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tableSchema[columnName]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableFieldSchema = tableSchema[columnName]
|
|
||||||
if (viewSetsOrder) {
|
|
||||||
delete tableFieldSchema.order
|
|
||||||
}
|
|
||||||
|
|
||||||
result[columnName] = merge(tableFieldSchema, columnUIMetadata)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
|
@ -102,18 +102,14 @@ describe("table sdk", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("if view schema only defines visiblility, should only fetch the selected fields", async () => {
|
it("if view schema only defines columns, should only fetch the selected fields", async () => {
|
||||||
const tableId = basicTable._id!
|
const tableId = basicTable._id!
|
||||||
const view: ViewV2 = {
|
const view: ViewV2 = {
|
||||||
version: 2,
|
version: 2,
|
||||||
id: generator.guid(),
|
id: generator.guid(),
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
tableId,
|
tableId,
|
||||||
columns: {
|
columns: ["name", "id"],
|
||||||
name: { visible: true },
|
|
||||||
id: { visible: true },
|
|
||||||
description: { visible: false },
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = enrichSchema(view, basicTable.schema)
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
@ -151,7 +147,7 @@ describe("table sdk", () => {
|
||||||
id: generator.guid(),
|
id: generator.guid(),
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
tableId,
|
tableId,
|
||||||
columns: { unnexisting: { visible: true }, name: { visible: true } },
|
columns: ["unnexisting", "name"],
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = enrichSchema(view, basicTable.schema)
|
const res = enrichSchema(view, basicTable.schema)
|
||||||
|
@ -175,16 +171,17 @@ describe("table sdk", () => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("if view schema only defines visiblility, should only fetch the selected fields", async () => {
|
it("if the view schema overrides the schema UI, the table schema should be overridden", async () => {
|
||||||
const tableId = basicTable._id!
|
const tableId = basicTable._id!
|
||||||
const view: ViewV2 = {
|
const view: ViewV2 = {
|
||||||
version: 2,
|
version: 2,
|
||||||
id: generator.guid(),
|
id: generator.guid(),
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
tableId,
|
tableId,
|
||||||
columns: {
|
columns: ["name", "id", "description"],
|
||||||
name: { visible: true },
|
schemaUI: {
|
||||||
id: { visible: true },
|
name: { visible: true, width: 100 },
|
||||||
|
id: { visible: true, width: 20 },
|
||||||
description: { visible: false },
|
description: { visible: false },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -200,7 +197,7 @@ describe("table sdk", () => {
|
||||||
name: "name",
|
name: "name",
|
||||||
order: 2,
|
order: 2,
|
||||||
visible: true,
|
visible: true,
|
||||||
width: 80,
|
width: 100,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
|
@ -210,23 +207,34 @@ describe("table sdk", () => {
|
||||||
name: "id",
|
name: "id",
|
||||||
order: 1,
|
order: 1,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
width: 20,
|
||||||
constraints: {
|
constraints: {
|
||||||
type: "number",
|
type: "number",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
name: "description",
|
||||||
|
visible: false,
|
||||||
|
width: 200,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("if view defines order, the table schema order should be ignored", async () => {
|
it("if the view defines order, the table schema order should be ignored", async () => {
|
||||||
const tableId = basicTable._id!
|
const tableId = basicTable._id!
|
||||||
const view: ViewV2 = {
|
const view: ViewV2 = {
|
||||||
version: 2,
|
version: 2,
|
||||||
id: generator.guid(),
|
id: generator.guid(),
|
||||||
name: generator.guid(),
|
name: generator.guid(),
|
||||||
tableId,
|
tableId,
|
||||||
columns: {
|
columns: ["name", "id", "description"],
|
||||||
|
schemaUI: {
|
||||||
name: { visible: true, order: 1 },
|
name: { visible: true, order: 1 },
|
||||||
id: { visible: true },
|
id: { visible: true },
|
||||||
description: { visible: false, order: 2 },
|
description: { visible: false, order: 2 },
|
||||||
|
@ -257,6 +265,16 @@ describe("table sdk", () => {
|
||||||
type: "number",
|
type: "number",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
name: "description",
|
||||||
|
order: 2,
|
||||||
|
visible: false,
|
||||||
|
width: 200,
|
||||||
|
constraints: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,6 +7,21 @@ export class RowAPI extends TestAPI {
|
||||||
super(config)
|
super(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get = async (
|
||||||
|
tableId: string,
|
||||||
|
rowId: string,
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
) => {
|
||||||
|
const request = this.request
|
||||||
|
.get(`/api/${tableId}/rows/${rowId}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(expectStatus)
|
||||||
|
if (expectStatus !== 404) {
|
||||||
|
request.expect("Content-Type", /json/)
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
patch = async (
|
patch = async (
|
||||||
tableId: string,
|
tableId: string,
|
||||||
row: PatchRowRequest,
|
row: PatchRowRequest,
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
import { CreateViewRequest, SortOrder, SortType, ViewV2 } from "@budibase/types"
|
import {
|
||||||
|
CreateViewRequest,
|
||||||
|
SortOrder,
|
||||||
|
SortType,
|
||||||
|
UpdateViewRequest,
|
||||||
|
DeleteRowRequest,
|
||||||
|
PatchRowRequest,
|
||||||
|
PatchRowResponse,
|
||||||
|
Row,
|
||||||
|
ViewV2,
|
||||||
|
} from "@budibase/types"
|
||||||
import TestConfiguration from "../TestConfiguration"
|
import TestConfiguration from "../TestConfiguration"
|
||||||
import { TestAPI } from "./base"
|
import { TestAPI } from "./base"
|
||||||
import { generator } from "@budibase/backend-core/tests"
|
import { generator } from "@budibase/backend-core/tests"
|
||||||
import { Response } from "superagent"
|
import { Response } from "superagent"
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
export class ViewV2API extends TestAPI {
|
export class ViewV2API extends TestAPI {
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
|
@ -33,7 +44,7 @@ export class ViewV2API extends TestAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
update = async (
|
update = async (
|
||||||
view: ViewV2,
|
view: UpdateViewRequest,
|
||||||
{
|
{
|
||||||
expectStatus,
|
expectStatus,
|
||||||
handleResponse,
|
handleResponse,
|
||||||
|
@ -62,6 +73,12 @@ export class ViewV2API extends TestAPI {
|
||||||
.expect(expectStatus)
|
.expect(expectStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get = async (viewId: string) => {
|
||||||
|
return await this.config.doInContext(this.config.appId, () =>
|
||||||
|
sdk.views.get(viewId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
search = async (
|
search = async (
|
||||||
viewId: string,
|
viewId: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
@ -93,4 +110,46 @@ export class ViewV2API extends TestAPI {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(expectStatus)
|
.expect(expectStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row = {
|
||||||
|
create: async (
|
||||||
|
viewId: string,
|
||||||
|
row: Row,
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
): Promise<Row> => {
|
||||||
|
const result = await this.request
|
||||||
|
.post(`/api/v2/views/${viewId}/rows`)
|
||||||
|
.send(row)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(expectStatus)
|
||||||
|
return result.body as Row
|
||||||
|
},
|
||||||
|
update: async (
|
||||||
|
viewId: string,
|
||||||
|
rowId: string,
|
||||||
|
row: PatchRowRequest,
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
): Promise<PatchRowResponse> => {
|
||||||
|
const result = await this.request
|
||||||
|
.patch(`/api/v2/views/${viewId}/rows/${rowId}`)
|
||||||
|
.send(row)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(expectStatus)
|
||||||
|
return result.body as PatchRowResponse
|
||||||
|
},
|
||||||
|
delete: async (
|
||||||
|
viewId: string,
|
||||||
|
body: DeleteRowRequest,
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
): Promise<any> => {
|
||||||
|
const result = await this.request
|
||||||
|
.delete(`/api/v2/views/${viewId}/rows`)
|
||||||
|
.send(body)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect(expectStatus)
|
||||||
|
return result.body
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
import { TableSchema, ViewV2 } from "../../../documents"
|
import { ViewV2, UIFieldMetadata } from "../../../documents"
|
||||||
|
|
||||||
export interface ViewResponse {
|
export interface ViewResponse {
|
||||||
data: ViewV2
|
data: ViewV2
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateViewRequest = Omit<ViewV2, "version" | "id">
|
export interface CreateViewRequest
|
||||||
|
extends Omit<ViewV2, "version" | "id" | "columns" | "schemaUI"> {
|
||||||
|
schema?: Record<string, UIFieldMetadata>
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateViewRequest = ViewV2
|
export interface UpdateViewRequest
|
||||||
|
extends Omit<ViewV2, "columns" | "schemaUI"> {
|
||||||
|
schema?: Record<string, UIFieldMetadata>
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@ export interface ViewV2 {
|
||||||
order?: SortOrder
|
order?: SortOrder
|
||||||
type?: SortType
|
type?: SortType
|
||||||
}
|
}
|
||||||
columns?: Record<string, UIFieldMetadata>
|
columns?: string[]
|
||||||
|
schemaUI?: Record<string, UIFieldMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema
|
export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from "./documents"
|
export * from "./documents"
|
||||||
export * from "./sdk"
|
export * from "./sdk"
|
||||||
export * from "./api"
|
export * from "./api"
|
||||||
|
export * from "./shared"
|
||||||
|
|
|
@ -3,3 +3,7 @@ export type DeepPartial<T> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ISO8601 = string
|
export type ISO8601 = string
|
||||||
|
|
||||||
|
export type RequiredKeys<T> = {
|
||||||
|
[K in keyof Required<T>]: T[K]
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue