diff --git a/lerna.json b/lerna.json index a1006702cc..dc238bb392 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "3.2.11", + "version": "3.2.12", "npmClient": "yarn", "concurrency": 20, "command": { diff --git a/package.json b/package.json index 860447fc57..e354f36d2a 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,12 @@ "prettier": "2.8.8", "prettier-plugin-svelte": "^2.3.0", "proper-lockfile": "^4.1.2", - "svelte": "^4.2.10", + "svelte": "4.2.19", "svelte-eslint-parser": "^0.33.1", "typescript": "5.5.2", "typescript-eslint": "^7.3.1", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "cross-spawn": "7.0.6" }, "scripts": { "get-past-client-version": "node scripts/getPastClientVersion.js", diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 8f91d1e55d..a4381b4200 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -40,7 +40,7 @@ "bcryptjs": "2.4.3", "bull": "4.10.1", "correlation-id": "4.0.0", - "dd-trace": "5.23.0", + "dd-trace": "5.26.0", "dotenv": "16.0.1", "google-auth-library": "^8.0.1", "google-spreadsheet": "npm:@budibase/google-spreadsheet@4.1.5", diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 83b9b69d0b..371f3dc997 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -190,7 +190,7 @@ export class DatabaseImpl implements Database { } } - private async performCall(call: DBCallback): Promise { + private async performCall(call: DBCallback): Promise { const db = this.getDb() const fnc = await call(db) try { @@ -467,7 +467,7 @@ export class DatabaseImpl implements Database { } catch (err: any) { // didn't exist, don't worry if (err.statusCode === 404) { - return + return { ok: true } } else { throw new CouchDBError(err.message, err) } diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index e08bfc0362..0c0056d6ed 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -27,7 +27,7 @@ export class DDInstrumentedDatabase implements Database { exists(docId?: string): Promise { return tracer.trace("db.exists", span => { - span?.addTags({ db_name: this.name, doc_id: docId }) + span.addTags({ db_name: this.name, doc_id: docId }) if (docId) { return this.db.exists(docId) } @@ -37,15 +37,17 @@ export class DDInstrumentedDatabase implements Database { get(id?: string | undefined): Promise { return tracer.trace("db.get", span => { - span?.addTags({ db_name: this.name, doc_id: id }) + span.addTags({ db_name: this.name, doc_id: id }) return this.db.get(id) }) } tryGet(id?: string | undefined): Promise { - return tracer.trace("db.tryGet", span => { - span?.addTags({ db_name: this.name, doc_id: id }) - return this.db.tryGet(id) + return tracer.trace("db.tryGet", async span => { + span.addTags({ db_name: this.name, doc_id: id }) + const doc = await this.db.tryGet(id) + span.addTags({ doc_found: doc !== undefined }) + return doc }) } @@ -53,13 +55,15 @@ export class DDInstrumentedDatabase implements Database { ids: string[], opts?: { allowMissing?: boolean | undefined } | undefined ): Promise { - return tracer.trace("db.getMultiple", span => { - span?.addTags({ + return tracer.trace("db.getMultiple", async span => { + span.addTags({ db_name: this.name, num_docs: ids.length, allow_missing: opts?.allowMissing, }) - return this.db.getMultiple(ids, opts) + const docs = await this.db.getMultiple(ids, opts) + span.addTags({ num_docs_found: docs.length }) + return docs }) } @@ -69,12 +73,14 @@ export class DDInstrumentedDatabase implements Database { idOrDoc: string | Document, rev?: string ): Promise { - return tracer.trace("db.remove", span => { - span?.addTags({ db_name: this.name, doc_id: idOrDoc }) + return tracer.trace("db.remove", async span => { + span.addTags({ db_name: this.name, doc_id: idOrDoc, rev }) const isDocument = typeof idOrDoc === "object" const id = isDocument ? idOrDoc._id! : idOrDoc rev = isDocument ? idOrDoc._rev : rev - return this.db.remove(id, rev) + const resp = await this.db.remove(id, rev) + span.addTags({ ok: resp.ok }) + return resp }) } @@ -83,7 +89,11 @@ export class DDInstrumentedDatabase implements Database { opts?: { silenceErrors?: boolean } ): Promise { return tracer.trace("db.bulkRemove", span => { - span?.addTags({ db_name: this.name, num_docs: documents.length }) + span.addTags({ + db_name: this.name, + num_docs: documents.length, + silence_errors: opts?.silenceErrors, + }) return this.db.bulkRemove(documents, opts) }) } @@ -92,15 +102,21 @@ export class DDInstrumentedDatabase implements Database { document: AnyDocument, opts?: DatabasePutOpts | undefined ): Promise { - return tracer.trace("db.put", span => { - span?.addTags({ db_name: this.name, doc_id: document._id }) - return this.db.put(document, opts) + return tracer.trace("db.put", async span => { + span.addTags({ + db_name: this.name, + doc_id: document._id, + force: opts?.force, + }) + const resp = await this.db.put(document, opts) + span.addTags({ ok: resp.ok }) + return resp }) } bulkDocs(documents: AnyDocument[]): Promise { return tracer.trace("db.bulkDocs", span => { - span?.addTags({ db_name: this.name, num_docs: documents.length }) + span.addTags({ db_name: this.name, num_docs: documents.length }) return this.db.bulkDocs(documents) }) } @@ -108,9 +124,15 @@ export class DDInstrumentedDatabase implements Database { allDocs( params: DatabaseQueryOpts ): Promise> { - return tracer.trace("db.allDocs", span => { - span?.addTags({ db_name: this.name }) - return this.db.allDocs(params) + return tracer.trace("db.allDocs", async span => { + span.addTags({ db_name: this.name, ...params }) + const resp = await this.db.allDocs(params) + span.addTags({ + total_rows: resp.total_rows, + rows_length: resp.rows.length, + offset: resp.offset, + }) + return resp }) } @@ -118,57 +140,75 @@ export class DDInstrumentedDatabase implements Database { viewName: string, params: DatabaseQueryOpts ): Promise> { - return tracer.trace("db.query", span => { - span?.addTags({ db_name: this.name, view_name: viewName }) - return this.db.query(viewName, params) + return tracer.trace("db.query", async span => { + span.addTags({ db_name: this.name, view_name: viewName, ...params }) + const resp = await this.db.query(viewName, params) + span.addTags({ + total_rows: resp.total_rows, + rows_length: resp.rows.length, + offset: resp.offset, + }) + return resp }) } - destroy(): Promise { - return tracer.trace("db.destroy", span => { - span?.addTags({ db_name: this.name }) - return this.db.destroy() + destroy(): Promise { + return tracer.trace("db.destroy", async span => { + span.addTags({ db_name: this.name }) + const resp = await this.db.destroy() + span.addTags({ ok: resp.ok }) + return resp }) } - compact(): Promise { - return tracer.trace("db.compact", span => { - span?.addTags({ db_name: this.name }) - return this.db.compact() + compact(): Promise { + return tracer.trace("db.compact", async span => { + span.addTags({ db_name: this.name }) + const resp = await this.db.compact() + span.addTags({ ok: resp.ok }) + return resp }) } dump(stream: Writable, opts?: DatabaseDumpOpts | undefined): Promise { return tracer.trace("db.dump", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ + db_name: this.name, + batch_limit: opts?.batch_limit, + batch_size: opts?.batch_size, + style: opts?.style, + timeout: opts?.timeout, + num_doc_ids: opts?.doc_ids?.length, + view: opts?.view, + }) return this.db.dump(stream, opts) }) } load(...args: any[]): Promise { return tracer.trace("db.load", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.load(...args) }) } createIndex(...args: any[]): Promise { return tracer.trace("db.createIndex", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.createIndex(...args) }) } deleteIndex(...args: any[]): Promise { return tracer.trace("db.deleteIndex", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.deleteIndex(...args) }) } getIndexes(...args: any[]): Promise { return tracer.trace("db.getIndexes", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name, num_args: args.length }) return this.db.getIndexes(...args) }) } @@ -177,22 +217,27 @@ export class DDInstrumentedDatabase implements Database { sql: string, parameters?: SqlQueryBinding ): Promise { - return tracer.trace("db.sql", span => { - span?.addTags({ db_name: this.name }) - return this.db.sql(sql, parameters) + return tracer.trace("db.sql", async span => { + span.addTags({ db_name: this.name, num_bindings: parameters?.length }) + const resp = await this.db.sql(sql, parameters) + span.addTags({ num_rows: resp.length }) + return resp }) } sqlPurgeDocument(docIds: string[] | string): Promise { return tracer.trace("db.sqlPurgeDocument", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ + db_name: this.name, + num_docs: Array.isArray(docIds) ? docIds.length : 1, + }) return this.db.sqlPurgeDocument(docIds) }) } sqlDiskCleanup(): Promise { return tracer.trace("db.sqlDiskCleanup", span => { - span?.addTags({ db_name: this.name }) + span.addTags({ db_name: this.name }) return this.db.sqlDiskCleanup() }) } diff --git a/packages/pro b/packages/pro index 3b56ed03a5..25dd40ee12 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 3b56ed03a562b7caa8da8962243efe9050b78e9d +Subproject commit 25dd40ee12b048307b558ebcedb36548d6e042cd diff --git a/packages/server/package.json b/packages/server/package.json index 4e192ec286..d2a51b4453 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -63,6 +63,7 @@ "@bull-board/koa": "5.10.2", "@elastic/elasticsearch": "7.10.0", "@google-cloud/firestore": "7.8.0", + "@koa/cors": "5.0.0", "@koa/router": "13.1.0", "@socket.io/redis-adapter": "^8.2.1", "@types/xml2js": "^0.4.14", @@ -80,8 +81,8 @@ "cookies": "0.8.0", "csvtojson": "2.0.10", "curlconverter": "3.21.0", - "dd-trace": "5.23.0", "dayjs": "^1.10.8", + "dd-trace": "5.26.0", "dotenv": "8.2.0", "form-data": "4.0.0", "global-agent": "3.0.0", @@ -131,6 +132,7 @@ "xml2js": "0.6.2" }, "devDependencies": { + "@babel/core": "^7.22.5", "@babel/preset-env": "7.16.11", "@jest/types": "^29.6.3", "@swc/core": "1.3.71", @@ -140,6 +142,7 @@ "@types/jest": "29.5.5", "@types/koa": "2.13.4", "@types/koa-send": "^4.1.6", + "@types/koa__cors": "5.0.0", "@types/koa__router": "12.0.4", "@types/lodash": "4.14.200", "@types/mssql": "9.1.5", @@ -172,8 +175,7 @@ "tsconfig-paths": "4.0.0", "typescript": "5.5.2", "update-dotenv": "1.1.1", - "yargs": "13.2.4", - "@babel/core": "^7.22.5" + "yargs": "13.2.4" }, "nx": { "targets": { diff --git a/packages/server/specs/openapi.json b/packages/server/specs/openapi.json index f3091a1fc7..c47a14cf21 100644 --- a/packages/server/specs/openapi.json +++ b/packages/server/specs/openapi.json @@ -32,6 +32,15 @@ "type": "string" } }, + "viewId": { + "in": "path", + "name": "viewId", + "required": true, + "description": "The ID of the view which this request is targeting.", + "schema": { + "type": "string" + } + }, "rowId": { "in": "path", "name": "rowId", @@ -47,7 +56,7 @@ "required": true, "description": "The ID of the app which this request is targeting.", "schema": { - "default": "{{ appId }}", + "default": "{{appId}}", "type": "string" } }, @@ -57,7 +66,7 @@ "required": true, "description": "The ID of the app which this request is targeting.", "schema": { - "default": "{{ appId }}", + "default": "{{appId}}", "type": "string" } }, @@ -423,6 +432,110 @@ }, "metrics": { "value": "# HELP budibase_os_uptime Time in seconds that the host operating system has been up.\n# TYPE budibase_os_uptime counter\nbudibase_os_uptime 54958\n# HELP budibase_os_free_mem Bytes of memory free for usage on the host operating system.\n# TYPE budibase_os_free_mem gauge\nbudibase_os_free_mem 804507648\n# HELP budibase_os_total_mem Total bytes of memory on the host operating system.\n# TYPE budibase_os_total_mem gauge\nbudibase_os_total_mem 16742404096\n# HELP budibase_os_used_mem Total bytes of memory in use on the host operating system.\n# TYPE budibase_os_used_mem gauge\nbudibase_os_used_mem 15937896448\n# HELP budibase_os_load1 Host operating system load average.\n# TYPE budibase_os_load1 gauge\nbudibase_os_load1 1.91\n# HELP budibase_os_load5 Host operating system load average.\n# TYPE budibase_os_load5 gauge\nbudibase_os_load5 1.75\n# HELP budibase_os_load15 Host operating system load average.\n# TYPE budibase_os_load15 gauge\nbudibase_os_load15 1.56\n# HELP budibase_tenant_user_count The number of users created.\n# TYPE budibase_tenant_user_count gauge\nbudibase_tenant_user_count 1\n# HELP budibase_tenant_app_count The number of apps created by a user.\n# TYPE budibase_tenant_app_count gauge\nbudibase_tenant_app_count 2\n# HELP budibase_tenant_production_app_count The number of apps a user has published.\n# TYPE budibase_tenant_production_app_count gauge\nbudibase_tenant_production_app_count 1\n# HELP budibase_tenant_dev_app_count The number of apps a user has unpublished in development.\n# TYPE budibase_tenant_dev_app_count gauge\nbudibase_tenant_dev_app_count 1\n# HELP budibase_tenant_db_count The number of couchdb databases including global tables such as _users.\n# TYPE budibase_tenant_db_count gauge\nbudibase_tenant_db_count 3\n# HELP budibase_quota_usage_apps The number of apps created.\n# TYPE budibase_quota_usage_apps gauge\nbudibase_quota_usage_apps 1\n# HELP budibase_quota_limit_apps The limit on the number of apps that can be created.\n# TYPE budibase_quota_limit_apps gauge\nbudibase_quota_limit_apps 9007199254740991\n# HELP budibase_quota_usage_rows The number of database rows used from the quota.\n# TYPE budibase_quota_usage_rows gauge\nbudibase_quota_usage_rows 0\n# HELP budibase_quota_limit_rows The limit on the number of rows that can be created.\n# TYPE budibase_quota_limit_rows gauge\nbudibase_quota_limit_rows 9007199254740991\n# HELP budibase_quota_usage_plugins The number of plugins in use.\n# TYPE budibase_quota_usage_plugins gauge\nbudibase_quota_usage_plugins 0\n# HELP budibase_quota_limit_plugins The limit on the number of plugins that can be created.\n# TYPE budibase_quota_limit_plugins gauge\nbudibase_quota_limit_plugins 9007199254740991\n# HELP budibase_quota_usage_user_groups The number of user groups created.\n# TYPE budibase_quota_usage_user_groups gauge\nbudibase_quota_usage_user_groups 0\n# HELP budibase_quota_limit_user_groups The limit on the number of user groups that can be created.\n# TYPE budibase_quota_limit_user_groups gauge\nbudibase_quota_limit_user_groups 9007199254740991\n# HELP budibase_quota_usage_queries The number of queries used in the current month.\n# TYPE budibase_quota_usage_queries gauge\nbudibase_quota_usage_queries 0\n# HELP budibase_quota_limit_queries The limit on the number of queries for the current month.\n# TYPE budibase_quota_limit_queries gauge\nbudibase_quota_limit_queries 9007199254740991\n# HELP budibase_quota_usage_automations The number of automations used in the current month.\n# TYPE budibase_quota_usage_automations gauge\nbudibase_quota_usage_automations 0\n# HELP budibase_quota_limit_automations The limit on the number of automations that can be created.\n# TYPE budibase_quota_limit_automations gauge\nbudibase_quota_limit_automations 9007199254740991\n" + }, + "view": { + "value": { + "data": { + "name": "peopleView", + "tableId": "ta_896a325f7e8147d2a2cda93c5d236511", + "schema": { + "name": { + "visible": true, + "readonly": false, + "order": 1, + "width": 300 + }, + "age": { + "visible": true, + "readonly": true, + "order": 2, + "width": 200 + }, + "salary": { + "visible": false, + "readonly": false + } + }, + "query": { + "logicalOperator": "all", + "onEmptyFilter": "none", + "groups": [ + { + "logicalOperator": "any", + "filters": [ + { + "operator": "string", + "field": "name", + "value": "John" + }, + { + "operator": "range", + "field": "age", + "value": { + "low": 18, + "high": 100 + } + } + ] + } + ] + }, + "primaryDisplay": "name" + } + } + }, + "views": { + "value": { + "data": [ + { + "name": "peopleView", + "tableId": "ta_896a325f7e8147d2a2cda93c5d236511", + "schema": { + "name": { + "visible": true, + "readonly": false, + "order": 1, + "width": 300 + }, + "age": { + "visible": true, + "readonly": true, + "order": 2, + "width": 200 + }, + "salary": { + "visible": false, + "readonly": false + } + }, + "query": { + "logicalOperator": "all", + "onEmptyFilter": "none", + "groups": [ + { + "logicalOperator": "any", + "filters": [ + { + "operator": "string", + "field": "name", + "value": "John" + }, + { + "operator": "range", + "field": "age", + "value": { + "low": 18, + "high": 100 + } + } + ] + } + ] + }, + "primaryDisplay": "name" + } + ] + } } }, "securitySchemes": { @@ -831,8 +944,7 @@ "type": "string", "enum": [ "static", - "dynamic", - "ai" + "dynamic" ], "description": "Defines whether this is a static or dynamic formula." } @@ -1042,8 +1154,7 @@ "type": "string", "enum": [ "static", - "dynamic", - "ai" + "dynamic" ], "description": "Defines whether this is a static or dynamic formula." } @@ -1264,8 +1375,7 @@ "type": "string", "enum": [ "static", - "dynamic", - "ai" + "dynamic" ], "description": "Defines whether this is a static or dynamic formula." } @@ -2024,6 +2134,872 @@ "required": [ "data" ] + }, + "view": { + "description": "The view to be created/updated.", + "type": "object", + "required": [ + "name", + "schema", + "tableId" + ], + "properties": { + "name": { + "description": "The name of the view.", + "type": "string" + }, + "tableId": { + "description": "The ID of the table this view is based on.", + "type": "string" + }, + "type": { + "description": "The type of view - standard (empty value) or calculation.", + "type": "string", + "enum": [ + "calculation" + ] + }, + "primaryDisplay": { + "type": "string", + "description": "A column used to display rows from this view - usually used when rendered in tables." + }, + "query": { + "description": "Search parameters for view", + "type": "object", + "required": [], + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "onEmptyFilter": { + "description": "If no filters match, should the view return all rows, or no rows.", + "type": "string", + "enum": [ + "all", + "none" + ] + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "sort": { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "The field from the table/view schema to sort on." + }, + "order": { + "type": "string", + "description": "The order in which to sort.", + "enum": [ + "ascending", + "descending" + ] + }, + "type": { + "type": "string", + "description": "The type of sort to perform (by number, or by alphabetically).", + "enum": [ + "string", + "number" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it." + }, + "readonly": { + "type": "boolean", + "description": "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated." + }, + "order": { + "type": "integer", + "description": "A number defining where the column shows up in tables, lowest being first." + }, + "width": { + "type": "integer", + "description": "A width for the column, defined in pixels - this affects rendering in tables." + }, + "column": { + "type": "array", + "description": "If this is a relationship column, we can set the columns we wish to include", + "items": { + "type": "object", + "properties": { + "readonly": { + "type": "boolean" + } + } + } + } + } + }, + { + "type": "object", + "properties": { + "calculationType": { + "type": "string", + "description": "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + "enum": [ + "sum", + "avg", + "count", + "min", + "max" + ] + }, + "field": { + "type": "string", + "description": "The field from the table to perform the calculation on." + }, + "distinct": { + "type": "boolean", + "description": "Can be used in tandem with the count calculation type, to count unique entries." + } + } + } + ] + } + } + } + }, + "viewOutput": { + "type": "object", + "properties": { + "data": { + "description": "The view to be created/updated.", + "type": "object", + "required": [ + "name", + "schema", + "tableId", + "id" + ], + "properties": { + "name": { + "description": "The name of the view.", + "type": "string" + }, + "tableId": { + "description": "The ID of the table this view is based on.", + "type": "string" + }, + "type": { + "description": "The type of view - standard (empty value) or calculation.", + "type": "string", + "enum": [ + "calculation" + ] + }, + "primaryDisplay": { + "type": "string", + "description": "A column used to display rows from this view - usually used when rendered in tables." + }, + "query": { + "description": "Search parameters for view", + "type": "object", + "required": [], + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "onEmptyFilter": { + "description": "If no filters match, should the view return all rows, or no rows.", + "type": "string", + "enum": [ + "all", + "none" + ] + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "sort": { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "The field from the table/view schema to sort on." + }, + "order": { + "type": "string", + "description": "The order in which to sort.", + "enum": [ + "ascending", + "descending" + ] + }, + "type": { + "type": "string", + "description": "The type of sort to perform (by number, or by alphabetically).", + "enum": [ + "string", + "number" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it." + }, + "readonly": { + "type": "boolean", + "description": "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated." + }, + "order": { + "type": "integer", + "description": "A number defining where the column shows up in tables, lowest being first." + }, + "width": { + "type": "integer", + "description": "A width for the column, defined in pixels - this affects rendering in tables." + }, + "column": { + "type": "array", + "description": "If this is a relationship column, we can set the columns we wish to include", + "items": { + "type": "object", + "properties": { + "readonly": { + "type": "boolean" + } + } + } + } + } + }, + { + "type": "object", + "properties": { + "calculationType": { + "type": "string", + "description": "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + "enum": [ + "sum", + "avg", + "count", + "min", + "max" + ] + }, + "field": { + "type": "string", + "description": "The field from the table to perform the calculation on." + }, + "distinct": { + "type": "boolean", + "description": "Can be used in tandem with the count calculation type, to count unique entries." + } + } + } + ] + } + }, + "id": { + "description": "The ID of the view.", + "type": "string" + } + } + } + }, + "required": [ + "data" + ] + }, + "viewSearch": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "description": "The view to be created/updated.", + "type": "object", + "required": [ + "name", + "schema", + "tableId", + "id" + ], + "properties": { + "name": { + "description": "The name of the view.", + "type": "string" + }, + "tableId": { + "description": "The ID of the table this view is based on.", + "type": "string" + }, + "type": { + "description": "The type of view - standard (empty value) or calculation.", + "type": "string", + "enum": [ + "calculation" + ] + }, + "primaryDisplay": { + "type": "string", + "description": "A column used to display rows from this view - usually used when rendered in tables." + }, + "query": { + "description": "Search parameters for view", + "type": "object", + "required": [], + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "onEmptyFilter": { + "description": "If no filters match, should the view return all rows, or no rows.", + "type": "string", + "enum": [ + "all", + "none" + ] + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + }, + "groups": { + "description": "A grouping of filters to be applied.", + "type": "array", + "items": { + "type": "object", + "properties": { + "logicalOperator": { + "description": "When using groups this defines whether all of the filters must match, or only one of them.", + "type": "string", + "enum": [ + "all", + "any" + ] + }, + "filters": { + "description": "A list of filters to apply", + "type": "array", + "items": { + "type": "object", + "properties": { + "operator": { + "type": "string", + "description": "The type of search operation which is being performed.", + "enum": [ + "equal", + "notEqual", + "empty", + "notEmpty", + "fuzzy", + "string", + "contains", + "notContains", + "containsAny", + "oneOf", + "range" + ] + }, + "field": { + "type": "string", + "description": "The field in the view to perform the search on." + }, + "value": { + "description": "The value to search for - the type will depend on the operator in use.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + } + } + } + } + } + } + } + } + } + } + } + }, + "sort": { + "type": "object", + "required": [ + "field" + ], + "properties": { + "field": { + "type": "string", + "description": "The field from the table/view schema to sort on." + }, + "order": { + "type": "string", + "description": "The order in which to sort.", + "enum": [ + "ascending", + "descending" + ] + }, + "type": { + "type": "string", + "description": "The type of sort to perform (by number, or by alphabetically).", + "enum": [ + "string", + "number" + ] + } + } + }, + "schema": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "visible": { + "type": "boolean", + "description": "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it." + }, + "readonly": { + "type": "boolean", + "description": "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated." + }, + "order": { + "type": "integer", + "description": "A number defining where the column shows up in tables, lowest being first." + }, + "width": { + "type": "integer", + "description": "A width for the column, defined in pixels - this affects rendering in tables." + }, + "column": { + "type": "array", + "description": "If this is a relationship column, we can set the columns we wish to include", + "items": { + "type": "object", + "properties": { + "readonly": { + "type": "boolean" + } + } + } + } + } + }, + { + "type": "object", + "properties": { + "calculationType": { + "type": "string", + "description": "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + "enum": [ + "sum", + "avg", + "count", + "min", + "max" + ] + }, + "field": { + "type": "string", + "description": "The field from the table to perform the calculation on." + }, + "distinct": { + "type": "boolean", + "description": "Can be used in tandem with the count calculation type, to count unique entries." + } + } + } + ] + } + }, + "id": { + "description": "The ID of the view.", + "type": "string" + } + } + } + } + }, + "required": [ + "data" + ] } } }, @@ -2741,6 +3717,50 @@ } } }, + "/views/{viewId}/rows/search": { + "post": { + "operationId": "rowViewSearch", + "summary": "Search for rows in a view", + "tags": [ + "rows" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/rowSearch" + } + } + } + }, + "responses": { + "200": { + "description": "The response will contain an array of rows that match the search parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/searchOutput" + }, + "examples": { + "search": { + "$ref": "#/components/examples/rows" + } + } + } + } + } + } + } + }, "/tables": { "post": { "operationId": "tableCreate", @@ -3115,6 +4135,209 @@ } } } + }, + "/views": { + "post": { + "operationId": "viewCreate", + "summary": "Create a view", + "description": "Create a view, this can be against an internal or external table.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/view" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns the created view, including the ID which has been generated for it.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + } + }, + "/views/{viewId}": { + "put": { + "operationId": "viewUpdate", + "summary": "Update a view", + "description": "Update a view, this can be against an internal or external table.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/view" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + }, + "responses": { + "200": { + "description": "Returns the updated view.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + }, + "delete": { + "operationId": "viewDestroy", + "summary": "Delete a view", + "description": "Delete a view, this can be against an internal or external table.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "responses": { + "200": { + "description": "Returns the deleted view.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + }, + "get": { + "operationId": "viewGetById", + "summary": "Retrieve a view", + "description": "Lookup a view, this could be internal or external.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/viewId" + }, + { + "$ref": "#/components/parameters/appId" + } + ], + "responses": { + "200": { + "description": "Returns the retrieved view.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewOutput" + }, + "examples": { + "view": { + "$ref": "#/components/examples/view" + } + } + } + } + } + } + } + }, + "/views/search": { + "post": { + "operationId": "viewSearch", + "summary": "Search for views", + "description": "Based on view properties (currently only name) search for views.", + "tags": [ + "views" + ], + "parameters": [ + { + "$ref": "#/components/parameters/appId" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/nameSearch" + } + } + } + }, + "responses": { + "200": { + "description": "Returns the found views, based on the search parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/viewSearch" + }, + "examples": { + "views": { + "$ref": "#/components/examples/views" + } + } + } + } + } + } + } } }, "tags": [] diff --git a/packages/server/specs/openapi.yaml b/packages/server/specs/openapi.yaml index 1e9b9921cf..edfb29f432 100644 --- a/packages/server/specs/openapi.yaml +++ b/packages/server/specs/openapi.yaml @@ -23,6 +23,13 @@ components: description: The ID of the table which this request is targeting. schema: type: string + viewId: + in: path + name: viewId + required: true + description: The ID of the view which this request is targeting. + schema: + type: string rowId: in: path name: rowId @@ -36,7 +43,7 @@ components: required: true description: The ID of the app which this request is targeting. schema: - default: "{{ appId }}" + default: "{{appId}}" type: string appIdUrl: in: path @@ -44,7 +51,7 @@ components: required: true description: The ID of the app which this request is targeting. schema: - default: "{{ appId }}" + default: "{{appId}}" type: string queryId: in: path @@ -442,6 +449,74 @@ components: # TYPE budibase_quota_limit_automations gauge budibase_quota_limit_automations 9007199254740991 + view: + value: + data: + name: peopleView + tableId: ta_896a325f7e8147d2a2cda93c5d236511 + schema: + name: + visible: true + readonly: false + order: 1 + width: 300 + age: + visible: true + readonly: true + order: 2 + width: 200 + salary: + visible: false + readonly: false + query: + logicalOperator: all + onEmptyFilter: none + groups: + - logicalOperator: any + filters: + - operator: string + field: name + value: John + - operator: range + field: age + value: + low: 18 + high: 100 + primaryDisplay: name + views: + value: + data: + - name: peopleView + tableId: ta_896a325f7e8147d2a2cda93c5d236511 + schema: + name: + visible: true + readonly: false + order: 1 + width: 300 + age: + visible: true + readonly: true + order: 2 + width: 200 + salary: + visible: false + readonly: false + query: + logicalOperator: all + onEmptyFilter: none + groups: + - logicalOperator: any + filters: + - operator: string + field: name + value: John + - operator: range + field: age + value: + low: 18 + high: 100 + primaryDisplay: name securitySchemes: ApiKeyAuth: type: apiKey @@ -761,7 +836,6 @@ components: enum: - static - dynamic - - ai description: Defines whether this is a static or dynamic formula. - type: object properties: @@ -931,7 +1005,6 @@ components: enum: - static - dynamic - - ai description: Defines whether this is a static or dynamic formula. - type: object properties: @@ -1108,7 +1181,6 @@ components: enum: - static - dynamic - - ai description: Defines whether this is a static or dynamic formula. - type: object properties: @@ -1704,6 +1776,644 @@ components: - userIds required: - data + view: + description: The view to be created/updated. + type: object + required: + - name + - schema + - tableId + properties: + name: + description: The name of the view. + type: string + tableId: + description: The ID of the table this view is based on. + type: string + type: + description: The type of view - standard (empty value) or calculation. + type: string + enum: + - calculation + primaryDisplay: + type: string + description: A column used to display rows from this view - usually used when + rendered in tables. + query: + description: Search parameters for view + type: object + required: [] + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + onEmptyFilter: + description: If no filters match, should the view return all rows, or no rows. + type: string + enum: + - all + - none + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + sort: + type: object + required: + - field + properties: + field: + type: string + description: The field from the table/view schema to sort on. + order: + type: string + description: The order in which to sort. + enum: + - ascending + - descending + type: + type: string + description: The type of sort to perform (by number, or by alphabetically). + enum: + - string + - number + schema: + type: object + additionalProperties: + oneOf: + - type: object + properties: + visible: + type: boolean + description: Defines whether the column is visible or not - rows + retrieved/updated through this view will not be able to + access it. + readonly: + type: boolean + description: "When used in combination with 'visible: true' the column will be + visible in row responses but cannot be updated." + order: + type: integer + description: A number defining where the column shows up in tables, lowest being + first. + width: + type: integer + description: A width for the column, defined in pixels - this affects rendering + in tables. + column: + type: array + description: If this is a relationship column, we can set the columns we wish to + include + items: + type: object + properties: + readonly: + type: boolean + - type: object + properties: + calculationType: + type: string + description: This column should be built from a calculation, specifying a type + and field. It is important to note when a calculation is + configured all non-calculation columns will be used for + grouping. + enum: + - sum + - avg + - count + - min + - max + field: + type: string + description: The field from the table to perform the calculation on. + distinct: + type: boolean + description: Can be used in tandem with the count calculation type, to count + unique entries. + viewOutput: + type: object + properties: + data: + description: The view to be created/updated. + type: object + required: + - name + - schema + - tableId + - id + properties: + name: + description: The name of the view. + type: string + tableId: + description: The ID of the table this view is based on. + type: string + type: + description: The type of view - standard (empty value) or calculation. + type: string + enum: + - calculation + primaryDisplay: + type: string + description: A column used to display rows from this view - usually used when + rendered in tables. + query: + description: Search parameters for view + type: object + required: [] + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + onEmptyFilter: + description: If no filters match, should the view return all rows, or no rows. + type: string + enum: + - all + - none + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + sort: + type: object + required: + - field + properties: + field: + type: string + description: The field from the table/view schema to sort on. + order: + type: string + description: The order in which to sort. + enum: + - ascending + - descending + type: + type: string + description: The type of sort to perform (by number, or by alphabetically). + enum: + - string + - number + schema: + type: object + additionalProperties: + oneOf: + - type: object + properties: + visible: + type: boolean + description: Defines whether the column is visible or not - rows + retrieved/updated through this view will not be able + to access it. + readonly: + type: boolean + description: "When used in combination with 'visible: true' the column will be + visible in row responses but cannot be updated." + order: + type: integer + description: A number defining where the column shows up in tables, lowest being + first. + width: + type: integer + description: A width for the column, defined in pixels - this affects rendering + in tables. + column: + type: array + description: If this is a relationship column, we can set the columns we wish to + include + items: + type: object + properties: + readonly: + type: boolean + - type: object + properties: + calculationType: + type: string + description: This column should be built from a calculation, specifying a type + and field. It is important to note when a calculation + is configured all non-calculation columns will be used + for grouping. + enum: + - sum + - avg + - count + - min + - max + field: + type: string + description: The field from the table to perform the calculation on. + distinct: + type: boolean + description: Can be used in tandem with the count calculation type, to count + unique entries. + id: + description: The ID of the view. + type: string + required: + - data + viewSearch: + type: object + properties: + data: + type: array + items: + description: The view to be created/updated. + type: object + required: + - name + - schema + - tableId + - id + properties: + name: + description: The name of the view. + type: string + tableId: + description: The ID of the table this view is based on. + type: string + type: + description: The type of view - standard (empty value) or calculation. + type: string + enum: + - calculation + primaryDisplay: + type: string + description: A column used to display rows from this view - usually used when + rendered in tables. + query: + description: Search parameters for view + type: object + required: [] + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + onEmptyFilter: + description: If no filters match, should the view return all rows, or no rows. + type: string + enum: + - all + - none + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + groups: + description: A grouping of filters to be applied. + type: array + items: + type: object + properties: + logicalOperator: + description: When using groups this defines whether all of the filters must + match, or only one of them. + type: string + enum: + - all + - any + filters: + description: A list of filters to apply + type: array + items: + type: object + properties: + operator: + type: string + description: The type of search operation which is being performed. + enum: + - equal + - notEqual + - empty + - notEmpty + - fuzzy + - string + - contains + - notContains + - containsAny + - oneOf + - range + field: + type: string + description: The field in the view to perform the search on. + value: + description: The value to search for - the type will depend on the operator in + use. + oneOf: + - type: string + - type: number + - type: boolean + - type: object + - type: array + sort: + type: object + required: + - field + properties: + field: + type: string + description: The field from the table/view schema to sort on. + order: + type: string + description: The order in which to sort. + enum: + - ascending + - descending + type: + type: string + description: The type of sort to perform (by number, or by alphabetically). + enum: + - string + - number + schema: + type: object + additionalProperties: + oneOf: + - type: object + properties: + visible: + type: boolean + description: Defines whether the column is visible or not - rows + retrieved/updated through this view will not be able + to access it. + readonly: + type: boolean + description: "When used in combination with 'visible: true' the column will be + visible in row responses but cannot be updated." + order: + type: integer + description: A number defining where the column shows up in tables, lowest being + first. + width: + type: integer + description: A width for the column, defined in pixels - this affects rendering + in tables. + column: + type: array + description: If this is a relationship column, we can set the columns we wish to + include + items: + type: object + properties: + readonly: + type: boolean + - type: object + properties: + calculationType: + type: string + description: This column should be built from a calculation, specifying a type + and field. It is important to note when a + calculation is configured all non-calculation + columns will be used for grouping. + enum: + - sum + - avg + - count + - min + - max + field: + type: string + description: The field from the table to perform the calculation on. + distinct: + type: boolean + description: Can be used in tandem with the count calculation type, to count + unique entries. + id: + description: The ID of the view. + type: string + required: + - data security: - ApiKeyAuth: [] paths: @@ -2136,6 +2846,32 @@ paths: examples: search: $ref: "#/components/examples/rows" + "/views/{viewId}/rows/search": + post: + operationId: rowViewSearch + summary: Search for rows in a view + tags: + - rows + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/rowSearch" + responses: + "200": + description: The response will contain an array of rows that match the search + parameters. + content: + application/json: + schema: + $ref: "#/components/schemas/searchOutput" + examples: + search: + $ref: "#/components/examples/rows" /tables: post: operationId: tableCreate @@ -2359,4 +3095,123 @@ paths: examples: users: $ref: "#/components/examples/users" + /views: + post: + operationId: viewCreate + summary: Create a view + description: Create a view, this can be against an internal or external table. + tags: + - views + parameters: + - $ref: "#/components/parameters/appId" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/view" + examples: + view: + $ref: "#/components/examples/view" + responses: + "200": + description: Returns the created view, including the ID which has been generated + for it. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + "/views/{viewId}": + put: + operationId: viewUpdate + summary: Update a view + description: Update a view, this can be against an internal or external table. + tags: + - views + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/view" + examples: + view: + $ref: "#/components/examples/view" + responses: + "200": + description: Returns the updated view. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + delete: + operationId: viewDestroy + summary: Delete a view + description: Delete a view, this can be against an internal or external table. + tags: + - views + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + responses: + "200": + description: Returns the deleted view. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + get: + operationId: viewGetById + summary: Retrieve a view + description: Lookup a view, this could be internal or external. + tags: + - views + parameters: + - $ref: "#/components/parameters/viewId" + - $ref: "#/components/parameters/appId" + responses: + "200": + description: Returns the retrieved view. + content: + application/json: + schema: + $ref: "#/components/schemas/viewOutput" + examples: + view: + $ref: "#/components/examples/view" + /views/search: + post: + operationId: viewSearch + summary: Search for views + description: Based on view properties (currently only name) search for views. + tags: + - views + parameters: + - $ref: "#/components/parameters/appId" + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/nameSearch" + responses: + "200": + description: Returns the found views, based on the search parameters. + content: + application/json: + schema: + $ref: "#/components/schemas/viewSearch" + examples: + views: + $ref: "#/components/examples/views" tags: [] diff --git a/packages/server/specs/parameters.ts b/packages/server/specs/parameters.ts index 2726ca5064..b3fb274567 100644 --- a/packages/server/specs/parameters.ts +++ b/packages/server/specs/parameters.ts @@ -8,6 +8,16 @@ export const tableId = { }, } +export const viewId = { + in: "path", + name: "viewId", + required: true, + description: "The ID of the view which this request is targeting.", + schema: { + type: "string", + }, +} + export const rowId = { in: "path", name: "rowId", diff --git a/packages/server/specs/resources/index.ts b/packages/server/specs/resources/index.ts index 49508e2e4f..0d32f2a007 100644 --- a/packages/server/specs/resources/index.ts +++ b/packages/server/specs/resources/index.ts @@ -6,6 +6,7 @@ import user from "./user" import metrics from "./metrics" import misc from "./misc" import roles from "./roles" +import view from "./view" export const examples = { ...application.getExamples(), @@ -16,6 +17,7 @@ export const examples = { ...misc.getExamples(), ...metrics.getExamples(), ...roles.getExamples(), + ...view.getExamples(), } export const schemas = { @@ -26,4 +28,5 @@ export const schemas = { ...user.getSchemas(), ...misc.getSchemas(), ...roles.getSchemas(), + ...view.getSchemas(), } diff --git a/packages/server/specs/resources/misc.ts b/packages/server/specs/resources/misc.ts index f56dff3301..8f77d2b22a 100644 --- a/packages/server/specs/resources/misc.ts +++ b/packages/server/specs/resources/misc.ts @@ -1,99 +1,101 @@ import { object } from "./utils" import Resource from "./utils/Resource" +export const searchSchema = { + type: "object", + properties: { + allOr: { + type: "boolean", + description: + "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used.", + }, + string: { + type: "object", + example: { + columnName1: "value", + columnName2: "value", + }, + description: + "A map of field name to the string to search for, this will look for rows that have a value starting with the string value.", + additionalProperties: { + type: "string", + description: "The value to search for in the column.", + }, + }, + fuzzy: { + type: "object", + description: + "Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'.", + }, + range: { + type: "object", + description: + 'Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.', + example: { + columnName1: { + low: 10, + high: 20, + }, + }, + }, + equal: { + type: "object", + description: + "Searches for rows that have a column value that is exactly the value set.", + }, + notEqual: { + type: "object", + description: + "Searches for any row which does not contain the specified column value.", + }, + empty: { + type: "object", + description: + "Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.", + example: { + columnName1: "", + }, + }, + notEmpty: { + type: "object", + description: "Searches for rows which have the specified column.", + }, + oneOf: { + type: "object", + description: + "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].", + }, + contains: { + type: "object", + description: + "Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.", + example: { + arrayColumn: ["a", "b"], + }, + }, + notContains: { + type: "object", + description: + "The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.", + example: { + arrayColumn: ["a", "b"], + }, + }, + containsAny: { + type: "object", + description: + "As with the contains search, only works on array column types and searches for any of the provided values when given an array.", + example: { + arrayColumn: ["a", "b"], + }, + }, + }, +} + export default new Resource().setSchemas({ rowSearch: object( { - query: { - type: "object", - properties: { - allOr: { - type: "boolean", - description: - "Specifies that a row should be returned if it satisfies any of the specified options, rather than requiring it to fulfill all the search parameters. This defaults to false, meaning AND logic will be used.", - }, - string: { - type: "object", - example: { - columnName1: "value", - columnName2: "value", - }, - description: - "A map of field name to the string to search for, this will look for rows that have a value starting with the string value.", - additionalProperties: { - type: "string", - description: "The value to search for in the column.", - }, - }, - fuzzy: { - type: "object", - description: - "Searches for a sub-string within a string column, e.g. searching for 'dib' will match 'Budibase'.", - }, - range: { - type: "object", - description: - 'Searches within a range, the format of this must be in the format of an object with a "low" and "high" property.', - example: { - columnName1: { - low: 10, - high: 20, - }, - }, - }, - equal: { - type: "object", - description: - "Searches for rows that have a column value that is exactly the value set.", - }, - notEqual: { - type: "object", - description: - "Searches for any row which does not contain the specified column value.", - }, - empty: { - type: "object", - description: - "Searches for rows which do not contain the specified column. The object should simply contain keys of the column names, these can map to any value.", - example: { - columnName1: "", - }, - }, - notEmpty: { - type: "object", - description: "Searches for rows which have the specified column.", - }, - oneOf: { - type: "object", - description: - "Searches for rows which have a column value that is any of the specified values. The format of this must be columnName -> [value1, value2].", - }, - contains: { - type: "object", - description: - "Searches for a value, or set of values in array column types (such as a multi-select). If an array of search options is provided then it must match all.", - example: { - arrayColumn: ["a", "b"], - }, - }, - notContains: { - type: "object", - description: - "The logical inverse of contains. Only works on array column types. If an array of values is passed, the row must not match any of them to be returned in the response.", - example: { - arrayColumn: ["a", "b"], - }, - }, - containsAny: { - type: "object", - description: - "As with the contains search, only works on array column types and searches for any of the provided values when given an array.", - example: { - arrayColumn: ["a", "b"], - }, - }, - }, - }, + query: searchSchema, paginate: { type: "boolean", description: "Enables pagination, by default this is disabled.", diff --git a/packages/server/specs/resources/view.ts b/packages/server/specs/resources/view.ts new file mode 100644 index 0000000000..aeb2b97aa9 --- /dev/null +++ b/packages/server/specs/resources/view.ts @@ -0,0 +1,274 @@ +import { object } from "./utils" +import Resource from "./utils/Resource" +import { + ArrayOperator, + BasicOperator, + CalculationType, + RangeOperator, + SortOrder, + SortType, +} from "@budibase/types" +import { cloneDeep } from "lodash" + +const view = { + name: "peopleView", + tableId: "ta_896a325f7e8147d2a2cda93c5d236511", + schema: { + name: { + visible: true, + readonly: false, + order: 1, + width: 300, + }, + age: { + visible: true, + readonly: true, + order: 2, + width: 200, + }, + salary: { + visible: false, + readonly: false, + }, + }, + query: { + logicalOperator: "all", + onEmptyFilter: "none", + groups: [ + { + logicalOperator: "any", + filters: [ + { operator: "string", field: "name", value: "John" }, + { operator: "range", field: "age", value: { low: 18, high: 100 } }, + ], + }, + ], + }, + primaryDisplay: "name", +} + +const baseColumnDef = { + visible: { + type: "boolean", + description: + "Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it.", + }, + readonly: { + type: "boolean", + description: + "When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated.", + }, + order: { + type: "integer", + description: + "A number defining where the column shows up in tables, lowest being first.", + }, + width: { + type: "integer", + description: + "A width for the column, defined in pixels - this affects rendering in tables.", + }, + column: { + type: "array", + description: + "If this is a relationship column, we can set the columns we wish to include", + items: { + type: "object", + properties: { + readonly: { + type: "boolean", + }, + }, + }, + }, +} + +const logicalOperator = { + description: + "When using groups this defines whether all of the filters must match, or only one of them.", + type: "string", + enum: ["all", "any"], +} + +const filterGroup = { + description: "A grouping of filters to be applied.", + type: "array", + items: { + type: "object", + properties: { + logicalOperator, + filters: { + description: "A list of filters to apply", + type: "array", + items: { + type: "object", + properties: { + operator: { + type: "string", + description: + "The type of search operation which is being performed.", + enum: [ + ...Object.values(BasicOperator), + ...Object.values(ArrayOperator), + ...Object.values(RangeOperator), + ], + }, + field: { + type: "string", + description: "The field in the view to perform the search on.", + }, + value: { + description: + "The value to search for - the type will depend on the operator in use.", + oneOf: [ + { type: "string" }, + { type: "number" }, + { type: "boolean" }, + { type: "object" }, + { type: "array" }, + ], + }, + }, + }, + }, + }, + }, +} + +// have to clone to avoid constantly recursive structure - we can't represent this easily +const layeredFilterGroup: any = cloneDeep(filterGroup) +layeredFilterGroup.items.properties.groups = filterGroup + +const viewQuerySchema = { + description: "Search parameters for view", + type: "object", + required: [], + properties: { + logicalOperator, + onEmptyFilter: { + description: + "If no filters match, should the view return all rows, or no rows.", + type: "string", + enum: ["all", "none"], + }, + groups: layeredFilterGroup, + }, +} + +const viewSchema = { + description: "The view to be created/updated.", + type: "object", + required: ["name", "schema", "tableId"], + properties: { + name: { + description: "The name of the view.", + type: "string", + }, + tableId: { + description: "The ID of the table this view is based on.", + type: "string", + }, + type: { + description: "The type of view - standard (empty value) or calculation.", + type: "string", + enum: ["calculation"], + }, + primaryDisplay: { + type: "string", + description: + "A column used to display rows from this view - usually used when rendered in tables.", + }, + query: viewQuerySchema, + sort: { + type: "object", + required: ["field"], + properties: { + field: { + type: "string", + description: "The field from the table/view schema to sort on.", + }, + order: { + type: "string", + description: "The order in which to sort.", + enum: Object.values(SortOrder), + }, + type: { + type: "string", + description: + "The type of sort to perform (by number, or by alphabetically).", + enum: Object.values(SortType), + }, + }, + }, + schema: { + type: "object", + additionalProperties: { + oneOf: [ + { + type: "object", + properties: baseColumnDef, + }, + { + type: "object", + properties: { + calculationType: { + type: "string", + description: + "This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping.", + enum: Object.values(CalculationType), + }, + field: { + type: "string", + description: + "The field from the table to perform the calculation on.", + }, + distinct: { + type: "boolean", + description: + "Can be used in tandem with the count calculation type, to count unique entries.", + }, + }, + }, + ], + }, + }, + }, +} + +const viewOutputSchema = { + ...viewSchema, + properties: { + ...viewSchema.properties, + id: { + description: "The ID of the view.", + type: "string", + }, + }, + required: [...viewSchema.required, "id"], +} + +export default new Resource() + .setExamples({ + view: { + value: { + data: view, + }, + }, + views: { + value: { + data: [view], + }, + }, + }) + .setSchemas({ + view: viewSchema, + viewOutput: object({ + data: viewOutputSchema, + }), + viewSearch: object({ + data: { + type: "array", + items: viewOutputSchema, + }, + }), + }) diff --git a/packages/server/src/api/controllers/public/mapping/applications.ts b/packages/server/src/api/controllers/public/mapping/applications.ts index 0b729fc610..74c55e1c7b 100644 --- a/packages/server/src/api/controllers/public/mapping/applications.ts +++ b/packages/server/src/api/controllers/public/mapping/applications.ts @@ -1,6 +1,7 @@ import { Application } from "./types" +import { RequiredKeys } from "@budibase/types" -function application(body: any): Application { +function application(body: any): RequiredKeys { let app = body?.application ? body.application : body return { _id: app.appId, diff --git a/packages/server/src/api/controllers/public/mapping/index.ts b/packages/server/src/api/controllers/public/mapping/index.ts index 0cdcfbbe4b..b765f4bd76 100644 --- a/packages/server/src/api/controllers/public/mapping/index.ts +++ b/packages/server/src/api/controllers/public/mapping/index.ts @@ -3,6 +3,7 @@ import applications from "./applications" import users from "./users" import rows from "./rows" import queries from "./queries" +import views from "./views" export default { ...tables, @@ -10,4 +11,5 @@ export default { ...users, ...rows, ...queries, + ...views, } diff --git a/packages/server/src/api/controllers/public/mapping/queries.ts b/packages/server/src/api/controllers/public/mapping/queries.ts index 481b5f18c4..d0857453f6 100644 --- a/packages/server/src/api/controllers/public/mapping/queries.ts +++ b/packages/server/src/api/controllers/public/mapping/queries.ts @@ -1,6 +1,7 @@ import { Query, ExecuteQuery } from "./types" +import { RequiredKeys } from "@budibase/types" -function query(body: any): Query { +function query(body: any): RequiredKeys { return { _id: body._id, datasourceId: body.datasourceId, diff --git a/packages/server/src/api/controllers/public/mapping/rows.ts b/packages/server/src/api/controllers/public/mapping/rows.ts index c1cba43718..69f376bebf 100644 --- a/packages/server/src/api/controllers/public/mapping/rows.ts +++ b/packages/server/src/api/controllers/public/mapping/rows.ts @@ -1,6 +1,7 @@ import { Row, RowSearch } from "./types" +import { RequiredKeys } from "@budibase/types" -function row(body: any): Row { +function row(body: any): RequiredKeys { delete body._rev // have to input everything, since structure unknown return { diff --git a/packages/server/src/api/controllers/public/mapping/tables.ts b/packages/server/src/api/controllers/public/mapping/tables.ts index 72ed9f1a9a..857feb82ca 100644 --- a/packages/server/src/api/controllers/public/mapping/tables.ts +++ b/packages/server/src/api/controllers/public/mapping/tables.ts @@ -1,6 +1,7 @@ import { Table } from "./types" +import { RequiredKeys } from "@budibase/types" -function table(body: any): Table { +function table(body: any): RequiredKeys { return { _id: body._id, name: body.name, diff --git a/packages/server/src/api/controllers/public/mapping/types.ts b/packages/server/src/api/controllers/public/mapping/types.ts index 9fea9b7213..6cbcfddb92 100644 --- a/packages/server/src/api/controllers/public/mapping/types.ts +++ b/packages/server/src/api/controllers/public/mapping/types.ts @@ -9,6 +9,9 @@ export type CreateApplicationParams = components["schemas"]["application"] export type Table = components["schemas"]["tableOutput"]["data"] export type CreateTableParams = components["schemas"]["table"] +export type View = components["schemas"]["viewOutput"]["data"] +export type CreateViewParams = components["schemas"]["view"] + export type Row = components["schemas"]["rowOutput"]["data"] export type RowSearch = components["schemas"]["searchOutput"] export type CreateRowParams = components["schemas"]["row"] diff --git a/packages/server/src/api/controllers/public/mapping/users.ts b/packages/server/src/api/controllers/public/mapping/users.ts index 2a158bede9..232c81cec0 100644 --- a/packages/server/src/api/controllers/public/mapping/users.ts +++ b/packages/server/src/api/controllers/public/mapping/users.ts @@ -1,6 +1,7 @@ import { User } from "./types" +import { RequiredKeys } from "@budibase/types" -function user(body: any): User { +function user(body: any): RequiredKeys { return { _id: body._id, email: body.email, diff --git a/packages/server/src/api/controllers/public/mapping/views.ts b/packages/server/src/api/controllers/public/mapping/views.ts new file mode 100644 index 0000000000..9ee1fe42d5 --- /dev/null +++ b/packages/server/src/api/controllers/public/mapping/views.ts @@ -0,0 +1,32 @@ +import { View } from "./types" +import { ViewV2, Ctx, RequiredKeys } from "@budibase/types" +import { dataFilters } from "@budibase/shared-core" + +function view(body: ViewV2): RequiredKeys { + return { + id: body.id, + tableId: body.tableId, + type: body.type, + name: body.name, + schema: body.schema!, + primaryDisplay: body.primaryDisplay, + query: dataFilters.buildQuery(body.query), + sort: body.sort, + } +} + +function mapView(ctx: Ctx<{ data: ViewV2 }>): { data: View } { + return { + data: view(ctx.body.data), + } +} + +function mapViews(ctx: Ctx<{ data: ViewV2[] }>): { data: View[] } { + const views = ctx.body.data.map((body: ViewV2) => view(body)) + return { data: views } +} + +export default { + mapView, + mapViews, +} diff --git a/packages/server/src/api/controllers/public/rows.ts b/packages/server/src/api/controllers/public/rows.ts index 16403b06c9..3c9cbf0ddd 100644 --- a/packages/server/src/api/controllers/public/rows.ts +++ b/packages/server/src/api/controllers/public/rows.ts @@ -22,13 +22,13 @@ export function fixRow(row: Row, params: any) { return row } -export async function search(ctx: UserCtx, next: Next) { +function buildSearchRequestBody(ctx: UserCtx) { let { sort, paginate, bookmark, limit, query } = ctx.request.body // update the body to the correct format of the internal search if (!sort) { sort = {} } - ctx.request.body = { + return { sort: sort.column, sortType: sort.type, sortOrder: sort.order, @@ -37,10 +37,23 @@ export async function search(ctx: UserCtx, next: Next) { limit, query, } +} + +export async function search(ctx: UserCtx, next: Next) { + ctx.request.body = buildSearchRequestBody(ctx) await rowController.search(ctx) await next() } +export async function viewSearch(ctx: UserCtx, next: Next) { + ctx.request.body = buildSearchRequestBody(ctx) + ctx.params = { + viewId: ctx.params.viewId, + } + await rowController.views.searchView(ctx) + await next() +} + export async function create(ctx: UserCtx, next: Next) { ctx.request.body = fixRow(ctx.request.body, ctx.params) await rowController.save(ctx) @@ -79,4 +92,5 @@ export default { update, destroy, search, + viewSearch, } diff --git a/packages/server/src/api/controllers/public/views.ts b/packages/server/src/api/controllers/public/views.ts new file mode 100644 index 0000000000..5b08f39e36 --- /dev/null +++ b/packages/server/src/api/controllers/public/views.ts @@ -0,0 +1,95 @@ +import { search as stringSearch } from "./utils" +import * as controller from "../view" +import { ViewV2, UserCtx, UISearchFilter, PublicAPIView } from "@budibase/types" +import { Next } from "koa" +import { merge } from "lodash" + +function viewRequest(view: PublicAPIView, params?: { viewId: string }) { + const viewV2: ViewV2 = view + if (!viewV2) { + return viewV2 + } + if (params?.viewId) { + viewV2.id = params.viewId + } + if (!view.query) { + viewV2.query = {} + } else { + // public API only has one form of query + viewV2.queryUI = viewV2.query as UISearchFilter + } + viewV2.version = 2 + return viewV2 +} + +function viewResponse(view: ViewV2): PublicAPIView { + // remove our internal structure - always un-necessary + delete view.query + return { + ...view, + query: view.queryUI, + } +} + +function viewsResponse(views: ViewV2[]): PublicAPIView[] { + return views.map(viewResponse) +} + +export async function search(ctx: UserCtx, next: Next) { + const { name } = ctx.request.body + await controller.v2.fetch(ctx) + ctx.body.data = viewsResponse(stringSearch(ctx.body.data, name)) + await next() +} + +export async function create(ctx: UserCtx, next: Next) { + ctx = merge(ctx, { + request: { + body: viewRequest(ctx.request.body), + }, + }) + await controller.v2.create(ctx) + ctx.body.data = viewResponse(ctx.body.data) + await next() +} + +export async function read(ctx: UserCtx, next: Next) { + ctx = merge(ctx, { + params: { + viewId: ctx.params.viewId, + }, + }) + await controller.v2.get(ctx) + ctx.body.data = viewResponse(ctx.body.data) + await next() +} + +export async function update(ctx: UserCtx, next: Next) { + const viewId = ctx.params.viewId + ctx = merge(ctx, { + request: { + body: { + data: viewRequest(ctx.request.body, { viewId }), + }, + }, + params: { + viewId, + }, + }) + await controller.v2.update(ctx) + ctx.body.data = viewResponse(ctx.body.data) + await next() +} + +export async function destroy(ctx: UserCtx, next: Next) { + await controller.v2.remove(ctx) + await next() +} + +export default { + create, + read, + update, + destroy, + search, +} diff --git a/packages/server/src/api/controllers/row/views.ts b/packages/server/src/api/controllers/row/views.ts index b8d01424f2..02ac871de0 100644 --- a/packages/server/src/api/controllers/row/views.ts +++ b/packages/server/src/api/controllers/row/views.ts @@ -50,6 +50,7 @@ export async function searchView( result.rows.forEach(r => (r._viewId = view.id)) ctx.body = result } + function getSortOptions(request: SearchViewRowRequest, view: ViewV2) { if (request.sort) { return { diff --git a/packages/server/src/api/controllers/view/viewsV2.ts b/packages/server/src/api/controllers/view/viewsV2.ts index 753125e0f6..986764a697 100644 --- a/packages/server/src/api/controllers/view/viewsV2.ts +++ b/packages/server/src/api/controllers/view/viewsV2.ts @@ -12,6 +12,7 @@ import { RelationSchemaField, ViewFieldMetadata, CalculationType, + ViewFetchResponseEnriched, CountDistinctCalculationFieldMetadata, CountCalculationFieldMetadata, } from "@budibase/types" @@ -125,6 +126,12 @@ export async function get(ctx: Ctx) { } } +export async function fetch(ctx: Ctx) { + ctx.body = { + data: await sdk.views.getAllEnriched(), + } +} + export async function create(ctx: Ctx) { const view = ctx.request.body const { tableId } = view diff --git a/packages/server/src/api/routes/public/index.ts b/packages/server/src/api/routes/public/index.ts index b665e83f6f..531192811c 100644 --- a/packages/server/src/api/routes/public/index.ts +++ b/packages/server/src/api/routes/public/index.ts @@ -4,19 +4,21 @@ import queryEndpoints from "./queries" import tableEndpoints from "./tables" import rowEndpoints from "./rows" import userEndpoints from "./users" +import viewEndpoints from "./views" import roleEndpoints from "./roles" import authorized from "../../../middleware/authorized" import publicApi from "../../../middleware/publicApi" import { paramResource, paramSubResource } from "../../../middleware/resourceId" -import { PermissionType, PermissionLevel } from "@budibase/types" +import { PermissionLevel, PermissionType } from "@budibase/types" import { CtxFn } from "./utils/Endpoint" import mapperMiddleware from "./middleware/mapper" import env from "../../../environment" +import { middleware, redis } from "@budibase/backend-core" +import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils" +import cors from "@koa/cors" // below imports don't have declaration files const Router = require("@koa/router") const { RateLimit, Stores } = require("koa2-ratelimit") -import { middleware, redis } from "@budibase/backend-core" -import { SelectableDatabase } from "@budibase/backend-core/src/redis/utils" interface KoaRateLimitOptions { socket: { @@ -81,6 +83,7 @@ const publicRouter = new Router({ if (limiter && !env.isDev()) { publicRouter.use(limiter) } +publicRouter.use(cors()) function addMiddleware( endpoints: any, @@ -149,6 +152,7 @@ applyAdminRoutes(metricEndpoints) applyAdminRoutes(roleEndpoints) applyRoutes(appEndpoints, PermissionType.APP, "appId") applyRoutes(tableEndpoints, PermissionType.TABLE, "tableId") +applyRoutes(viewEndpoints, PermissionType.VIEW, "viewId") applyRoutes(userEndpoints, PermissionType.USER, "userId") applyRoutes(queryEndpoints, PermissionType.QUERY, "queryId") // needs to be applied last for routing purposes, don't override other endpoints diff --git a/packages/server/src/api/routes/public/middleware/mapper.ts b/packages/server/src/api/routes/public/middleware/mapper.ts index 03feb6cc5c..9d0cae5d61 100644 --- a/packages/server/src/api/routes/public/middleware/mapper.ts +++ b/packages/server/src/api/routes/public/middleware/mapper.ts @@ -1,9 +1,10 @@ import { Ctx } from "@budibase/types" import mapping from "../../../controllers/public/mapping" -enum Resources { +enum Resource { APPLICATION = "applications", TABLES = "tables", + VIEWS = "views", ROWS = "rows", USERS = "users", QUERIES = "queries", @@ -15,7 +16,7 @@ function isAttachment(ctx: Ctx) { } function isArrayResponse(ctx: Ctx) { - return ctx.url.endsWith(Resources.SEARCH) || Array.isArray(ctx.body) + return ctx.url.endsWith(Resource.SEARCH) || Array.isArray(ctx.body) } function noResponse(ctx: Ctx) { @@ -38,6 +39,14 @@ function processTables(ctx: Ctx) { } } +function processViews(ctx: Ctx) { + if (isArrayResponse(ctx)) { + return mapping.mapViews(ctx) + } else { + return mapping.mapView(ctx) + } +} + function processRows(ctx: Ctx) { if (isArrayResponse(ctx)) { return mapping.mapRowSearch(ctx) @@ -71,20 +80,27 @@ export default async (ctx: Ctx, next: any) => { let body = {} switch (urlParts[0]) { - case Resources.APPLICATION: + case Resource.APPLICATION: body = processApplications(ctx) break - case Resources.TABLES: - if (urlParts[2] === Resources.ROWS) { + case Resource.TABLES: + if (urlParts[2] === Resource.ROWS) { body = processRows(ctx) } else { body = processTables(ctx) } break - case Resources.USERS: + case Resource.VIEWS: + if (urlParts[2] === Resource.ROWS) { + body = processRows(ctx) + } else { + body = processViews(ctx) + } + break + case Resource.USERS: body = processUsers(ctx) break - case Resources.QUERIES: + case Resource.QUERIES: body = processQueries(ctx) break } diff --git a/packages/server/src/api/routes/public/rows.ts b/packages/server/src/api/routes/public/rows.ts index 80ad6d6434..2fb81d4601 100644 --- a/packages/server/src/api/routes/public/rows.ts +++ b/packages/server/src/api/routes/public/rows.ts @@ -1,4 +1,4 @@ -import controller from "../../controllers/public/rows" +import controller, { viewSearch } from "../../controllers/public/rows" import Endpoint from "./utils/Endpoint" import { externalSearchValidator } from "../utils/validators" @@ -168,4 +168,40 @@ read.push( ).addMiddleware(externalSearchValidator()) ) +/** + * @openapi + * /views/{viewId}/rows/search: + * post: + * operationId: rowViewSearch + * summary: Search for rows in a view + * tags: + * - rows + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/rowSearch' + * responses: + * 200: + * description: The response will contain an array of rows that match the search parameters. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/searchOutput' + * examples: + * search: + * $ref: '#/components/examples/rows' + */ +read.push( + new Endpoint( + "post", + "/views/:viewId/rows/search", + controller.viewSearch + ).addMiddleware(externalSearchValidator()) +) + export default { read, write } diff --git a/packages/server/src/api/routes/public/tests/Request.ts b/packages/server/src/api/routes/public/tests/Request.ts index 92a4996124..3dfa11c0b3 100644 --- a/packages/server/src/api/routes/public/tests/Request.ts +++ b/packages/server/src/api/routes/public/tests/Request.ts @@ -1,13 +1,24 @@ -import { User, Table, SearchFilters, Row } from "@budibase/types" +import { + User, + Table, + SearchFilters, + Row, + ViewV2Schema, + ViewV2, + ViewV2Type, + PublicAPIView, +} from "@budibase/types" import { HttpMethod, MakeRequestResponse, generateMakeRequest } from "./utils" import TestConfiguration from "../../../../tests/utilities/TestConfiguration" -import { Expectations } from "../../../../tests/utilities/api/base" type RequestOpts = { internal?: boolean; appId?: string } +type Response = { data: T } + export interface PublicAPIExpectations { status?: number body?: Record + headers?: Record } export class PublicAPIRequest { @@ -15,6 +26,7 @@ export class PublicAPIRequest { private appId: string | undefined tables: PublicTableAPI + views: PublicViewAPI rows: PublicRowAPI apiKey: string @@ -28,6 +40,7 @@ export class PublicAPIRequest { this.appId = appId this.tables = new PublicTableAPI(this) this.rows = new PublicRowAPI(this) + this.views = new PublicViewAPI(this) } static async init(config: TestConfiguration, user: User, opts?: RequestOpts) { @@ -59,6 +72,12 @@ export class PublicAPIRequest { if (expectations?.body) { expect(res.body).toEqual(expectations?.body) } + if (expectations?.headers) { + for (let [header, value] of Object.entries(expectations.headers)) { + const found = res.headers[header] + expect(found?.toLowerCase()).toEqual(value) + } + } return res.body } } @@ -73,9 +92,16 @@ export class PublicTableAPI { async create( table: Table, expectations?: PublicAPIExpectations - ): Promise<{ data: Table }> { + ): Promise> { return this.request.send("post", "/tables", table, expectations) } + + async search( + name: string, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("post", "/tables/search", { name }, expectations) + } } export class PublicRowAPI { @@ -85,11 +111,24 @@ export class PublicRowAPI { this.request = request } + async create( + tableId: string, + row: Row, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send( + "post", + `/tables/${tableId}/rows`, + row, + expectations + ) + } + async search( tableId: string, query: SearchFilters, expectations?: PublicAPIExpectations - ): Promise<{ data: Row[] }> { + ): Promise> { return this.request.send( "post", `/tables/${tableId}/rows/search`, @@ -99,4 +138,75 @@ export class PublicRowAPI { expectations ) } + + async viewSearch( + viewId: string, + query: SearchFilters, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send( + "post", + `/views/${viewId}/rows/search`, + { + query, + }, + expectations + ) + } +} + +export class PublicViewAPI { + request: PublicAPIRequest + + constructor(request: PublicAPIRequest) { + this.request = request + } + + async create( + view: Omit, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("post", "/views", view, expectations) + } + + async update( + viewId: string, + view: Omit, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("put", `/views/${viewId}`, view, expectations) + } + + async destroy( + viewId: string, + expectations?: PublicAPIExpectations + ): Promise { + return this.request.send( + "delete", + `/views/${viewId}`, + undefined, + expectations + ) + } + + async find( + viewId: string, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send("get", `/views/${viewId}`, undefined, expectations) + } + + async search( + viewName: string, + expectations?: PublicAPIExpectations + ): Promise> { + return this.request.send( + "post", + "/views/search", + { + name: viewName, + }, + expectations + ) + } } diff --git a/packages/server/src/api/routes/public/tests/cors.spec.ts b/packages/server/src/api/routes/public/tests/cors.spec.ts new file mode 100644 index 0000000000..1a5895575d --- /dev/null +++ b/packages/server/src/api/routes/public/tests/cors.spec.ts @@ -0,0 +1,21 @@ +import * as setup from "../../tests/utilities" +import { PublicAPIRequest } from "./Request" + +describe("check public API security", () => { + const config = setup.getConfig() + let request: PublicAPIRequest + + beforeAll(async () => { + await config.init() + request = await PublicAPIRequest.init(config, await config.globalUser()) + }) + + it("should have Access-Control-Allow-Origin set to *", async () => { + await request.tables.search("", { + status: 200, + headers: { + "access-control-allow-origin": "*", + }, + }) + }) +}) diff --git a/packages/server/src/api/routes/public/tests/views.spec.ts b/packages/server/src/api/routes/public/tests/views.spec.ts new file mode 100644 index 0000000000..1cbc383f7e --- /dev/null +++ b/packages/server/src/api/routes/public/tests/views.spec.ts @@ -0,0 +1,95 @@ +import * as setup from "../../tests/utilities" +import { basicTable } from "../../../../tests/utilities/structures" +import { BasicOperator, Table, UILogicalOperator } from "@budibase/types" +import { PublicAPIRequest } from "./Request" +import { generator } from "@budibase/backend-core/tests" + +describe("check public API security", () => { + const config = setup.getConfig() + let request: PublicAPIRequest, table: Table + + beforeAll(async () => { + await config.init() + request = await PublicAPIRequest.init(config, await config.globalUser()) + table = (await request.tables.create(basicTable())).data + }) + + function baseView() { + return { + name: generator.word(), + tableId: table._id!, + query: {}, + schema: { + name: { + readonly: true, + visible: true, + }, + }, + } + } + + it("should be able to create a view", async () => { + await request.views.create(baseView(), { status: 201 }) + }) + + it("should be able to update a view", async () => { + const view = await request.views.create(baseView(), { status: 201 }) + const response = await request.views.update(view.data.id, { + ...view.data, + name: "new name", + }) + }) + + it("should be able to search views", async () => { + const viewName = "view to search for" + const view = await request.views.create( + { + ...baseView(), + name: viewName, + }, + { status: 201 } + ) + const results = await request.views.search(viewName, { + status: 200, + }) + expect(results.data.length).toEqual(1) + expect(results.data[0].id).toEqual(view.data.id) + }) + + it("should be able to delete a view", async () => { + const view = await request.views.create(baseView(), { status: 201 }) + const result = await request.views.destroy(view.data.id, { status: 204 }) + expect(result).toBeDefined() + }) + + it("should be able to search rows through a view", async () => { + const row1 = await request.rows.create( + table._id!, + { name: "hello world" }, + { status: 200 } + ) + await request.rows.create(table._id!, { name: "foo bar" }, { status: 200 }) + const response = await request.views.create( + { + ...baseView(), + query: { + logicalOperator: UILogicalOperator.ANY, + groups: [ + { + filters: [ + { + operator: BasicOperator.STRING, + field: "name", + value: "hello", + }, + ], + }, + ], + }, + }, + { status: 201 } + ) + const results = await request.rows.viewSearch(response.data.id, {}) + expect(results.data.length).toEqual(1) + }) +}) diff --git a/packages/server/src/api/routes/public/views.ts b/packages/server/src/api/routes/public/views.ts new file mode 100644 index 0000000000..139d79be1f --- /dev/null +++ b/packages/server/src/api/routes/public/views.ts @@ -0,0 +1,165 @@ +import controller from "../../controllers/public/views" +import Endpoint from "./utils/Endpoint" +import { viewValidator, nameValidator } from "../utils/validators" + +const read = [], + write = [] + +/** + * @openapi + * /views: + * post: + * operationId: viewCreate + * summary: Create a view + * description: Create a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/appId' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/view' + * examples: + * view: + * $ref: '#/components/examples/view' + * responses: + * 200: + * description: Returns the created view, including the ID which has been generated for it. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push( + new Endpoint("post", "/views", controller.create).addMiddleware( + viewValidator() + ) +) + +/** + * @openapi + * /views/{viewId}: + * put: + * operationId: viewUpdate + * summary: Update a view + * description: Update a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * requestBody: + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/view' + * examples: + * view: + * $ref: '#/components/examples/view' + * responses: + * 200: + * description: Returns the updated view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push( + new Endpoint("put", "/views/:viewId", controller.update).addMiddleware( + viewValidator() + ) +) + +/** + * @openapi + * /views/{viewId}: + * delete: + * operationId: viewDestroy + * summary: Delete a view + * description: Delete a view, this can be against an internal or external table. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * responses: + * 200: + * description: Returns the deleted view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +write.push(new Endpoint("delete", "/views/:viewId", controller.destroy)) + +/** + * @openapi + * /views/{viewId}: + * get: + * operationId: viewGetById + * summary: Retrieve a view + * description: Lookup a view, this could be internal or external. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/viewId' + * - $ref: '#/components/parameters/appId' + * responses: + * 200: + * description: Returns the retrieved view. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewOutput' + * examples: + * view: + * $ref: '#/components/examples/view' + */ +read.push(new Endpoint("get", "/views/:viewId", controller.read)) + +/** + * @openapi + * /views/search: + * post: + * operationId: viewSearch + * summary: Search for views + * description: Based on view properties (currently only name) search for views. + * tags: + * - views + * parameters: + * - $ref: '#/components/parameters/appId' + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/nameSearch' + * responses: + * 200: + * description: Returns the found views, based on the search parameters. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/viewSearch' + * examples: + * views: + * $ref: '#/components/examples/views' + */ +read.push( + new Endpoint("post", "/views/search", controller.search).addMiddleware( + nameValidator() + ) +) + +export default { read, write } diff --git a/packages/server/src/api/routes/tests/viewV2.spec.ts b/packages/server/src/api/routes/tests/viewV2.spec.ts index 6e82395e19..23ae7c79d3 100644 --- a/packages/server/src/api/routes/tests/viewV2.spec.ts +++ b/packages/server/src/api/routes/tests/viewV2.spec.ts @@ -2592,6 +2592,33 @@ if (descriptions.length) { }) }) + describe("fetch", () => { + let view: ViewV2, view2: ViewV2 + let table: Table, table2: Table + beforeEach(async () => { + table = await config.api.table.save(saveTableRequest()) + table2 = await config.api.table.save(saveTableRequest()) + view = await config.api.viewV2.create({ + tableId: table._id!, + name: generator.guid(), + schema: {}, + }) + view2 = await config.api.viewV2.create({ + tableId: table2._id!, + name: generator.guid(), + schema: {}, + }) + }) + + it("should be able to list views", async () => { + const response = await config.api.viewV2.fetch({ + status: 200, + }) + expect(response.data.find(v => v.id === view.id)).toBeDefined() + expect(response.data.find(v => v.id === view2.id)).toBeDefined() + }) + }) + describe("destroy", () => { const getRowUsage = async () => { const { total } = await config.doInContext(undefined, () => diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts index 0343cc186c..3bee4f88ce 100644 --- a/packages/server/src/api/routes/utils/validators.ts +++ b/packages/server/src/api/routes/utils/validators.ts @@ -9,6 +9,13 @@ import { Table, WebhookActionType, BuiltinPermissionID, + ViewV2Type, + SortOrder, + SortType, + UILogicalOperator, + BasicOperator, + ArrayOperator, + RangeOperator, } from "@budibase/types" import Joi, { CustomValidator } from "joi" import { ValidSnippetNameRegex, helpers } from "@budibase/shared-core" @@ -66,6 +73,66 @@ export function tableValidator() { ) } +function searchUIFilterValidator() { + const logicalOperator = Joi.string().valid( + ...Object.values(UILogicalOperator) + ) + const operators = [ + ...Object.values(BasicOperator), + ...Object.values(ArrayOperator), + ...Object.values(RangeOperator), + ] + const filters = Joi.array().items( + Joi.object({ + operator: Joi.string() + .valid(...operators) + .required(), + field: Joi.string().required(), + // could do with better validation of value based on operator + value: Joi.any().required(), + }) + ) + return Joi.object({ + logicalOperator, + onEmptyFilter: Joi.string().valid(...Object.values(EmptyFilterOption)), + groups: Joi.array().items( + Joi.object({ + logicalOperator, + filters, + groups: Joi.array().items( + Joi.object({ + filters, + logicalOperator, + }) + ), + }) + ), + }) +} + +export function viewValidator() { + return auth.joiValidator.body( + Joi.object({ + id: OPTIONAL_STRING, + tableId: Joi.string().required(), + name: Joi.string().required(), + type: Joi.string().optional().valid(null, ViewV2Type.CALCULATION), + primaryDisplay: OPTIONAL_STRING, + schema: Joi.object().required(), + query: searchUIFilterValidator().optional(), + sort: Joi.object({ + field: Joi.string().required(), + order: Joi.string() + .optional() + .valid(...Object.values(SortOrder)), + type: Joi.string() + .optional() + .valid(...Object.values(SortType)), + }).optional(), + }) + ) +} + export function nameValidator() { return auth.joiValidator.body( Joi.object({ @@ -91,8 +158,7 @@ export function datasourceValidator() { ) } -function filterObject(opts?: { unknown: boolean }) { - const { unknown = true } = opts || {} +function searchFiltersValidator() { const conditionalFilteringObject = () => Joi.object({ conditions: Joi.array().items(Joi.link("#schema")).required(), @@ -119,7 +185,14 @@ function filterObject(opts?: { unknown: boolean }) { fuzzyOr: Joi.forbidden(), documentType: Joi.forbidden(), } - return Joi.object(filtersValidators).unknown(unknown).id("schema") + + return Joi.object(filtersValidators) +} + +function filterObject(opts?: { unknown: boolean }) { + const { unknown = true } = opts || {} + + return searchFiltersValidator().unknown(unknown).id("schema") } export function internalSearchValidator() { diff --git a/packages/server/src/api/routes/view.ts b/packages/server/src/api/routes/view.ts index 807d8e2f28..92e5f02a18 100644 --- a/packages/server/src/api/routes/view.ts +++ b/packages/server/src/api/routes/view.ts @@ -8,6 +8,11 @@ import { permissions } from "@budibase/backend-core" const router: Router = new Router() router + .get( + "/api/v2/views", + authorized(permissions.BUILDER), + viewController.v2.fetch + ) .get( "/api/v2/views/:viewId", authorizedResource( diff --git a/packages/server/src/definitions/openapi.ts b/packages/server/src/definitions/openapi.ts index 4479c89d9d..b82229130b 100644 --- a/packages/server/src/definitions/openapi.ts +++ b/packages/server/src/definitions/openapi.ts @@ -65,6 +65,9 @@ export interface paths { "/tables/{tableId}/rows/search": { post: operations["rowSearch"]; }; + "/views/{viewId}/rows/search": { + post: operations["rowViewSearch"]; + }; "/tables": { /** Create a table, this could be internal or external. */ post: operations["tableCreate"]; @@ -93,6 +96,22 @@ export interface paths { /** Based on user properties (currently only name) search for users. */ post: operations["userSearch"]; }; + "/views": { + /** Create a view, this can be against an internal or external table. */ + post: operations["viewCreate"]; + }; + "/views/{viewId}": { + /** Lookup a view, this could be internal or external. */ + get: operations["viewGetById"]; + /** Update a view, this can be against an internal or external table. */ + put: operations["viewUpdate"]; + /** Delete a view, this can be against an internal or external table. */ + delete: operations["viewDestroy"]; + }; + "/views/search": { + /** Based on view properties (currently only name) search for views. */ + post: operations["viewSearch"]; + }; } export interface components { @@ -813,10 +832,442 @@ export interface components { userIds: string[]; }; }; + /** @description The view to be created/updated. */ + view: { + /** @description The name of the view. */ + name: string; + /** @description The ID of the table this view is based on. */ + tableId: string; + /** + * @description The type of view - standard (empty value) or calculation. + * @enum {string} + */ + type?: "calculation"; + /** @description A column used to display rows from this view - usually used when rendered in tables. */ + primaryDisplay?: string; + /** @description Search parameters for view */ + query?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** + * @description If no filters match, should the view return all rows, or no rows. + * @enum {string} + */ + onEmptyFilter?: "all" | "none"; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + }[]; + }[]; + }; + sort?: { + /** @description The field from the table/view schema to sort on. */ + field: string; + /** + * @description The order in which to sort. + * @enum {string} + */ + order?: "ascending" | "descending"; + /** + * @description The type of sort to perform (by number, or by alphabetically). + * @enum {string} + */ + type?: "string" | "number"; + }; + schema: { + [key: string]: + | { + /** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */ + visible?: boolean; + /** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */ + readonly?: boolean; + /** @description A number defining where the column shows up in tables, lowest being first. */ + order?: number; + /** @description A width for the column, defined in pixels - this affects rendering in tables. */ + width?: number; + /** @description If this is a relationship column, we can set the columns we wish to include */ + column?: { + readonly?: boolean; + }[]; + } + | { + /** + * @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping. + * @enum {string} + */ + calculationType?: "sum" | "avg" | "count" | "min" | "max"; + /** @description The field from the table to perform the calculation on. */ + field?: string; + /** @description Can be used in tandem with the count calculation type, to count unique entries. */ + distinct?: boolean; + }; + }; + }; + viewOutput: { + /** @description The view to be created/updated. */ + data: { + /** @description The name of the view. */ + name: string; + /** @description The ID of the table this view is based on. */ + tableId: string; + /** + * @description The type of view - standard (empty value) or calculation. + * @enum {string} + */ + type?: "calculation"; + /** @description A column used to display rows from this view - usually used when rendered in tables. */ + primaryDisplay?: string; + /** @description Search parameters for view */ + query?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** + * @description If no filters match, should the view return all rows, or no rows. + * @enum {string} + */ + onEmptyFilter?: "all" | "none"; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + }[]; + }[]; + }; + sort?: { + /** @description The field from the table/view schema to sort on. */ + field: string; + /** + * @description The order in which to sort. + * @enum {string} + */ + order?: "ascending" | "descending"; + /** + * @description The type of sort to perform (by number, or by alphabetically). + * @enum {string} + */ + type?: "string" | "number"; + }; + schema: { + [key: string]: + | { + /** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */ + visible?: boolean; + /** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */ + readonly?: boolean; + /** @description A number defining where the column shows up in tables, lowest being first. */ + order?: number; + /** @description A width for the column, defined in pixels - this affects rendering in tables. */ + width?: number; + /** @description If this is a relationship column, we can set the columns we wish to include */ + column?: { + readonly?: boolean; + }[]; + } + | { + /** + * @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping. + * @enum {string} + */ + calculationType?: "sum" | "avg" | "count" | "min" | "max"; + /** @description The field from the table to perform the calculation on. */ + field?: string; + /** @description Can be used in tandem with the count calculation type, to count unique entries. */ + distinct?: boolean; + }; + }; + /** @description The ID of the view. */ + id: string; + }; + }; + viewSearch: { + data: { + /** @description The name of the view. */ + name: string; + /** @description The ID of the table this view is based on. */ + tableId: string; + /** + * @description The type of view - standard (empty value) or calculation. + * @enum {string} + */ + type?: "calculation"; + /** @description A column used to display rows from this view - usually used when rendered in tables. */ + primaryDisplay?: string; + /** @description Search parameters for view */ + query?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** + * @description If no filters match, should the view return all rows, or no rows. + * @enum {string} + */ + onEmptyFilter?: "all" | "none"; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + /** @description A grouping of filters to be applied. */ + groups?: { + /** + * @description When using groups this defines whether all of the filters must match, or only one of them. + * @enum {string} + */ + logicalOperator?: "all" | "any"; + /** @description A list of filters to apply */ + filters?: { + /** + * @description The type of search operation which is being performed. + * @enum {string} + */ + operator?: + | "equal" + | "notEqual" + | "empty" + | "notEmpty" + | "fuzzy" + | "string" + | "contains" + | "notContains" + | "containsAny" + | "oneOf" + | "range"; + /** @description The field in the view to perform the search on. */ + field?: string; + /** @description The value to search for - the type will depend on the operator in use. */ + value?: + | string + | number + | boolean + | { [key: string]: unknown } + | unknown[]; + }[]; + }[]; + }[]; + }; + sort?: { + /** @description The field from the table/view schema to sort on. */ + field: string; + /** + * @description The order in which to sort. + * @enum {string} + */ + order?: "ascending" | "descending"; + /** + * @description The type of sort to perform (by number, or by alphabetically). + * @enum {string} + */ + type?: "string" | "number"; + }; + schema: { + [key: string]: + | { + /** @description Defines whether the column is visible or not - rows retrieved/updated through this view will not be able to access it. */ + visible?: boolean; + /** @description When used in combination with 'visible: true' the column will be visible in row responses but cannot be updated. */ + readonly?: boolean; + /** @description A number defining where the column shows up in tables, lowest being first. */ + order?: number; + /** @description A width for the column, defined in pixels - this affects rendering in tables. */ + width?: number; + /** @description If this is a relationship column, we can set the columns we wish to include */ + column?: { + readonly?: boolean; + }[]; + } + | { + /** + * @description This column should be built from a calculation, specifying a type and field. It is important to note when a calculation is configured all non-calculation columns will be used for grouping. + * @enum {string} + */ + calculationType?: "sum" | "avg" | "count" | "min" | "max"; + /** @description The field from the table to perform the calculation on. */ + field?: string; + /** @description Can be used in tandem with the count calculation type, to count unique entries. */ + distinct?: boolean; + }; + }; + /** @description The ID of the view. */ + id: string; + }[]; + }; }; parameters: { /** @description The ID of the table which this request is targeting. */ tableId: string; + /** @description The ID of the view which this request is targeting. */ + viewId: string; /** @description The ID of the row which this request is targeting. */ rowId: string; /** @description The ID of the app which this request is targeting. */ @@ -1213,6 +1664,31 @@ export interface operations { }; }; }; + rowViewSearch: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** The response will contain an array of rows that match the search parameters. */ + 200: { + content: { + "application/json": components["schemas"]["searchOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["rowSearch"]; + }; + }; + }; /** Create a table, this could be internal or external. */ tableCreate: { parameters: { @@ -1409,6 +1885,118 @@ export interface operations { }; }; }; + /** Create a view, this can be against an internal or external table. */ + viewCreate: { + parameters: { + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the created view, including the ID which has been generated for it. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["view"]; + }; + }; + }; + /** Lookup a view, this could be internal or external. */ + viewGetById: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the retrieved view. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + }; + /** Update a view, this can be against an internal or external table. */ + viewUpdate: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the updated view. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["view"]; + }; + }; + }; + /** Delete a view, this can be against an internal or external table. */ + viewDestroy: { + parameters: { + path: { + /** The ID of the view which this request is targeting. */ + viewId: components["parameters"]["viewId"]; + }; + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the deleted view. */ + 200: { + content: { + "application/json": components["schemas"]["viewOutput"]; + }; + }; + }; + }; + /** Based on view properties (currently only name) search for views. */ + viewSearch: { + parameters: { + header: { + /** The ID of the app which this request is targeting. */ + "x-budibase-app-id": components["parameters"]["appId"]; + }; + }; + responses: { + /** Returns the found views, based on the search parameters. */ + 200: { + content: { + "application/json": components["schemas"]["viewSearch"]; + }; + }; + }; + requestBody: { + content: { + "application/json": components["schemas"]["nameSearch"]; + }; + }; + }; } export interface external {} diff --git a/packages/server/src/middleware/publicApi.ts b/packages/server/src/middleware/publicApi.ts index e3897d1a49..da10dd3539 100644 --- a/packages/server/src/middleware/publicApi.ts +++ b/packages/server/src/middleware/publicApi.ts @@ -1,8 +1,8 @@ import { constants, utils } from "@budibase/backend-core" -import { BBContext } from "@budibase/types" +import { Ctx } from "@budibase/types" export default function ({ requiresAppId }: { requiresAppId?: boolean } = {}) { - return async (ctx: BBContext, next: any) => { + return async (ctx: Ctx, next: any) => { const appId = await utils.getAppIdFromCtx(ctx) if (requiresAppId && !appId) { ctx.throw( diff --git a/packages/server/src/sdk/app/tables/getters.ts b/packages/server/src/sdk/app/tables/getters.ts index c59298faf1..1ad82b8e42 100644 --- a/packages/server/src/sdk/app/tables/getters.ts +++ b/packages/server/src/sdk/app/tables/getters.ts @@ -78,8 +78,11 @@ export async function getAllInternalTables(db?: Database): Promise { } async function getAllExternalTables(): Promise { + // this is all datasources, we'll need to filter out internal const datasources = await sdk.datasources.fetch({ enriched: true }) - const allEntities = datasources.map(datasource => datasource.entities) + const allEntities = datasources + .filter(datasource => datasource._id !== INTERNAL_TABLE_SOURCE_ID) + .map(datasource => datasource.entities) let final: Table[] = [] for (let entities of allEntities) { if (entities) { diff --git a/packages/server/src/sdk/app/views/index.ts b/packages/server/src/sdk/app/views/index.ts index 9c111ff079..58537c96ad 100644 --- a/packages/server/src/sdk/app/views/index.ts +++ b/packages/server/src/sdk/app/views/index.ts @@ -26,6 +26,7 @@ import { isExternalTableID } from "../../../integrations/utils" import * as internal from "./internal" import * as external from "./external" import sdk from "../../../sdk" +import { ensureQueryUISet } from "./utils" function pickApi(tableId: any) { if (isExternalTableID(tableId)) { @@ -44,6 +45,24 @@ export async function getEnriched(viewId: string): Promise { return pickApi(tableId).getEnriched(viewId) } +export async function getAllEnriched(): Promise { + const tables = await sdk.tables.getAllTables() + let views: ViewV2Enriched[] = [] + for (let table of tables) { + if (!table.views || Object.keys(table.views).length === 0) { + continue + } + const v2Views = Object.values(table.views).filter(isV2) + const enrichedViews = await Promise.all( + v2Views.map(view => + enrichSchema(ensureQueryUISet(view), table.schema, tables) + ) + ) + views = views.concat(enrichedViews) + } + return views +} + export async function getTable(view: string | ViewV2): Promise
{ const viewId = typeof view === "string" ? view : view.id const cached = context.getTableForView(viewId) @@ -333,13 +352,19 @@ export function allowedFields( export async function enrichSchema( view: ViewV2, - tableSchema: TableSchema + tableSchema: TableSchema, + tables?: Table[] ): Promise { async function populateRelTableSchema( tableId: string, viewFields: Record ) { - const relTable = await sdk.tables.getTable(tableId) + let relTable = tables + ? tables?.find(t => t._id === tableId) + : await sdk.tables.getTable(tableId) + if (!relTable) { + throw new Error("Cannot enrich relationship, table not found") + } const result: Record = {} for (const relTableFieldName of Object.keys(relTable.schema)) { const relTableField = relTable.schema[relTableFieldName] diff --git a/packages/server/src/tests/utilities/api/viewV2.ts b/packages/server/src/tests/utilities/api/viewV2.ts index 9741240f27..7cc57673a0 100644 --- a/packages/server/src/tests/utilities/api/viewV2.ts +++ b/packages/server/src/tests/utilities/api/viewV2.ts @@ -5,6 +5,7 @@ import { SearchViewRowRequest, PaginatedSearchRowResponse, ViewResponseEnriched, + ViewFetchResponseEnriched, } from "@budibase/types" import { Expectations, TestAPI } from "./base" @@ -49,6 +50,12 @@ export class ViewV2API extends TestAPI { .data } + fetch = async (expectations?: Expectations) => { + return await this._get(`/api/v2/views`, { + expectations, + }) + } + search = async ( viewId: string, params?: SearchViewRowRequest, diff --git a/packages/types/src/api/web/app/view.ts b/packages/types/src/api/web/app/view.ts index a6be5e2986..2560f7507f 100644 --- a/packages/types/src/api/web/app/view.ts +++ b/packages/types/src/api/web/app/view.ts @@ -9,6 +9,10 @@ export interface ViewResponseEnriched { data: ViewV2Enriched } +export interface ViewFetchResponseEnriched { + data: ViewV2Enriched[] +} + export interface CreateViewRequest extends Omit {} export interface UpdateViewRequest extends ViewV2 {} diff --git a/packages/types/src/documents/app/view.ts b/packages/types/src/documents/app/view.ts index 1212031f24..1170284b15 100644 --- a/packages/types/src/documents/app/view.ts +++ b/packages/types/src/documents/app/view.ts @@ -101,6 +101,10 @@ export interface ViewV2 { schema?: ViewV2Schema } +export interface PublicAPIView extends Omit { + query?: UISearchFilter +} + export type ViewV2Schema = Record export type ViewSchema = ViewCountOrSumSchema | ViewStatisticsSchema diff --git a/packages/types/src/documents/pouch.ts b/packages/types/src/documents/pouch.ts index 6ff851a515..c2ac1599ee 100644 --- a/packages/types/src/documents/pouch.ts +++ b/packages/types/src/documents/pouch.ts @@ -8,7 +8,7 @@ export interface RowValue { export interface RowResponse { id: string key: string - error: string + error?: string value: T doc?: T } diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 9797715329..9feecbdb2b 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -163,8 +163,8 @@ export interface Database { viewName: string, params: DatabaseQueryOpts ): Promise> - destroy(): Promise - compact(): Promise + destroy(): Promise + compact(): Promise // these are all PouchDB related functions that are rarely used - in future // should be replaced by better typed/non-pouch implemented methods dump(stream: Writable, opts?: DatabaseDumpOpts): Promise diff --git a/packages/worker/package.json b/packages/worker/package.json index 5b6804d38f..36c88a9a49 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -50,7 +50,7 @@ "bcrypt": "5.1.0", "bcryptjs": "2.4.3", "bull": "4.10.1", - "dd-trace": "5.23.0", + "dd-trace": "5.26.0", "dotenv": "8.6.0", "email-validator": "^2.0.4", "global-agent": "3.0.0", diff --git a/yarn.lock b/yarn.lock index 506cea0f03..3960a0d5a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2210,7 +2210,7 @@ bcryptjs "2.4.3" bull "4.10.1" correlation-id "4.0.0" - dd-trace "5.23.0" + dd-trace "5.26.0" dotenv "16.0.1" google-auth-library "^8.0.1" google-spreadsheet "npm:@budibase/google-spreadsheet@4.1.5" @@ -2477,6 +2477,11 @@ resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.4.tgz#d77bfa9ff49e2307c0c6e6b8b26b5dd3c05816c4" integrity sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw== +"@datadog/libdatadog@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@datadog/libdatadog/-/libdatadog-0.2.2.tgz#ac02c76ac9a38250dca740727c7cdf00244ce3d3" + integrity sha512-rTWo96mEPTY5UbtGoFj8/wY0uKSViJhsPg/Z6aoFWBFXQ8b45Ix2e/yvf92AAwrhG+gPLTxEqTXh3kef2dP8Ow== + "@datadog/native-appsec@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.1.1.tgz#76aa34697e6ecbd3d9ef7e6938d3cdcfa689b1f3" @@ -2484,6 +2489,13 @@ dependencies: node-gyp-build "^3.9.0" +"@datadog/native-appsec@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@datadog/native-appsec/-/native-appsec-8.3.0.tgz#91afd89d18d386be4da8a1b0e04500f2f8b5eb66" + integrity sha512-RYHbSJ/MwJcJaLzaCaZvUyNLUKFbMshayIiv4ckpFpQJDiq1T8t9iM2k7008s75g1vRuXfsRNX7MaLn4aoFuWA== + dependencies: + node-gyp-build "^3.9.0" + "@datadog/native-iast-rewriter@2.4.1": version "2.4.1" resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.4.1.tgz#e8211f78c818906513fb96a549374da0382c7623" @@ -2492,6 +2504,14 @@ lru-cache "^7.14.0" node-gyp-build "^4.5.0" +"@datadog/native-iast-rewriter@2.5.0": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-rewriter/-/native-iast-rewriter-2.5.0.tgz#b613defe86e78168f750d1f1662d4ffb3cf002e6" + integrity sha512-WRu34A3Wwp6oafX8KWNAbedtDaaJO+nzfYQht7pcJKjyC2ggfPeF7SoP+eDo9wTn4/nQwEOscSR4hkJqTRlpXQ== + dependencies: + lru-cache "^7.14.0" + node-gyp-build "^4.5.0" + "@datadog/native-iast-taint-tracking@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.1.0.tgz#7b2ed7f8fad212d65e5ab03bcdea8b42a3051b2e" @@ -2499,6 +2519,13 @@ dependencies: node-gyp-build "^3.9.0" +"@datadog/native-iast-taint-tracking@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@datadog/native-iast-taint-tracking/-/native-iast-taint-tracking-3.2.0.tgz#9fb6823d82f934e12c06ea1baa7399ca80deb2ec" + integrity sha512-Mc6FzCoyvU5yXLMsMS9yKnEqJMWoImAukJXolNWCTm+JQYCMf2yMsJ8pBAm7KyZKliamM9rCn7h7Tr2H3lXwjA== + dependencies: + node-gyp-build "^3.9.0" + "@datadog/native-metrics@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-2.0.0.tgz#65bf03313ee419956361e097551db36173e85712" @@ -2507,6 +2534,14 @@ node-addon-api "^6.1.0" node-gyp-build "^3.9.0" +"@datadog/native-metrics@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@datadog/native-metrics/-/native-metrics-3.0.1.tgz#dc276c93785c0377a048e316f23b7c8ff3acfa84" + integrity sha512-0GuMyYyXf+Qpb/F+Fcekz58f2mO37lit9U3jMbWY/m8kac44gCPABzL5q3gWbdH+hWgqYfQoEYsdNDGSrKfwoQ== + dependencies: + node-addon-api "^6.1.0" + node-gyp-build "^3.9.0" + "@datadog/pprof@5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.3.0.tgz#c2f58d328ecced7f99887f1a559d7fe3aecb9219" @@ -2518,6 +2553,17 @@ pprof-format "^2.1.0" source-map "^0.7.4" +"@datadog/pprof@5.4.1": + version "5.4.1" + resolved "https://registry.yarnpkg.com/@datadog/pprof/-/pprof-5.4.1.tgz#08c9bcf5d8efb2eeafdfc9f5bb5402f79fb41266" + integrity sha512-IvpL96e/cuh8ugP5O8Czdup7XQOLHeIDgM5pac5W7Lc1YzGe5zTtebKFpitvb1CPw1YY+1qFx0pWGgKP2kOfHg== + dependencies: + delay "^5.0.0" + node-gyp-build "<4.0" + p-limit "^3.1.0" + pprof-format "^2.1.0" + source-map "^0.7.4" + "@datadog/sketches-js@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@datadog/sketches-js/-/sketches-js-2.1.0.tgz#8c7e8028a5fc22ad102fa542b0a446c956830455" @@ -2846,6 +2892,11 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -3428,6 +3479,13 @@ resolved "https://registry.yarnpkg.com/@jsep-plugin/regex/-/regex-1.0.4.tgz#cb2fc423220fa71c609323b9ba7f7d344a755fcc" integrity sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg== +"@koa/cors@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-5.0.0.tgz#0029b5f057fa0d0ae0e37dd2c89ece315a0daffd" + integrity sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw== + dependencies: + vary "^1.1.2" + "@koa/router@13.1.0": version "13.1.0" resolved "https://registry.yarnpkg.com/@koa/router/-/router-13.1.0.tgz#43f4c554444ea4f4a148a5735a9525c6d16fd1b5" @@ -5698,6 +5756,13 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/koa__cors@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/koa__cors/-/koa__cors-5.0.0.tgz#74567a045b599266e2cd3940cef96cedecc2ef1f" + integrity sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g== + dependencies: + "@types/koa" "*" + "@types/koa__router@12.0.4": version "12.0.4" resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-12.0.4.tgz#a1f9afec9dc7e7d9fa1252d1938c44b403e19a28" @@ -8920,6 +8985,15 @@ cron-validate@1.4.5: dependencies: yup "0.32.9" +cross-spawn@7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -9292,6 +9366,44 @@ dd-trace@5.23.0: shell-quote "^1.8.1" tlhunter-sorted-set "^0.1.0" +dd-trace@5.26.0: + version "5.26.0" + resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-5.26.0.tgz#cc55061f66742bf01d0d7dc9f75c0e4937c82f40" + integrity sha512-AQ4usxrbAG41f7CKUUe7fayZgfrh24D0L0vNzcU2mMJOmqQ3bXeDz9uSHkF3aFY8Epcsegrep3ifjRC0/zOxTw== + dependencies: + "@datadog/libdatadog" "^0.2.2" + "@datadog/native-appsec" "8.3.0" + "@datadog/native-iast-rewriter" "2.5.0" + "@datadog/native-iast-taint-tracking" "3.2.0" + "@datadog/native-metrics" "^3.0.1" + "@datadog/pprof" "5.4.1" + "@datadog/sketches-js" "^2.1.0" + "@isaacs/ttlcache" "^1.4.1" + "@opentelemetry/api" ">=1.0.0 <1.9.0" + "@opentelemetry/core" "^1.14.0" + crypto-randomuuid "^1.0.0" + dc-polyfill "^0.1.4" + ignore "^5.2.4" + import-in-the-middle "1.11.2" + int64-buffer "^0.1.9" + istanbul-lib-coverage "3.2.0" + jest-docblock "^29.7.0" + koalas "^1.0.2" + limiter "1.1.5" + lodash.sortby "^4.7.0" + lru-cache "^7.14.0" + module-details-from-path "^1.0.3" + msgpack-lite "^0.1.26" + opentracing ">=0.12.1" + path-to-regexp "^0.1.10" + pprof-format "^2.1.0" + protobufjs "^7.2.5" + retry "^0.13.1" + rfdc "^1.3.1" + semver "^7.5.4" + shell-quote "^1.8.1" + tlhunter-sorted-set "^0.1.0" + debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.4: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" @@ -20147,16 +20259,7 @@ string-range@~1.2, string-range@~1.2.1: resolved "https://registry.yarnpkg.com/string-range/-/string-range-1.2.2.tgz#a893ed347e72299bc83befbbf2a692a8d239d5dd" integrity sha512-tYft6IFi8SjplJpxCUxyqisD3b+R2CSkomrtJYCkvuf1KuCAWgz7YXt4O0jip7efpfCemwHEzTEAO8EuOYgh3w== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20248,7 +20351,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20262,13 +20365,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -20517,6 +20613,26 @@ svelte-spa-router@^4.0.1: dependencies: regexparam "2.0.2" +svelte@4.2.19: + version "4.2.19" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.19.tgz#4e6e84a8818e2cd04ae0255fcf395bc211e61d4c" + integrity sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw== + dependencies: + "@ampproject/remapping" "^2.2.1" + "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/estree" "^1.0.1" + acorn "^8.9.0" + aria-query "^5.3.0" + axobject-query "^4.0.0" + code-red "^1.0.3" + css-tree "^2.3.1" + estree-walker "^3.0.3" + is-reference "^3.0.1" + locate-character "^3.0.0" + magic-string "^0.30.4" + periscopic "^3.1.0" + svelte@^4.2.10: version "4.2.12" resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.12.tgz#13d98d2274d24d3ad216c8fdc801511171c70bb1" @@ -22054,7 +22170,7 @@ worker-farm@1.7.0: dependencies: errno "~0.1.7" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22072,15 +22188,6 @@ wrap-ansi@^5.1.0: string-width "^3.0.0" strip-ansi "^5.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"