Merge branch 'develop' into backmerge-20230809
This commit is contained in:
commit
9e6bfaa958
|
@ -25,20 +25,20 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != 'Budibase/budibase'
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn lint
|
- run: yarn lint
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
@ -46,20 +46,20 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != 'Budibase/budibase'
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
# Run build all the projects
|
# Run build all the projects
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
# Check the types of the projects built via esbuild
|
# Check the types of the projects built via esbuild
|
||||||
|
@ -70,20 +70,20 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != 'Budibase/budibase'
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
- run: yarn test --ignore=@budibase/worker --ignore=@budibase/server --ignore=@budibase/pro
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -96,21 +96,22 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != 'Budibase/budibase'
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
- name: Test worker and server
|
||||||
|
run: yarn test --scope=@budibase/worker --scope=@budibase/server
|
||||||
- uses: codecov/codecov-action@v3
|
- uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
token: ${{ secrets.CODECOV_TOKEN || github.token }} # not required for public repos
|
||||||
|
@ -119,7 +120,7 @@ jobs:
|
||||||
|
|
||||||
test-pro:
|
test-pro:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -127,12 +128,12 @@ jobs:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn test --scope=@budibase/pro
|
- run: yarn test --scope=@budibase/pro
|
||||||
|
|
||||||
integration-test:
|
integration-test:
|
||||||
|
@ -140,20 +141,20 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||||
- name: Checkout repo only
|
- name: Checkout repo only
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
if: github.repository != 'Budibase/budibase'
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
|
||||||
|
|
||||||
- name: Use Node.js 14.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
- run: yarn
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn build --projects=@budibase/server,@budibase/worker,@budibase/client
|
- run: yarn build --projects=@budibase/server,@budibase/worker,@budibase/client
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
|
@ -166,7 +167,7 @@ jobs:
|
||||||
|
|
||||||
check-pro-submodule:
|
check-pro-submodule:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'Budibase/budibase'
|
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo and submodules
|
- name: Checkout repo and submodules
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
name: check_unreleased_changes
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check_unreleased:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check for unreleased changes
|
||||||
|
env:
|
||||||
|
REPO: "Budibase/budibase"
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
RELEASE_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||||
|
"https://api.github.com/repos/$REPO/releases/latest" | \
|
||||||
|
jq -r .published_at)
|
||||||
|
COMMIT_TIMESTAMP=$(curl -s -H "Authorization: token $TOKEN" \
|
||||||
|
"https://api.github.com/repos/$REPO/commits/master" | \
|
||||||
|
jq -r .commit.committer.date)
|
||||||
|
RELEASE_SECONDS=$(date --date="$RELEASE_TIMESTAMP" "+%s")
|
||||||
|
COMMIT_SECONDS=$(date --date="$COMMIT_TIMESTAMP" "+%s")
|
||||||
|
if (( COMMIT_SECONDS > RELEASE_SECONDS )); then
|
||||||
|
echo "There are unreleased changes. Please release these changes before merging."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No unreleased changes detected."
|
|
@ -44,7 +44,7 @@ jobs:
|
||||||
|
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 14.x
|
node-version: 18.x
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
- name: Update versions
|
- name: Update versions
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
nodejs 14.21.3
|
nodejs 18.17.0
|
||||||
python 3.10.0
|
python 3.10.0
|
||||||
yarn 1.22.19
|
yarn 1.22.19
|
||||||
|
|
|
@ -90,7 +90,7 @@ Component libraries are collections of components as well as the definition of t
|
||||||
|
|
||||||
#### 1. Prerequisites
|
#### 1. Prerequisites
|
||||||
|
|
||||||
- NodeJS version `14.x.x`
|
- NodeJS version `18.x.x`
|
||||||
- Python version `3.x`
|
- Python version `3.x`
|
||||||
|
|
||||||
### Using asdf (recommended)
|
### Using asdf (recommended)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:14-slim as build
|
FROM node:18-slim as build
|
||||||
|
|
||||||
# install node-gyp dependencies
|
# install node-gyp dependencies
|
||||||
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends apt-utils cron g++ make python
|
||||||
|
|
|
@ -109,7 +109,7 @@
|
||||||
"@budibase/types": "0.0.0"
|
"@budibase/types": "0.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0 <15.0.0"
|
"node": ">=18.0.0 <19.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ function getRedisConfig() {
|
||||||
|
|
||||||
export function setupEnv(...envs: any[]) {
|
export function setupEnv(...envs: any[]) {
|
||||||
const couch = getCouchConfig(),
|
const couch = getCouchConfig(),
|
||||||
minio = getCouchConfig(),
|
minio = getMinioConfig(),
|
||||||
redis = getRedisConfig()
|
redis = getRedisConfig()
|
||||||
const configs = [
|
const configs = [
|
||||||
{ key: "COUCH_DB_PORT", value: couch.port },
|
{ key: "COUCH_DB_PORT", value: couch.port },
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 3495feff36b73ab9abf2cc45df39900e79d38e27
|
Subproject commit ecee8071ebe0f98a5bb19646954e373264be210d
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:14-slim
|
FROM node:18-slim
|
||||||
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
|
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
|
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
|
||||||
|
|
|
@ -5,8 +5,9 @@ if [[ -n $CI ]]
|
||||||
then
|
then
|
||||||
# --runInBand performs better in ci where resources are limited
|
# --runInBand performs better in ci where resources are limited
|
||||||
export NODE_OPTIONS="--max-old-space-size=4096"
|
export NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
echo "jest --coverage --runInBand --forceExit --bail"
|
node ../../node_modules/jest/bin/jest.js --version
|
||||||
jest --coverage --runInBand --forceExit --bail
|
echo "jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail"
|
||||||
|
jest --coverage --maxWorkers=2 --forceExit --workerIdleMemoryLimit=2000MB --bail
|
||||||
else
|
else
|
||||||
# --maxWorkers performs better in development
|
# --maxWorkers performs better in development
|
||||||
echo "jest --coverage --maxWorkers=2 --forceExit"
|
echo "jest --coverage --maxWorkers=2 --forceExit"
|
||||||
|
|
|
@ -11,6 +11,9 @@ import {
|
||||||
Row,
|
Row,
|
||||||
PatchRowRequest,
|
PatchRowRequest,
|
||||||
PatchRowResponse,
|
PatchRowResponse,
|
||||||
|
SearchRowResponse,
|
||||||
|
SearchRowRequest,
|
||||||
|
SearchParams,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
import { gridSocket } from "../../../websockets"
|
import { gridSocket } from "../../../websockets"
|
||||||
|
@ -197,10 +200,10 @@ export async function destroy(ctx: UserCtx<DeleteRowRequest>) {
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function search(ctx: any) {
|
export async function search(ctx: Ctx<SearchRowRequest, SearchRowResponse>) {
|
||||||
const tableId = utils.getTableId(ctx)
|
const tableId = utils.getTableId(ctx)
|
||||||
|
|
||||||
const searchParams = {
|
const searchParams: SearchParams = {
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
tableId,
|
tableId,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
import { quotas } from "@budibase/pro"
|
import { quotas } from "@budibase/pro"
|
||||||
import {
|
import {
|
||||||
UserCtx,
|
UserCtx,
|
||||||
SearchResponse,
|
|
||||||
SortOrder,
|
|
||||||
SortType,
|
|
||||||
ViewV2,
|
ViewV2,
|
||||||
|
SearchRowResponse,
|
||||||
|
SearchViewRowRequest,
|
||||||
|
RequiredKeys,
|
||||||
|
SearchParams,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
export async function searchView(ctx: UserCtx<void, SearchResponse>) {
|
export async function searchView(
|
||||||
|
ctx: UserCtx<SearchViewRowRequest, SearchRowResponse>
|
||||||
|
) {
|
||||||
const { viewId } = ctx.params
|
const { viewId } = ctx.params
|
||||||
|
|
||||||
const view = await sdk.views.get(viewId)
|
const view = await sdk.views.get(viewId)
|
||||||
|
@ -29,49 +33,35 @@ export async function searchView(ctx: UserCtx<void, SearchResponse>) {
|
||||||
undefined
|
undefined
|
||||||
|
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
const result = await quotas.addQuery(
|
|
||||||
() =>
|
const { body } = ctx.request
|
||||||
sdk.rows.search({
|
const query = dataFilters.buildLuceneQuery(view.query || [])
|
||||||
tableId: view.tableId,
|
|
||||||
query: view.query || {},
|
const searchOptions: RequiredKeys<SearchViewRowRequest> &
|
||||||
fields: viewFields,
|
RequiredKeys<Pick<SearchParams, "tableId" | "query" | "fields">> = {
|
||||||
...getSortOptions(ctx, view),
|
tableId: view.tableId,
|
||||||
}),
|
query,
|
||||||
{
|
fields: viewFields,
|
||||||
datasourceId: view.tableId,
|
...getSortOptions(body, view),
|
||||||
}
|
limit: body.limit,
|
||||||
)
|
bookmark: body.bookmark,
|
||||||
|
paginate: body.paginate,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await quotas.addQuery(() => sdk.rows.search(searchOptions), {
|
||||||
|
datasourceId: view.tableId,
|
||||||
|
})
|
||||||
|
|
||||||
result.rows.forEach(r => (r._viewId = view.id))
|
result.rows.forEach(r => (r._viewId = view.id))
|
||||||
ctx.body = result
|
ctx.body = result
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSortOptions(
|
function getSortOptions(request: SearchViewRowRequest, view: ViewV2) {
|
||||||
ctx: UserCtx,
|
if (request.sort) {
|
||||||
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 {
|
return {
|
||||||
sort: sort_column,
|
sort: request.sort,
|
||||||
sortOrder: sort_order as SortOrder,
|
sortOrder: request.sortOrder,
|
||||||
sortType: sort_type as SortType,
|
sortType: request.sortType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (view.sort) {
|
if (view.sort) {
|
||||||
|
@ -82,5 +72,9 @@ function getSortOptions(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return {
|
||||||
|
sort: undefined,
|
||||||
|
sortOrder: undefined,
|
||||||
|
sortType: undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,7 +269,7 @@ router
|
||||||
)
|
)
|
||||||
|
|
||||||
router
|
router
|
||||||
.get(
|
.post(
|
||||||
"/api/v2/views/:viewId/search",
|
"/api/v2/views/:viewId/search",
|
||||||
authorized(PermissionType.VIEW, PermissionLevel.READ),
|
authorized(PermissionType.VIEW, PermissionLevel.READ),
|
||||||
rowController.views.searchView
|
rowController.views.searchView
|
||||||
|
|
|
@ -813,252 +813,6 @@ describe("/rows", () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("view search", () => {
|
|
||||||
function userTable(): Table {
|
|
||||||
return {
|
|
||||||
name: "user",
|
|
||||||
type: "user",
|
|
||||||
schema: {
|
|
||||||
name: {
|
|
||||||
type: FieldType.STRING,
|
|
||||||
name: "name",
|
|
||||||
constraints: { type: "string" },
|
|
||||||
},
|
|
||||||
age: {
|
|
||||||
type: FieldType.NUMBER,
|
|
||||||
name: "age",
|
|
||||||
constraints: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
it("returns table rows from view", async () => {
|
|
||||||
const table = await config.createTable(userTable())
|
|
||||||
const rows = []
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
rows.push(await config.createRow({ tableId: table._id }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const createViewResponse = await config.api.viewV2.create()
|
|
||||||
const response = await config.api.viewV2.search(createViewResponse.id)
|
|
||||||
|
|
||||||
expect(response.body.rows).toHaveLength(10)
|
|
||||||
expect(response.body).toEqual({
|
|
||||||
rows: expect.arrayContaining(rows.map(expect.objectContaining)),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("searching respects the view filters", async () => {
|
|
||||||
const table = await config.createTable(userTable())
|
|
||||||
const expectedRows = []
|
|
||||||
for (let i = 0; i < 10; i++)
|
|
||||||
await config.createRow({
|
|
||||||
tableId: table._id,
|
|
||||||
name: generator.name(),
|
|
||||||
age: generator.integer({ min: 10, max: 30 }),
|
|
||||||
})
|
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++)
|
|
||||||
expectedRows.push(
|
|
||||||
await config.createRow({
|
|
||||||
tableId: table._id,
|
|
||||||
name: generator.name(),
|
|
||||||
age: 40,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const createViewResponse = await config.api.viewV2.create({
|
|
||||||
query: { equal: { age: 40 } },
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await config.api.viewV2.search(createViewResponse.id)
|
|
||||||
|
|
||||||
expect(response.body.rows).toHaveLength(5)
|
|
||||||
expect(response.body).toEqual({
|
|
||||||
rows: expect.arrayContaining(expectedRows.map(expect.objectContaining)),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const sortTestOptions: [
|
|
||||||
{
|
|
||||||
field: string
|
|
||||||
order?: SortOrder
|
|
||||||
type?: SortType
|
|
||||||
},
|
|
||||||
string[]
|
|
||||||
][] = [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
order: SortOrder.ASCENDING,
|
|
||||||
type: SortType.STRING,
|
|
||||||
},
|
|
||||||
["Alice", "Bob", "Charly", "Danny"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
},
|
|
||||||
["Alice", "Bob", "Charly", "Danny"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
order: SortOrder.DESCENDING,
|
|
||||||
},
|
|
||||||
["Danny", "Charly", "Bob", "Alice"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "name",
|
|
||||||
order: SortOrder.DESCENDING,
|
|
||||||
type: SortType.STRING,
|
|
||||||
},
|
|
||||||
["Danny", "Charly", "Bob", "Alice"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "age",
|
|
||||||
order: SortOrder.ASCENDING,
|
|
||||||
type: SortType.number,
|
|
||||||
},
|
|
||||||
["Danny", "Alice", "Charly", "Bob"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "age",
|
|
||||||
order: SortOrder.ASCENDING,
|
|
||||||
},
|
|
||||||
["Danny", "Alice", "Charly", "Bob"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "age",
|
|
||||||
order: SortOrder.DESCENDING,
|
|
||||||
},
|
|
||||||
["Bob", "Charly", "Alice", "Danny"],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
field: "age",
|
|
||||||
order: SortOrder.DESCENDING,
|
|
||||||
type: SortType.number,
|
|
||||||
},
|
|
||||||
["Bob", "Charly", "Alice", "Danny"],
|
|
||||||
],
|
|
||||||
]
|
|
||||||
|
|
||||||
it.each(sortTestOptions)(
|
|
||||||
"allow sorting (%s)",
|
|
||||||
async (sortParams, expected) => {
|
|
||||||
await config.createTable(userTable())
|
|
||||||
const users = [
|
|
||||||
{ name: "Alice", age: 25 },
|
|
||||||
{ name: "Bob", age: 30 },
|
|
||||||
{ name: "Charly", age: 27 },
|
|
||||||
{ name: "Danny", age: 15 },
|
|
||||||
]
|
|
||||||
for (const user of users) {
|
|
||||||
await config.createRow({
|
|
||||||
tableId: config.table!._id,
|
|
||||||
...user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const createViewResponse = await config.api.viewV2.create({
|
|
||||||
sort: sortParams,
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await config.api.viewV2.search(createViewResponse.id)
|
|
||||||
|
|
||||||
expect(response.body.rows).toHaveLength(4)
|
|
||||||
expect(response.body).toEqual({
|
|
||||||
rows: expected.map(name => expect.objectContaining({ name })),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
it.each(sortTestOptions)(
|
|
||||||
"allow override the default view sorting (%s)",
|
|
||||||
async (sortParams, expected) => {
|
|
||||||
await config.createTable(userTable())
|
|
||||||
const users = [
|
|
||||||
{ name: "Alice", age: 25 },
|
|
||||||
{ name: "Bob", age: 30 },
|
|
||||||
{ name: "Charly", age: 27 },
|
|
||||||
{ name: "Danny", age: 15 },
|
|
||||||
]
|
|
||||||
for (const user of users) {
|
|
||||||
await config.createRow({
|
|
||||||
tableId: config.table!._id,
|
|
||||||
...user,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const createViewResponse = await config.api.viewV2.create({
|
|
||||||
sort: {
|
|
||||||
field: "name",
|
|
||||||
order: SortOrder.ASCENDING,
|
|
||||||
type: SortType.STRING,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await config.api.viewV2.search(createViewResponse.id, {
|
|
||||||
sort: {
|
|
||||||
column: sortParams.field,
|
|
||||||
order: sortParams.order,
|
|
||||||
type: sortParams.type,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response.body.rows).toHaveLength(4)
|
|
||||||
expect(response.body).toEqual({
|
|
||||||
rows: expected.map(name => expect.objectContaining({ name })),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
it("when schema is defined, defined columns and row attributes are returned", async () => {
|
|
||||||
const table = await config.createTable(userTable())
|
|
||||||
const rows = []
|
|
||||||
for (let i = 0; i < 10; i++) {
|
|
||||||
rows.push(
|
|
||||||
await config.createRow({
|
|
||||||
tableId: table._id,
|
|
||||||
name: generator.name(),
|
|
||||||
age: generator.age(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const view = await config.api.viewV2.create({
|
|
||||||
schema: { name: {} },
|
|
||||||
})
|
|
||||||
const response = await config.api.viewV2.search(view.id)
|
|
||||||
|
|
||||||
expect(response.body.rows).toHaveLength(10)
|
|
||||||
expect(response.body.rows).toEqual(
|
|
||||||
expect.arrayContaining(
|
|
||||||
rows.map(r => ({
|
|
||||||
...expectAnyInternalColsAttributes,
|
|
||||||
_viewId: view.id,
|
|
||||||
name: r.name,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("views without data can be returned", async () => {
|
|
||||||
const table = await config.createTable(userTable())
|
|
||||||
|
|
||||||
const createViewResponse = await config.api.viewV2.create()
|
|
||||||
const response = await config.api.viewV2.search(createViewResponse.id)
|
|
||||||
|
|
||||||
expect(response.body.rows).toHaveLength(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("view 2.0", () => {
|
describe("view 2.0", () => {
|
||||||
function userTable(): Table {
|
function userTable(): Table {
|
||||||
return {
|
return {
|
||||||
|
@ -1261,5 +1015,327 @@ describe("/rows", () => {
|
||||||
await config.api.row.get(tableId, rows[1]._id!, { expectStatus: 200 })
|
await config.api.row.get(tableId, rows[1]._id!, { expectStatus: 200 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("view search", () => {
|
||||||
|
function userTable(): Table {
|
||||||
|
return {
|
||||||
|
name: "user",
|
||||||
|
type: "user",
|
||||||
|
schema: {
|
||||||
|
name: {
|
||||||
|
type: FieldType.STRING,
|
||||||
|
name: "name",
|
||||||
|
constraints: { type: "string" },
|
||||||
|
},
|
||||||
|
age: {
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
name: "age",
|
||||||
|
constraints: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("returns table rows from view", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
rows.push(await config.createRow({ tableId: table._id }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create()
|
||||||
|
const response = await config.api.viewV2.search(createViewResponse.id)
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(10)
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
rows: expect.arrayContaining(rows.map(expect.objectContaining)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("searching respects the view filters", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const expectedRows = []
|
||||||
|
for (let i = 0; i < 10; i++)
|
||||||
|
await config.createRow({
|
||||||
|
tableId: table._id,
|
||||||
|
name: generator.name(),
|
||||||
|
age: generator.integer({ min: 10, max: 30 }),
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++)
|
||||||
|
expectedRows.push(
|
||||||
|
await config.createRow({
|
||||||
|
tableId: table._id,
|
||||||
|
name: generator.name(),
|
||||||
|
age: 40,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create({
|
||||||
|
query: [{ operator: "equal", field: "age", value: 40 }],
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(createViewResponse.id)
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(5)
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
rows: expect.arrayContaining(
|
||||||
|
expectedRows.map(expect.objectContaining)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortTestOptions: [
|
||||||
|
{
|
||||||
|
field: string
|
||||||
|
order?: SortOrder
|
||||||
|
type?: SortType
|
||||||
|
},
|
||||||
|
string[]
|
||||||
|
][] = [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
order: SortOrder.ASCENDING,
|
||||||
|
type: SortType.STRING,
|
||||||
|
},
|
||||||
|
["Alice", "Bob", "Charly", "Danny"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
},
|
||||||
|
["Alice", "Bob", "Charly", "Danny"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
order: SortOrder.DESCENDING,
|
||||||
|
},
|
||||||
|
["Danny", "Charly", "Bob", "Alice"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "name",
|
||||||
|
order: SortOrder.DESCENDING,
|
||||||
|
type: SortType.STRING,
|
||||||
|
},
|
||||||
|
["Danny", "Charly", "Bob", "Alice"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
order: SortOrder.ASCENDING,
|
||||||
|
type: SortType.number,
|
||||||
|
},
|
||||||
|
["Danny", "Alice", "Charly", "Bob"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
order: SortOrder.ASCENDING,
|
||||||
|
},
|
||||||
|
["Danny", "Alice", "Charly", "Bob"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
order: SortOrder.DESCENDING,
|
||||||
|
},
|
||||||
|
["Bob", "Charly", "Alice", "Danny"],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
field: "age",
|
||||||
|
order: SortOrder.DESCENDING,
|
||||||
|
type: SortType.number,
|
||||||
|
},
|
||||||
|
["Bob", "Charly", "Alice", "Danny"],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
it.each(sortTestOptions)(
|
||||||
|
"allow sorting (%s)",
|
||||||
|
async (sortParams, expected) => {
|
||||||
|
await config.createTable(userTable())
|
||||||
|
const users = [
|
||||||
|
{ name: "Alice", age: 25 },
|
||||||
|
{ name: "Bob", age: 30 },
|
||||||
|
{ name: "Charly", age: 27 },
|
||||||
|
{ name: "Danny", age: 15 },
|
||||||
|
]
|
||||||
|
for (const user of users) {
|
||||||
|
await config.createRow({
|
||||||
|
tableId: config.table!._id,
|
||||||
|
...user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create({
|
||||||
|
sort: sortParams,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(createViewResponse.id)
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(4)
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
rows: expected.map(name => expect.objectContaining({ name })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it.each(sortTestOptions)(
|
||||||
|
"allow override the default view sorting (%s)",
|
||||||
|
async (sortParams, expected) => {
|
||||||
|
await config.createTable(userTable())
|
||||||
|
const users = [
|
||||||
|
{ name: "Alice", age: 25 },
|
||||||
|
{ name: "Bob", age: 30 },
|
||||||
|
{ name: "Charly", age: 27 },
|
||||||
|
{ name: "Danny", age: 15 },
|
||||||
|
]
|
||||||
|
for (const user of users) {
|
||||||
|
await config.createRow({
|
||||||
|
tableId: config.table!._id,
|
||||||
|
...user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create({
|
||||||
|
sort: {
|
||||||
|
field: "name",
|
||||||
|
order: SortOrder.ASCENDING,
|
||||||
|
type: SortType.STRING,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.viewV2.search(
|
||||||
|
createViewResponse.id,
|
||||||
|
{
|
||||||
|
sort: sortParams.field,
|
||||||
|
sortOrder: sortParams.order,
|
||||||
|
sortType: sortParams.type,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(4)
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
rows: expected.map(name => expect.objectContaining({ name })),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it("when schema is defined, defined columns and row attributes are returned", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
rows.push(
|
||||||
|
await config.createRow({
|
||||||
|
tableId: table._id,
|
||||||
|
name: generator.name(),
|
||||||
|
age: generator.age(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = await config.api.viewV2.create({
|
||||||
|
schema: { name: {} },
|
||||||
|
})
|
||||||
|
const response = await config.api.viewV2.search(view.id)
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(10)
|
||||||
|
expect(response.body.rows).toEqual(
|
||||||
|
expect.arrayContaining(
|
||||||
|
rows.map(r => ({
|
||||||
|
...expectAnyInternalColsAttributes,
|
||||||
|
_viewId: view.id,
|
||||||
|
name: r.name,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("views without data can be returned", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create()
|
||||||
|
const response = await config.api.viewV2.search(createViewResponse.id)
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("respects the limit parameter", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
rows.push(await config.createRow({ tableId: table._id }))
|
||||||
|
}
|
||||||
|
const limit = generator.integer({ min: 1, max: 8 })
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create()
|
||||||
|
const response = await config.api.viewV2.search(createViewResponse.id, {
|
||||||
|
limit,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.body.rows).toHaveLength(limit)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("can handle pagination", async () => {
|
||||||
|
const table = await config.createTable(userTable())
|
||||||
|
const rows = []
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
rows.push(await config.createRow({ tableId: table._id }))
|
||||||
|
}
|
||||||
|
// rows.sort((a, b) => (a._id! > b._id! ? 1 : -1))
|
||||||
|
|
||||||
|
const createViewResponse = await config.api.viewV2.create()
|
||||||
|
const allRows = (await config.api.viewV2.search(createViewResponse.id))
|
||||||
|
.body.rows
|
||||||
|
|
||||||
|
const firstPageResponse = await config.api.viewV2.search(
|
||||||
|
createViewResponse.id,
|
||||||
|
{
|
||||||
|
paginate: true,
|
||||||
|
limit: 4,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(firstPageResponse.body).toEqual({
|
||||||
|
rows: expect.arrayContaining(allRows.slice(0, 4)),
|
||||||
|
totalRows: 10,
|
||||||
|
hasNextPage: true,
|
||||||
|
bookmark: expect.any(String),
|
||||||
|
})
|
||||||
|
|
||||||
|
const secondPageResponse = await config.api.viewV2.search(
|
||||||
|
createViewResponse.id,
|
||||||
|
{
|
||||||
|
paginate: true,
|
||||||
|
limit: 4,
|
||||||
|
bookmark: firstPageResponse.body.bookmark,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(secondPageResponse.body).toEqual({
|
||||||
|
rows: expect.arrayContaining(allRows.slice(4, 8)),
|
||||||
|
totalRows: 10,
|
||||||
|
hasNextPage: true,
|
||||||
|
bookmark: expect.any(String),
|
||||||
|
})
|
||||||
|
|
||||||
|
const lastPageResponse = await config.api.viewV2.search(
|
||||||
|
createViewResponse.id,
|
||||||
|
{
|
||||||
|
paginate: true,
|
||||||
|
limit: 4,
|
||||||
|
bookmark: secondPageResponse.body.bookmark,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(lastPageResponse.body).toEqual({
|
||||||
|
rows: expect.arrayContaining(allRows.slice(8)),
|
||||||
|
totalRows: 10,
|
||||||
|
hasNextPage: false,
|
||||||
|
bookmark: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -62,7 +62,7 @@ describe("/v2/views", () => {
|
||||||
name: generator.name(),
|
name: generator.name(),
|
||||||
tableId: config.table!._id!,
|
tableId: config.table!._id!,
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: { allOr: false, equal: { field: "value" } },
|
query: [{ operator: "equal", field: "field", value: "value" }],
|
||||||
sort: {
|
sort: {
|
||||||
field: "fieldToSort",
|
field: "fieldToSort",
|
||||||
order: SortOrder.DESCENDING,
|
order: SortOrder.DESCENDING,
|
||||||
|
@ -190,7 +190,7 @@ describe("/v2/views", () => {
|
||||||
const tableId = config.table!._id!
|
const tableId = config.table!._id!
|
||||||
await config.api.viewV2.update({
|
await config.api.viewV2.update({
|
||||||
...view,
|
...view,
|
||||||
query: { equal: { newField: "thatValue" } },
|
query: [{ operator: "equal", field: "newField", value: "thatValue" }],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(await config.api.table.get(tableId)).toEqual({
|
expect(await config.api.table.get(tableId)).toEqual({
|
||||||
|
@ -198,7 +198,9 @@ describe("/v2/views", () => {
|
||||||
views: {
|
views: {
|
||||||
[view.name]: {
|
[view.name]: {
|
||||||
...view,
|
...view,
|
||||||
query: { equal: { newField: "thatValue" } },
|
query: [
|
||||||
|
{ operator: "equal", field: "newField", value: "thatValue" },
|
||||||
|
],
|
||||||
schema: expect.anything(),
|
schema: expect.anything(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -216,7 +218,13 @@ describe("/v2/views", () => {
|
||||||
tableId,
|
tableId,
|
||||||
name: view.name,
|
name: view.name,
|
||||||
primaryDisplay: generator.word(),
|
primaryDisplay: generator.word(),
|
||||||
query: { equal: { [generator.word()]: generator.word() } },
|
query: [
|
||||||
|
{
|
||||||
|
operator: "equal",
|
||||||
|
field: generator.word(),
|
||||||
|
value: generator.word(),
|
||||||
|
},
|
||||||
|
],
|
||||||
sort: {
|
sort: {
|
||||||
field: generator.word(),
|
field: generator.word(),
|
||||||
order: SortOrder.DESCENDING,
|
order: SortOrder.DESCENDING,
|
||||||
|
@ -285,7 +293,7 @@ describe("/v2/views", () => {
|
||||||
{
|
{
|
||||||
...view,
|
...view,
|
||||||
tableId: generator.guid(),
|
tableId: generator.guid(),
|
||||||
query: { equal: { newField: "thatValue" } },
|
query: [{ operator: "equal", field: "newField", value: "thatValue" }],
|
||||||
},
|
},
|
||||||
{ expectStatus: 404 }
|
{ expectStatus: 404 }
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,23 +1,9 @@
|
||||||
import { SearchFilters, SortOrder, SortType } from "@budibase/types"
|
import { SearchFilters, SearchParams } from "@budibase/types"
|
||||||
import { isExternalTable } from "../../../integrations/utils"
|
import { isExternalTable } from "../../../integrations/utils"
|
||||||
import * as internal from "./search/internal"
|
import * as internal from "./search/internal"
|
||||||
import * as external from "./search/external"
|
import * as external from "./search/external"
|
||||||
import { Format } from "../../../api/controllers/view/exporters"
|
import { Format } from "../../../api/controllers/view/exporters"
|
||||||
|
|
||||||
export interface SearchParams {
|
|
||||||
tableId: string
|
|
||||||
paginate?: boolean
|
|
||||||
query: SearchFilters
|
|
||||||
bookmark?: string
|
|
||||||
limit?: number
|
|
||||||
sort?: string
|
|
||||||
sortOrder?: SortOrder
|
|
||||||
sortType?: SortType
|
|
||||||
version?: string
|
|
||||||
disableEscaping?: boolean
|
|
||||||
fields?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ViewParams {
|
export interface ViewParams {
|
||||||
calculation: string
|
calculation: string
|
||||||
group: string
|
group: string
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
IncludeRelationship,
|
IncludeRelationship,
|
||||||
Row,
|
Row,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
|
SearchParams,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import * as exporters from "../../../../api/controllers/view/exporters"
|
import * as exporters from "../../../../api/controllers/view/exporters"
|
||||||
import sdk from "../../../../sdk"
|
import sdk from "../../../../sdk"
|
||||||
|
@ -13,7 +14,7 @@ import { handleRequest } from "../../../../api/controllers/row/external"
|
||||||
import { breakExternalTableId } from "../../../../integrations/utils"
|
import { breakExternalTableId } from "../../../../integrations/utils"
|
||||||
import { cleanExportRows } from "../utils"
|
import { cleanExportRows } from "../utils"
|
||||||
import { utils } from "@budibase/shared-core"
|
import { utils } from "@budibase/shared-core"
|
||||||
import { ExportRowsParams, ExportRowsResult, SearchParams } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
import { HTTPError, db } from "@budibase/backend-core"
|
import { HTTPError, db } from "@budibase/backend-core"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from "../../../../db/utils"
|
} from "../../../../db/utils"
|
||||||
import { getGlobalUsersFromMetadata } from "../../../../utilities/global"
|
import { getGlobalUsersFromMetadata } from "../../../../utilities/global"
|
||||||
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
import { outputProcessing } from "../../../../utilities/rowProcessor"
|
||||||
import { Database, Row, Table } from "@budibase/types"
|
import { Database, Row, Table, SearchParams } from "@budibase/types"
|
||||||
import { cleanExportRows } from "../utils"
|
import { cleanExportRows } from "../utils"
|
||||||
import {
|
import {
|
||||||
Format,
|
Format,
|
||||||
|
@ -28,7 +28,7 @@ import {
|
||||||
getFromMemoryDoc,
|
getFromMemoryDoc,
|
||||||
} from "../../../../api/controllers/view/utils"
|
} from "../../../../api/controllers/view/utils"
|
||||||
import sdk from "../../../../sdk"
|
import sdk from "../../../../sdk"
|
||||||
import { ExportRowsParams, ExportRowsResult, SearchParams } from "../search"
|
import { ExportRowsParams, ExportRowsResult } from "../search"
|
||||||
import pick from "lodash/pick"
|
import pick from "lodash/pick"
|
||||||
|
|
||||||
export async function search(options: SearchParams) {
|
export async function search(options: SearchParams) {
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
import { GenericContainer } from "testcontainers"
|
import { GenericContainer } from "testcontainers"
|
||||||
|
|
||||||
import { Datasource, FieldType, Row, SourceName, Table } from "@budibase/types"
|
import {
|
||||||
|
Datasource,
|
||||||
|
FieldType,
|
||||||
|
Row,
|
||||||
|
SourceName,
|
||||||
|
Table,
|
||||||
|
SearchParams,
|
||||||
|
} from "@budibase/types"
|
||||||
|
|
||||||
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
|
||||||
import { SearchParams } from "../../search"
|
|
||||||
import { search } from "../external"
|
import { search } from "../external"
|
||||||
import {
|
import {
|
||||||
expectAnyExternalColsAttributes,
|
expectAnyExternalColsAttributes,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { FieldType, Row, Table } from "@budibase/types"
|
import { FieldType, Row, Table, SearchParams } from "@budibase/types"
|
||||||
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
|
import TestConfiguration from "../../../../../tests/utilities/TestConfiguration"
|
||||||
import { SearchParams } from "../../search"
|
|
||||||
import { search } from "../internal"
|
import { search } from "../internal"
|
||||||
import {
|
import {
|
||||||
expectAnyInternalColsAttributes,
|
expectAnyInternalColsAttributes,
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import {
|
import {
|
||||||
CreateViewRequest,
|
CreateViewRequest,
|
||||||
SortOrder,
|
|
||||||
SortType,
|
|
||||||
UpdateViewRequest,
|
UpdateViewRequest,
|
||||||
DeleteRowRequest,
|
DeleteRowRequest,
|
||||||
PatchRowRequest,
|
PatchRowRequest,
|
||||||
PatchRowResponse,
|
PatchRowResponse,
|
||||||
Row,
|
Row,
|
||||||
ViewV2,
|
ViewV2,
|
||||||
|
SearchViewRowRequest,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import TestConfiguration from "../TestConfiguration"
|
import TestConfiguration from "../TestConfiguration"
|
||||||
import { TestAPI } from "./base"
|
import { TestAPI } from "./base"
|
||||||
|
@ -81,31 +80,12 @@ export class ViewV2API extends TestAPI {
|
||||||
|
|
||||||
search = async (
|
search = async (
|
||||||
viewId: string,
|
viewId: string,
|
||||||
options?: {
|
params?: SearchViewRowRequest,
|
||||||
sort: {
|
|
||||||
column: string
|
|
||||||
order?: SortOrder
|
|
||||||
type?: SortType
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ expectStatus } = { expectStatus: 200 }
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
) => {
|
) => {
|
||||||
const qs: [string, any][] = []
|
|
||||||
if (options?.sort.column) {
|
|
||||||
qs.push(["sort_column", options.sort.column])
|
|
||||||
}
|
|
||||||
if (options?.sort.order) {
|
|
||||||
qs.push(["sort_order", options.sort.order])
|
|
||||||
}
|
|
||||||
if (options?.sort.type) {
|
|
||||||
qs.push(["sort_type", options.sort.type])
|
|
||||||
}
|
|
||||||
let url = `/api/v2/views/${viewId}/search`
|
|
||||||
if (qs.length) {
|
|
||||||
url += "?" + qs.map(q => q.join("=")).join("&")
|
|
||||||
}
|
|
||||||
return this.request
|
return this.request
|
||||||
.get(url)
|
.post(`/api/v2/views/${viewId}/search`)
|
||||||
|
.send(params)
|
||||||
.set(this.config.defaultHeaders())
|
.set(this.config.defaultHeaders())
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(expectStatus)
|
.expect(expectStatus)
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { Datasource, FieldType, SortDirection, SortType } from "@budibase/types"
|
import {
|
||||||
|
Datasource,
|
||||||
|
FieldType,
|
||||||
|
SearchFilter,
|
||||||
|
SearchQuery,
|
||||||
|
SearchQueryFields,
|
||||||
|
SortDirection,
|
||||||
|
SortType,
|
||||||
|
} from "@budibase/types"
|
||||||
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
import { OperatorOptions, SqlNumberTypeRangeMap } from "./constants"
|
||||||
import { deepGet } from "./helpers"
|
import { deepGet } from "./helpers"
|
||||||
|
|
||||||
|
@ -73,13 +81,13 @@ export const NoEmptyFilterStrings = [
|
||||||
OperatorOptions.NotEquals.value,
|
OperatorOptions.NotEquals.value,
|
||||||
OperatorOptions.Contains.value,
|
OperatorOptions.Contains.value,
|
||||||
OperatorOptions.NotContains.value,
|
OperatorOptions.NotContains.value,
|
||||||
] as (keyof QueryFields)[]
|
] as (keyof SearchQueryFields)[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes any fields that contain empty strings that would cause inconsistent
|
* Removes any fields that contain empty strings that would cause inconsistent
|
||||||
* behaviour with how backend tables are filtered (no value means no filter).
|
* behaviour with how backend tables are filtered (no value means no filter).
|
||||||
*/
|
*/
|
||||||
const cleanupQuery = (query: Query) => {
|
const cleanupQuery = (query: SearchQuery) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
@ -110,66 +118,12 @@ const removeKeyNumbering = (key: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Filter = {
|
|
||||||
operator: keyof Query
|
|
||||||
field: string
|
|
||||||
type: any
|
|
||||||
value: any
|
|
||||||
externalType: keyof typeof SqlNumberTypeRangeMap
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query = QueryFields & QueryConfig
|
|
||||||
type QueryFields = {
|
|
||||||
string?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
fuzzy?: {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
range?: {
|
|
||||||
[key: string]: {
|
|
||||||
high: number | string
|
|
||||||
low: number | string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
equal?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
notEqual?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
empty?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
notEmpty?: {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
oneOf?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
contains?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
notContains?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
containsAny?: {
|
|
||||||
[key: string]: any[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryConfig = {
|
|
||||||
allOr?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type QueryFieldsType = keyof QueryFields
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a lucene JSON query from the filter structure generated in the builder
|
* Builds a lucene JSON query from the filter structure generated in the builder
|
||||||
* @param filter the builder filter structure
|
* @param filter the builder filter structure
|
||||||
*/
|
*/
|
||||||
export const buildLuceneQuery = (filter: Filter[]) => {
|
export const buildLuceneQuery = (filter: SearchFilter[]) => {
|
||||||
let query: Query = {
|
let query: SearchQuery = {
|
||||||
string: {},
|
string: {},
|
||||||
fuzzy: {},
|
fuzzy: {},
|
||||||
range: {},
|
range: {},
|
||||||
|
@ -227,9 +181,13 @@ export const buildLuceneQuery = (filter: Filter[]) => {
|
||||||
}
|
}
|
||||||
if (operator.startsWith("range") && query.range) {
|
if (operator.startsWith("range") && query.range) {
|
||||||
const minint =
|
const minint =
|
||||||
SqlNumberTypeRangeMap[externalType]?.min || Number.MIN_SAFE_INTEGER
|
SqlNumberTypeRangeMap[
|
||||||
|
externalType as keyof typeof SqlNumberTypeRangeMap
|
||||||
|
]?.min || Number.MIN_SAFE_INTEGER
|
||||||
const maxint =
|
const maxint =
|
||||||
SqlNumberTypeRangeMap[externalType]?.max || Number.MAX_SAFE_INTEGER
|
SqlNumberTypeRangeMap[
|
||||||
|
externalType as keyof typeof SqlNumberTypeRangeMap
|
||||||
|
]?.max || Number.MAX_SAFE_INTEGER
|
||||||
if (!query.range[field]) {
|
if (!query.range[field]) {
|
||||||
query.range[field] = {
|
query.range[field] = {
|
||||||
low: type === "number" ? minint : "0000-00-00T00:00:00.000Z",
|
low: type === "number" ? minint : "0000-00-00T00:00:00.000Z",
|
||||||
|
@ -275,7 +233,7 @@ export const buildLuceneQuery = (filter: Filter[]) => {
|
||||||
* @param docs the data
|
* @param docs the data
|
||||||
* @param query the JSON lucene query
|
* @param query the JSON lucene query
|
||||||
*/
|
*/
|
||||||
export const runLuceneQuery = (docs: any[], query?: Query) => {
|
export const runLuceneQuery = (docs: any[], query?: SearchQuery) => {
|
||||||
if (!docs || !Array.isArray(docs)) {
|
if (!docs || !Array.isArray(docs)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -289,7 +247,7 @@ export const runLuceneQuery = (docs: any[], query?: Query) => {
|
||||||
// Iterates over a set of filters and evaluates a fail function against a doc
|
// Iterates over a set of filters and evaluates a fail function against a doc
|
||||||
const match =
|
const match =
|
||||||
(
|
(
|
||||||
type: QueryFieldsType,
|
type: keyof SearchQueryFields,
|
||||||
failFn: (docValue: any, testValue: any) => boolean
|
failFn: (docValue: any, testValue: any) => boolean
|
||||||
) =>
|
) =>
|
||||||
(doc: any) => {
|
(doc: any) => {
|
||||||
|
@ -456,7 +414,7 @@ export const luceneLimit = (docs: any[], limit: string) => {
|
||||||
return docs.slice(0, numLimit)
|
return docs.slice(0, numLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hasFilters = (query?: Query) => {
|
export const hasFilters = (query?: SearchQuery) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { SearchParams } from "../../../sdk"
|
||||||
import { Row } from "../../../documents"
|
import { Row } from "../../../documents"
|
||||||
|
|
||||||
export interface PatchRowRequest extends Row {
|
export interface PatchRowRequest extends Row {
|
||||||
|
@ -8,6 +9,14 @@ export interface PatchRowRequest extends Row {
|
||||||
|
|
||||||
export interface PatchRowResponse extends Row {}
|
export interface PatchRowResponse extends Row {}
|
||||||
|
|
||||||
export interface SearchResponse {
|
export interface SearchRowRequest extends Omit<SearchParams, "tableId"> {}
|
||||||
|
|
||||||
|
export interface SearchViewRowRequest
|
||||||
|
extends Pick<
|
||||||
|
SearchRowRequest,
|
||||||
|
"sort" | "sortOrder" | "sortType" | "limit" | "bookmark" | "paginate"
|
||||||
|
> {}
|
||||||
|
|
||||||
|
export interface SearchRowResponse {
|
||||||
rows: any[]
|
rows: any[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,3 +8,4 @@ export * from "./system"
|
||||||
export * from "./app"
|
export * from "./app"
|
||||||
export * from "./global"
|
export * from "./global"
|
||||||
export * from "./pagination"
|
export * from "./pagination"
|
||||||
|
export * from "./searchFilter"
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { FieldType } from "../../documents"
|
||||||
|
|
||||||
|
export type SearchFilter = {
|
||||||
|
operator: keyof SearchQuery
|
||||||
|
field: string
|
||||||
|
type?: FieldType
|
||||||
|
value: any
|
||||||
|
externalType?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchQuery = {
|
||||||
|
allOr?: boolean
|
||||||
|
string?: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
fuzzy?: {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
||||||
|
range?: {
|
||||||
|
[key: string]: {
|
||||||
|
high: number | string
|
||||||
|
low: number | string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equal?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
notEqual?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
empty?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
notEmpty?: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
oneOf?: {
|
||||||
|
[key: string]: any[]
|
||||||
|
}
|
||||||
|
contains?: {
|
||||||
|
[key: string]: any[]
|
||||||
|
}
|
||||||
|
notContains?: {
|
||||||
|
[key: string]: any[]
|
||||||
|
}
|
||||||
|
containsAny?: {
|
||||||
|
[key: string]: any[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchQueryFields = Omit<SearchQuery, "allOr">
|
|
@ -1,6 +1,5 @@
|
||||||
import { SortOrder, SortType } from "../../api"
|
import { SearchFilter, SortOrder, SortType } from "../../api"
|
||||||
import { SearchFilters } from "../../sdk"
|
import { UIFieldMetadata } from "./table"
|
||||||
import { TableSchema, UIFieldMetadata } from "./table"
|
|
||||||
|
|
||||||
export interface View {
|
export interface View {
|
||||||
name: string
|
name: string
|
||||||
|
@ -20,7 +19,7 @@ export interface ViewV2 {
|
||||||
name: string
|
name: string
|
||||||
primaryDisplay?: string
|
primaryDisplay?: string
|
||||||
tableId: string
|
tableId: string
|
||||||
query?: SearchFilters
|
query?: SearchFilter[]
|
||||||
sort?: {
|
sort?: {
|
||||||
field: string
|
field: string
|
||||||
order?: SortOrder
|
order?: SortOrder
|
||||||
|
|
|
@ -19,3 +19,4 @@ export * from "./user"
|
||||||
export * from "./cli"
|
export * from "./cli"
|
||||||
export * from "./websocket"
|
export * from "./websocket"
|
||||||
export * from "./permissions"
|
export * from "./permissions"
|
||||||
|
export * from "./row"
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { SortOrder, SortType } from "../api"
|
||||||
|
import { SearchFilters } from "./search"
|
||||||
|
|
||||||
|
export interface SearchParams {
|
||||||
|
tableId: string
|
||||||
|
paginate?: boolean
|
||||||
|
query: SearchFilters
|
||||||
|
bookmark?: string
|
||||||
|
limit?: number
|
||||||
|
sort?: string
|
||||||
|
sortOrder?: SortOrder
|
||||||
|
sortType?: SortType
|
||||||
|
version?: string
|
||||||
|
disableEscaping?: boolean
|
||||||
|
fields?: string[]
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:14-alpine
|
FROM node:18-alpine
|
||||||
|
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
|
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="scripts/watchtower-hooks/pre-check.sh"
|
||||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
|
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="scripts/watchtower-hooks/pre-update.sh"
|
||||||
|
|
|
@ -43,7 +43,6 @@ function runBuild(entry, outfile) {
|
||||||
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
|
TsconfigPathsPlugin({ tsconfig: tsconfigPathPluginContent }),
|
||||||
nodeExternalsPlugin(),
|
nodeExternalsPlugin(),
|
||||||
],
|
],
|
||||||
target: "node14",
|
|
||||||
preserveSymlinks: true,
|
preserveSymlinks: true,
|
||||||
loader: {
|
loader: {
|
||||||
".svelte": "copy",
|
".svelte": "copy",
|
||||||
|
|
Loading…
Reference in New Issue