diff --git a/charts/budibase/README.md b/charts/budibase/README.md index b803da18a4..207992087d 100644 --- a/charts/budibase/README.md +++ b/charts/budibase/README.md @@ -152,6 +152,8 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | services.apps.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the apps service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the apps pods. | | services.apps.extraContainers | list | `[]` | Additional containers to be added to the apps pod. | | services.apps.extraEnv | list | `[]` | Extra environment variables to set for apps pods. Takes a list of name=value pairs. | +| services.apps.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main apps container. | +| services.apps.extraVolumes | list | `[]` | Additional volumes to the apps pod. | | services.apps.httpLogging | int | `1` | Whether or not to log HTTP requests to the apps service. | | services.apps.livenessProbe | object | HTTP health checks. | Liveness probe configuration for apps pods. You shouldn't need to change this, but if you want to you can find more information here: | | services.apps.logLevel | string | `"info"` | The log level for the apps service. | @@ -166,6 +168,8 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | services.automationWorkers.enabled | bool | `true` | Whether or not to enable the automation worker service. If you disable this, automations will be processed by the apps service. | | services.automationWorkers.extraContainers | list | `[]` | Additional containers to be added to the automationWorkers pod. | | services.automationWorkers.extraEnv | list | `[]` | Extra environment variables to set for automation worker pods. Takes a list of name=value pairs. | +| services.automationWorkers.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main automationWorkers container. | +| services.automationWorkers.extraVolumes | list | `[]` | Additional volumes to the automationWorkers pod. | | services.automationWorkers.livenessProbe | object | HTTP health checks. | Liveness probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: | | services.automationWorkers.logLevel | string | `"info"` | The log level for the automation worker service. | | services.automationWorkers.readinessProbe | object | HTTP health checks. | Readiness probe configuration for automation worker pods. You shouldn't need to change this, but if you want to you can find more information here: | @@ -185,6 +189,8 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | services.objectStore.cloudfront.privateKey64 | string | `""` | Base64 encoded private key for the above public key. | | services.objectStore.cloudfront.publicKeyId | string | `""` | ID of public key stored in cloudfront. | | services.objectStore.extraContainers | list | `[]` | Additional containers to be added to the objectStore pod. | +| services.objectStore.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main objectStore container. | +| services.objectStore.extraVolumes | list | `[]` | Additional volumes to the objectStore pod. | | services.objectStore.minio | bool | `true` | Set to false if using another object store, such as S3. You will need to set `services.objectStore.url` to point to your bucket if you do this. | | services.objectStore.region | string | `""` | AWS_REGION if using S3 | | services.objectStore.resources | object | `{}` | The resources to use for Minio pods. See for more information on how to set these. | @@ -197,6 +203,8 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | services.proxy.autoscaling.minReplicas | int | `1` | | | services.proxy.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the proxy service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the proxy pods. | | services.proxy.extraContainers | list | `[]` | | +| services.proxy.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main proxy container. | +| services.proxy.extraVolumes | list | `[]` | Additional volumes to the proxy pod. | | services.proxy.livenessProbe | object | HTTP health checks. | Liveness probe configuration for proxy pods. You shouldn't need to change this, but if you want to you can find more information here: | | services.proxy.readinessProbe | object | HTTP health checks. | Readiness probe configuration for proxy pods. You shouldn't need to change this, but if you want to you can find more information here: | | services.proxy.replicaCount | int | `1` | The number of proxy replicas to run. | @@ -204,6 +212,9 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | services.proxy.startupProbe | object | HTTP health checks. | Startup probe configuration for proxy pods. You shouldn't need to change this, but if you want to you can find more information here: | | services.redis.enabled | bool | `true` | Whether or not to deploy a Redis pod into your cluster. | | services.redis.extraContainers | list | `[]` | Additional containers to be added to the redis pod. | +| services.redis.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main redis container. | +| services.redis.extraVolumes | list | `[]` | Additional volumes to the redis pod. | +| services.redis.image | string | `"redis"` | The Redis image to use. | | services.redis.password | string | `"budibase"` | The password to use when connecting to Redis. It's recommended that you change this from the default if you're running Redis in-cluster. | | services.redis.port | int | `6379` | Port to expose Redis on. | | services.redis.resources | object | `{}` | The resources to use for Redis pods. See for more information on how to set these. | @@ -216,6 +227,8 @@ $ helm install --create-namespace --namespace budibase budibase . -f values.yaml | services.worker.autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization percentage for the worker service. Note that for autoscaling to work, you will need to have metrics-server configured, and resources set for the worker pods. | | services.worker.extraContainers | list | `[]` | Additional containers to be added to the worker pod. | | services.worker.extraEnv | list | `[]` | Extra environment variables to set for worker pods. Takes a list of name=value pairs. | +| services.worker.extraVolumeMounts | list | `[]` | Additional volumeMounts to the main worker container. | +| services.worker.extraVolumes | list | `[]` | Additional volumes to the worker pod. | | services.worker.httpLogging | int | `1` | Whether or not to log HTTP requests to the worker service. | | services.worker.livenessProbe | object | HTTP health checks. | Liveness probe configuration for worker pods. You shouldn't need to change this, but if you want to you can find more information here: | | services.worker.logLevel | string | `"info"` | The log level for the worker service. | diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 2fd8506e30..b380908dd1 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -235,6 +235,10 @@ spec: args: {{- toYaml .Values.services.apps.args | nindent 10 }} {{ end }} + {{ if .Values.services.apps.extraVolumeMounts }} + volumeMounts: + {{- toYaml .Values.services.apps.extraVolumeMounts | nindent 10 }} + {{- end }} {{- if .Values.services.apps.extraContainers }} {{- toYaml .Values.services.apps.extraContainers | nindent 6 }} {{- end }} @@ -261,4 +265,8 @@ spec: - name: ndots value: {{ .Values.services.apps.ndots | quote }} {{ end }} + {{ if .Values.services.apps.extraVolumes }} + volumes: + {{- toYaml .Values.services.apps.extraVolumes | nindent 6 }} + {{- end }} status: {} diff --git a/charts/budibase/templates/automation-worker-service-deployment.yaml b/charts/budibase/templates/automation-worker-service-deployment.yaml index 53d5fcc860..51fa9ee4bb 100644 --- a/charts/budibase/templates/automation-worker-service-deployment.yaml +++ b/charts/budibase/templates/automation-worker-service-deployment.yaml @@ -235,6 +235,10 @@ spec: args: {{- toYaml .Values.services.automationWorkers.args | nindent 10 }} {{ end }} + {{ if .Values.services.automationWorkers.extraVolumeMounts }} + volumeMounts: + {{- toYaml .Values.services.automationWorkers.extraVolumeMounts | nindent 10 }} + {{ end }} {{- if .Values.services.automationWorkers.extraContainers }} {{- toYaml .Values.services.automationWorkers.extraContainers | nindent 6 }} {{- end }} @@ -261,5 +265,9 @@ spec: - name: ndots value: {{ .Values.services.automationWorkers.ndots | quote }} {{ end }} + {{ if .Values.services.automationWorkers.extraVolumes }} + volumes: + {{- toYaml .Values.services.automationWorkers.extraVolumes | nindent 8 }} + {{ end }} status: {} {{- end }} \ No newline at end of file diff --git a/charts/budibase/templates/minio-service-deployment.yaml b/charts/budibase/templates/minio-service-deployment.yaml index ade1d37cd2..901ead2b46 100644 --- a/charts/budibase/templates/minio-service-deployment.yaml +++ b/charts/budibase/templates/minio-service-deployment.yaml @@ -54,6 +54,9 @@ spec: volumeMounts: - mountPath: /data name: minio-data + {{ if .Values.services.objectStore.extraVolumeMounts }} + {{- toYaml .Values.services.objectStore.extraVolumeMounts | nindent 8 }} + {{- end }} {{- if .Values.services.objectStore.extraContainers }} {{- toYaml .Values.services.objectStore.extraContainers | nindent 6 }} {{- end }} @@ -78,5 +81,8 @@ spec: - name: minio-data persistentVolumeClaim: claimName: minio-data + {{ if .Values.services.objectStore.extraVolumes }} + {{- toYaml .Values.services.objectStore.extraVolumes | nindent 6 }} + {{- end }} status: {} {{- end }} diff --git a/charts/budibase/templates/proxy-service-deployment.yaml b/charts/budibase/templates/proxy-service-deployment.yaml index 462c6a0749..d5ea696431 100644 --- a/charts/budibase/templates/proxy-service-deployment.yaml +++ b/charts/budibase/templates/proxy-service-deployment.yaml @@ -82,6 +82,10 @@ spec: resources: {{- toYaml . | nindent 10 }} {{ end }} + {{ if .Values.services.proxy.extraVolumeMounts }} + volumeMounts: + {{- toYaml .Values.services.proxy.extraVolumeMounts | nindent 8 }} + {{- end }} {{- if .Values.services.proxy.extraContainers }} {{- toYaml .Values.services.proxy.extraContainers | nindent 6 }} {{- end }} @@ -110,7 +114,10 @@ spec: args: {{- toYaml .Values.services.proxy.args | nindent 8 }} {{ end }} + {{ if .Values.services.proxy.extraVolumes }} volumes: + {{- toYaml .Values.services.proxy.extraVolumes | nindent 6 }} + {{ end }} {{ if .Values.services.proxy.ndots }} dnsConfig: options: diff --git a/charts/budibase/templates/redis-service-deployment.yaml b/charts/budibase/templates/redis-service-deployment.yaml index 1a003d3814..9ad12e0167 100644 --- a/charts/budibase/templates/redis-service-deployment.yaml +++ b/charts/budibase/templates/redis-service-deployment.yaml @@ -22,7 +22,7 @@ spec: - redis-server - --requirepass - {{ .Values.services.redis.password }} - image: redis + image: {{ .Values.services.redis.image }} imagePullPolicy: "" name: redis-service ports: @@ -34,6 +34,9 @@ spec: volumeMounts: - mountPath: /data name: redis-data + {{ if .Values.services.redis.extraVolumeMounts }} + {{- toYaml .Values.services.redis.extraVolumeMounts | nindent 8 }} + {{- end }} {{- if .Values.services.redis.extraContainers }} {{- toYaml .Values.services.redis.extraContainers | nindent 6 }} {{- end }} @@ -58,6 +61,9 @@ spec: - name: redis-data persistentVolumeClaim: claimName: redis-data + {{ if .Values.services.redis.extraVolumes }} + {{- toYaml .Values.services.redis.extraVolumes | nindent 6 }} + {{- end }} status: {} {{- end }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index cc27bf429e..e37b2bc0e4 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -221,6 +221,10 @@ spec: args: {{- toYaml .Values.services.worker.args | nindent 10 }} {{ end }} + {{ if .Values.services.worker.extraVolumeMounts }} + volumeMounts: + {{- toYaml .Values.services.worker.extraVolumeMounts | nindent 10 }} + {{- end }} {{- if .Values.services.worker.extraContainers }} {{- toYaml .Values.services.worker.extraContainers | nindent 6 }} {{- end }} @@ -247,4 +251,8 @@ spec: - name: ndots value: {{ .Values.services.worker.ndots | quote }} {{ end }} + {{ if .Values.services.worker.extraVolumes }} + volumes: + {{- toYaml .Values.services.worker.extraVolumes | nindent 6 }} + {{- end }} status: {} diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index dfbbca6cad..9ace768625 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -211,6 +211,16 @@ services: # - name: my-sidecar # image: myimage:latest + # -- Additional volumeMounts to the main proxy container. + extraVolumeMounts: [] + # - name: my-volume + # mountPath: /path/to/mount + + # -- Additional volumes to the proxy pod. + extraVolumes: [] + # - name: my-volume + # emptyDir: {} + apps: # @ignore (you shouldn't need to change this) port: 4002 @@ -283,6 +293,16 @@ services: # - name: my-sidecar # image: myimage:latest + # -- Additional volumeMounts to the main apps container. + extraVolumeMounts: [] + # - name: my-volume + # mountPath: /path/to/mount + + # -- Additional volumes to the apps pod. + extraVolumes: [] + # - name: my-volume + # emptyDir: {} + automationWorkers: # -- Whether or not to enable the automation worker service. If you disable this, # automations will be processed by the apps service. @@ -359,6 +379,16 @@ services: # - name: my-sidecar # image: myimage:latest + # -- Additional volumeMounts to the main automationWorkers container. + extraVolumeMounts: [] + # - name: my-volume + # mountPath: /path/to/mount + + # -- Additional volumes to the automationWorkers pod. + extraVolumes: [] + # - name: my-volume + # emptyDir: {} + worker: # @ignore (you shouldn't need to change this) port: 4003 @@ -431,6 +461,16 @@ services: # - name: my-sidecar # image: myimage:latest + # -- Additional volumeMounts to the main worker container. + extraVolumeMounts: [] + # - name: my-volume + # mountPath: /path/to/mount + + # -- Additional volumes to the worker pod. + extraVolumes: [] + # - name: my-volume + # emptyDir: {} + couchdb: # -- Whether or not to spin up a CouchDB instance in your cluster. True by # default, and the configuration for the CouchDB instance is under the @@ -456,6 +496,8 @@ services: resources: {} redis: + # -- The Redis image to use. + image: redis # -- Whether or not to deploy a Redis pod into your cluster. enabled: true # -- Port to expose Redis on. @@ -484,6 +526,16 @@ services: # - name: my-sidecar # image: myimage:latest + # -- Additional volumeMounts to the main redis container. + extraVolumeMounts: [] + # - name: my-volume + # mountPath: /path/to/mount + + # -- Additional volumes to the redis pod. + extraVolumes: [] + # - name: my-volume + # emptyDir: {} + objectStore: # -- Set to false if using another object store, such as S3. You will need # to set `services.objectStore.url` to point to your bucket if you do this. @@ -530,6 +582,16 @@ services: # - name: my-sidecar # image: myimage:latest + # -- Additional volumeMounts to the main objectStore container. + extraVolumeMounts: [] + # - name: my-volume + # mountPath: /path/to/mount + + # -- Additional volumes to the objectStore pod. + extraVolumes: [] + # - name: my-volume + # emptyDir: {} + # Override values in couchDB subchart. We're only specifying the values we're changing. # If you want to see all of the available values, see: # https://github.com/apache/couchdb-helm/tree/couchdb-4.3.0/couchdb diff --git a/lerna.json b/lerna.json index 78a3aa13e9..9839b8b166 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.23.4", + "version": "2.23.5", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/package.json b/package.json index 2816247939..e520b7c2cf 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up --ignore @budibase/account-portal-server && lerna run --stream dev --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker --ignore=@budibase/account-portal-ui --ignore @budibase/account-portal-server", "dev:server": "yarn run kill-server && lerna run --stream dev --scope @budibase/worker --scope @budibase/server", "dev:accountportal": "yarn kill-accountportal && lerna run dev --stream --scope @budibase/account-portal-ui --scope @budibase/account-portal-server", + "dev:camunda": "./scripts/deploy-camunda.sh", "dev:all": "yarn run kill-all && lerna run --stream dev", "dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built", "dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0", diff --git a/packages/account-portal b/packages/account-portal index a0ee9cad8c..bd0e01d639 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit a0ee9cad8cefb8f9f40228705711be174f018fa9 +Subproject commit bd0e01d639ec3b2547e7c859a1c43b622dce8344 diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index ceef421fab..6acdfcd465 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -320,6 +320,7 @@ async function performAppCreate(ctx: UserCtx) { "theme", "customTheme", "icon", + "snippets", ] keys.forEach(key => { if (existing[key]) { diff --git a/packages/server/src/api/routes/tests/search.spec.ts b/packages/server/src/api/routes/tests/search.spec.ts index fdf1ed7603..5b71ec9044 100644 --- a/packages/server/src/api/routes/tests/search.spec.ts +++ b/packages/server/src/api/routes/tests/search.spec.ts @@ -8,6 +8,8 @@ import { FieldType, RowSearchParams, SearchFilters, + SortOrder, + SortType, Table, TableSchema, } from "@budibase/types" @@ -62,7 +64,32 @@ describe.each([ class SearchAssertion { constructor(private readonly query: RowSearchParams) {} - async toFind(expectedRows: any[]) { + // Asserts that the query returns rows matching exactly the set of rows + // passed in. The order of the rows matters. Rows returned in an order + // different to the one passed in will cause the assertion to fail. Extra + // rows returned by the query will also cause the assertion to fail. + async toMatchExactly(expectedRows: any[]) { + const { rows: foundRows } = await config.api.row.search(table._id!, { + ...this.query, + tableId: table._id!, + }) + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(expectedRows.length) + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toEqual( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) + ) + ) + ) + } + + // Asserts that the query returns rows matching exactly the set of rows + // passed in. The order of the rows is not important, but extra rows will + // cause the assertion to fail. + async toContainExactly(expectedRows: any[]) { const { rows: foundRows } = await config.api.row.search(table._id!, { ...this.query, tableId: table._id!, @@ -82,8 +109,39 @@ describe.each([ ) } + // Asserts that the query returns rows matching the set of rows passed in. + // The order of the rows is not important. Extra rows will not cause the + // assertion to fail. + async toContain(expectedRows: any[]) { + const { rows: foundRows } = await config.api.row.search(table._id!, { + ...this.query, + tableId: table._id!, + }) + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toEqual( + expect.arrayContaining( + expectedRows.map((expectedRow: any) => + expect.objectContaining( + foundRows.find(foundRow => _.isMatch(foundRow, expectedRow)) + ) + ) + ) + ) + } + async toFindNothing() { - await this.toFind([]) + await this.toContainExactly([]) + } + + async toHaveLength(length: number) { + const { rows: foundRows } = await config.api.row.search(table._id!, { + ...this.query, + tableId: table._id!, + }) + + // eslint-disable-next-line jest/no-standalone-expect + expect(foundRows).toHaveLength(length) } } @@ -105,28 +163,33 @@ describe.each([ describe("misc", () => { it("should return all if no query is passed", () => - expectSearch({} as RowSearchParams).toFind([ + expectSearch({} as RowSearchParams).toContainExactly([ { name: "foo" }, { name: "bar" }, ])) it("should return all if empty query is passed", () => - expectQuery({}).toFind([{ name: "foo" }, { name: "bar" }])) + expectQuery({}).toContainExactly([{ name: "foo" }, { name: "bar" }])) it("should return all if onEmptyFilter is RETURN_ALL", () => expectQuery({ onEmptyFilter: EmptyFilterOption.RETURN_ALL, - }).toFind([{ name: "foo" }, { name: "bar" }])) + }).toContainExactly([{ name: "foo" }, { name: "bar" }])) it("should return nothing if onEmptyFilter is RETURN_NONE", () => expectQuery({ onEmptyFilter: EmptyFilterOption.RETURN_NONE, }).toFindNothing()) + + it("should respect limit", () => + expectSearch({ limit: 1, paginate: true, query: {} }).toHaveLength(1)) }) describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { name: "foo" } }).toFind([{ name: "foo" }])) + expectQuery({ equal: { name: "foo" } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ equal: { name: "none" } }).toFindNothing()) @@ -134,15 +197,21 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { name: "foo" } }).toFind([{ name: "bar" }])) + expectQuery({ notEqual: { name: "foo" } }).toContainExactly([ + { name: "bar" }, + ])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { name: "bar" } }).toFind([{ name: "foo" }])) + expectQuery({ notEqual: { name: "bar" } }).toContainExactly([ + { name: "foo" }, + ])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { name: ["foo"] } }).toFind([{ name: "foo" }])) + expectQuery({ oneOf: { name: ["foo"] } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { name: ["none"] } }).toFindNothing()) @@ -150,11 +219,69 @@ describe.each([ describe("fuzzy", () => { it("successfully finds a row", () => - expectQuery({ fuzzy: { name: "oo" } }).toFind([{ name: "foo" }])) + expectQuery({ fuzzy: { name: "oo" } }).toContainExactly([ + { name: "foo" }, + ])) it("fails to find nonexistent row", () => expectQuery({ fuzzy: { name: "none" } }).toFindNothing()) }) + + describe("range", () => { + it("successfully finds multiple rows", () => + expectQuery({ + range: { name: { low: "a", high: "z" } }, + }).toContainExactly([{ name: "bar" }, { name: "foo" }])) + + it("successfully finds a row with a high bound", () => + expectQuery({ + range: { name: { low: "a", high: "c" } }, + }).toContainExactly([{ name: "bar" }])) + + it("successfully finds a row with a low bound", () => + expectQuery({ + range: { name: { low: "f", high: "z" } }, + }).toContainExactly([{ name: "foo" }])) + + it("successfully finds no rows", () => + expectQuery({ + range: { name: { low: "g", high: "h" } }, + }).toFindNothing()) + }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "name", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) + + describe("sortType STRING", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "name", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ name: "bar" }, { name: "foo" }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "name", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ name: "foo" }, { name: "bar" }])) + }) + }) }) describe("numbers", () => { @@ -167,7 +294,7 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { age: 1 } }).toFind([{ age: 1 }])) + expectQuery({ equal: { age: 1 } }).toContainExactly([{ age: 1 }])) it("fails to find nonexistent row", () => expectQuery({ equal: { age: 2 } }).toFindNothing()) @@ -175,15 +302,15 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { age: 1 } }).toFind([{ age: 10 }])) + expectQuery({ notEqual: { age: 1 } }).toContainExactly([{ age: 10 }])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { age: 10 } }).toFind([{ age: 1 }])) + expectQuery({ notEqual: { age: 10 } }).toContainExactly([{ age: 1 }])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { age: [1] } }).toFind([{ age: 1 }])) + expectQuery({ oneOf: { age: [1] } }).toContainExactly([{ age: 1 }])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { age: [2] } }).toFindNothing()) @@ -193,17 +320,56 @@ describe.each([ it("successfully finds a row", () => expectQuery({ range: { age: { low: 1, high: 5 } }, - }).toFind([{ age: 1 }])) + }).toContainExactly([{ age: 1 }])) it("successfully finds multiple rows", () => expectQuery({ range: { age: { low: 1, high: 10 } }, - }).toFind([{ age: 1 }, { age: 10 }])) + }).toContainExactly([{ age: 1 }, { age: 10 }])) it("successfully finds a row with a high bound", () => expectQuery({ range: { age: { low: 5, high: 10 } }, - }).toFind([{ age: 10 }])) + }).toContainExactly([{ age: 10 }])) + + it("successfully finds no rows", () => + expectQuery({ + range: { age: { low: 5, high: 9 } }, + }).toFindNothing()) + }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "age", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { age: 10 }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "age", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }])) + }) + + describe("sortType NUMBER", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ age: 1 }, { age: 10 }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "age", + sortType: SortType.NUMBER, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ age: 10 }, { age: 1 }])) }) }) @@ -211,6 +377,7 @@ describe.each([ const JAN_1ST = "2020-01-01T00:00:00.000Z" const JAN_2ND = "2020-01-02T00:00:00.000Z" const JAN_5TH = "2020-01-05T00:00:00.000Z" + const JAN_9TH = "2020-01-09T00:00:00.000Z" const JAN_10TH = "2020-01-10T00:00:00.000Z" beforeAll(async () => { @@ -223,7 +390,9 @@ describe.each([ describe("equal", () => { it("successfully finds a row", () => - expectQuery({ equal: { dob: JAN_1ST } }).toFind([{ dob: JAN_1ST }])) + expectQuery({ equal: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_1ST }, + ])) it("fails to find nonexistent row", () => expectQuery({ equal: { dob: JAN_2ND } }).toFindNothing()) @@ -231,15 +400,21 @@ describe.each([ describe("notEqual", () => { it("successfully finds a row", () => - expectQuery({ notEqual: { dob: JAN_1ST } }).toFind([{ dob: JAN_10TH }])) + expectQuery({ notEqual: { dob: JAN_1ST } }).toContainExactly([ + { dob: JAN_10TH }, + ])) it("fails to find nonexistent row", () => - expectQuery({ notEqual: { dob: JAN_10TH } }).toFind([{ dob: JAN_1ST }])) + expectQuery({ notEqual: { dob: JAN_10TH } }).toContainExactly([ + { dob: JAN_1ST }, + ])) }) describe("oneOf", () => { it("successfully finds a row", () => - expectQuery({ oneOf: { dob: [JAN_1ST] } }).toFind([{ dob: JAN_1ST }])) + expectQuery({ oneOf: { dob: [JAN_1ST] } }).toContainExactly([ + { dob: JAN_1ST }, + ])) it("fails to find nonexistent row", () => expectQuery({ oneOf: { dob: [JAN_2ND] } }).toFindNothing()) @@ -249,17 +424,130 @@ describe.each([ it("successfully finds a row", () => expectQuery({ range: { dob: { low: JAN_1ST, high: JAN_5TH } }, - }).toFind([{ dob: JAN_1ST }])) + }).toContainExactly([{ dob: JAN_1ST }])) it("successfully finds multiple rows", () => expectQuery({ range: { dob: { low: JAN_1ST, high: JAN_10TH } }, - }).toFind([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + }).toContainExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) it("successfully finds a row with a high bound", () => expectQuery({ range: { dob: { low: JAN_5TH, high: JAN_10TH } }, - }).toFind([{ dob: JAN_10TH }])) + }).toContainExactly([{ dob: JAN_10TH }])) + + it("successfully finds no rows", () => + expectQuery({ + range: { dob: { low: JAN_5TH, high: JAN_9TH } }, + }).toFindNothing()) + }) + + describe("sort", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "dob", + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "dob", + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])) + + describe("sortType STRING", () => { + it("sorts ascending", () => + expectSearch({ + query: {}, + sort: "dob", + sortType: SortType.STRING, + sortOrder: SortOrder.ASCENDING, + }).toMatchExactly([{ dob: JAN_1ST }, { dob: JAN_10TH }])) + + it("sorts descending", () => + expectSearch({ + query: {}, + sort: "dob", + sortType: SortType.STRING, + sortOrder: SortOrder.DESCENDING, + }).toMatchExactly([{ dob: JAN_10TH }, { dob: JAN_1ST }])) + }) + }) + }) + + describe("array of strings", () => { + beforeAll(async () => { + await createTable({ + numbers: { + name: "numbers", + type: FieldType.ARRAY, + constraints: { inclusion: ["one", "two", "three"] }, + }, + }) + await createRows([{ numbers: ["one", "two"] }, { numbers: ["three"] }]) + }) + + describe("contains", () => { + it("successfully finds a row", () => + expectQuery({ contains: { numbers: ["one"] } }).toContainExactly([ + { numbers: ["one", "two"] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ contains: { numbers: ["none"] } }).toFindNothing()) + + it("fails to find row containing all", () => + expectQuery({ + contains: { numbers: ["one", "two", "three"] }, + }).toFindNothing()) + + it("finds all with empty list", () => + expectQuery({ contains: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + }) + + describe("notContains", () => { + it("successfully finds a row", () => + expectQuery({ notContains: { numbers: ["one"] } }).toContainExactly([ + { numbers: ["three"] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ + notContains: { numbers: ["one", "two", "three"] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + + it("finds all with empty list", () => + expectQuery({ notContains: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + }) + + describe("containsAny", () => { + it("successfully finds rows", () => + expectQuery({ + containsAny: { numbers: ["one", "two", "three"] }, + }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) + + it("fails to find nonexistent row", () => + expectQuery({ containsAny: { numbers: ["none"] } }).toFindNothing()) + + it("finds all with empty list", () => + expectQuery({ containsAny: { numbers: [] } }).toContainExactly([ + { numbers: ["one", "two"] }, + { numbers: ["three"] }, + ])) }) }) }) diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts index 42a1b53224..37c275c8a3 100644 --- a/packages/server/src/constants/index.ts +++ b/packages/server/src/constants/index.ts @@ -20,6 +20,7 @@ export enum FilterTypes { NOT_EMPTY = "notEmpty", CONTAINS = "contains", NOT_CONTAINS = "notContains", + CONTAINS_ANY = "containsAny", ONE_OF = "oneOf", } @@ -30,6 +31,7 @@ export const NoEmptyFilterStrings = [ FilterTypes.NOT_EQUAL, FilterTypes.CONTAINS, FilterTypes.NOT_CONTAINS, + FilterTypes.CONTAINS_ANY, ] export const CanSwitchTypes = [ diff --git a/packages/server/src/integrations/base/sql.ts b/packages/server/src/integrations/base/sql.ts index f5828f9419..259abec106 100644 --- a/packages/server/src/integrations/base/sql.ts +++ b/packages/server/src/integrations/base/sql.ts @@ -233,6 +233,11 @@ class InternalBuilder { (statement ? andOr : "") + `LOWER(${likeKey(this.client, key)}) LIKE ?` } + + if (statement === "") { + return + } + // @ts-ignore query = query[rawFnc](`${not}(${statement})`, value) }) diff --git a/packages/server/src/sdk/app/rows/search.ts b/packages/server/src/sdk/app/rows/search.ts index f681bfeb90..5a016c821f 100644 --- a/packages/server/src/sdk/app/rows/search.ts +++ b/packages/server/src/sdk/app/rows/search.ts @@ -29,6 +29,10 @@ function pickApi(tableId: any) { return internal } +function isEmptyArray(value: any) { + return Array.isArray(value) && value.length === 0 +} + // don't do a pure falsy check, as 0 is included // https://github.com/Budibase/budibase/issues/10118 export function removeEmptyFilters(filters: SearchFilters) { @@ -47,7 +51,7 @@ export function removeEmptyFilters(filters: SearchFilters) { for (let [key, value] of Object.entries( filters[filterType] as object )) { - if (value == null || value === "") { + if (value == null || value === "" || isEmptyArray(value)) { // @ts-ignore delete filters[filterField][key] } diff --git a/packages/server/src/sdk/app/rows/search/sqs.ts b/packages/server/src/sdk/app/rows/search/sqs.ts index 5b0b6e3bc7..7abd7d9e72 100644 --- a/packages/server/src/sdk/app/rows/search/sqs.ts +++ b/packages/server/src/sdk/app/rows/search/sqs.ts @@ -132,7 +132,7 @@ export async function search( type: "row", } - if (params.sort && !params.sortType) { + if (params.sort) { const sortField = table.schema[params.sort] const sortType = sortField.type === FieldType.NUMBER ? SortType.NUMBER : SortType.STRING diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index 2f74b9e7b3..239d845722 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -102,6 +102,7 @@ export function isVerifiableSSOProvider(provider: AccountSSOProvider): boolean { } export interface AccountSSO { + ssoId?: string provider: AccountSSOProvider providerType: AccountSSOProviderType oauth2?: OAuthTokens diff --git a/packages/types/src/documents/app/row.ts b/packages/types/src/documents/app/row.ts index 222c346591..865ab4ba64 100644 --- a/packages/types/src/documents/app/row.ts +++ b/packages/types/src/documents/app/row.ts @@ -1,22 +1,111 @@ import { Document } from "../document" export enum FieldType { + /** + * a primitive type, stores a string, called Text within Budibase. This is one of the default + * types of Budibase, if an external type is not fully understood, we will treat it as text. + */ STRING = "string", + /** + * similar to string type, called Long Form Text within Budibase. This is mainly a frontend + * orientated type which enables a larger text input area. This can also be used + * in conjunction with the 'useRichText' option to support a markdown editor/viewer. + */ LONGFORM = "longform", + /** + * similar to string type, called Options within Budibase. This works very similarly to + * the string type within the backend, but is validated to a list of options. This will + * display a