@budibase/server 0.9.185-alpha.1 → 0.9.185-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +1 -1
- package/builder/assets/bb-space-man.5be51a45.svg +290 -0
- package/builder/assets/index.4a7c5636.css +1 -0
- package/builder/assets/index.dc495306.js +1317 -0
- package/builder/assets/vendor.470042a4.js +152 -0
- package/builder/assets/vendor.5e1fe644.css +1 -0
- package/builder/index.html +4 -4
- package/coverage/clover.xml +689 -598
- package/coverage/coverage-final.json +89 -87
- package/coverage/lcov-report/api/controllers/analytics.js.html +2 -2
- package/coverage/lcov-report/api/controllers/apikeys.js.html +2 -2
- package/coverage/lcov-report/api/controllers/application.js.html +43 -13
- package/coverage/lcov-report/api/controllers/auth.js.html +7 -7
- package/coverage/lcov-report/api/controllers/automation.js.html +3 -3
- package/coverage/lcov-report/api/controllers/backup.js.html +1 -1
- package/coverage/lcov-report/api/controllers/cloud.js.html +1 -1
- package/coverage/lcov-report/api/controllers/component.js.html +4 -4
- package/coverage/lcov-report/api/controllers/datasource.js.html +129 -21
- package/coverage/lcov-report/api/controllers/dev.js.html +16 -7
- package/coverage/lcov-report/api/controllers/hosting.js.html +10 -10
- package/coverage/lcov-report/api/controllers/index.html +83 -83
- package/coverage/lcov-report/api/controllers/integration.js.html +1 -1
- package/coverage/lcov-report/api/controllers/layout.js.html +3 -3
- package/coverage/lcov-report/api/controllers/metadata.js.html +3 -3
- package/coverage/lcov-report/api/controllers/permission.js.html +3 -3
- package/coverage/lcov-report/api/controllers/query.js.html +48 -210
- package/coverage/lcov-report/api/controllers/role.js.html +9 -9
- package/coverage/lcov-report/api/controllers/routing.js.html +3 -3
- package/coverage/lcov-report/api/controllers/row/external.js.html +2 -2
- package/coverage/lcov-report/api/controllers/row/index.html +10 -10
- package/coverage/lcov-report/api/controllers/row/index.js.html +2 -2
- package/coverage/lcov-report/api/controllers/row/internal.js.html +9 -9
- package/coverage/lcov-report/api/controllers/row/internalSearch.js.html +2 -2
- package/coverage/lcov-report/api/controllers/row/utils.js.html +2 -2
- package/coverage/lcov-report/api/controllers/screen.js.html +2 -2
- package/coverage/lcov-report/api/controllers/script.js.html +3 -3
- package/coverage/lcov-report/api/controllers/table/external.js.html +1 -1
- package/coverage/lcov-report/api/controllers/table/index.html +11 -11
- package/coverage/lcov-report/api/controllers/table/index.js.html +3 -3
- package/coverage/lcov-report/api/controllers/table/internal.js.html +2 -2
- package/coverage/lcov-report/api/controllers/table/utils.js.html +8 -8
- package/coverage/lcov-report/api/controllers/templates.js.html +4 -4
- package/coverage/lcov-report/api/controllers/user.js.html +33 -18
- package/coverage/lcov-report/api/controllers/view/exporters.js.html +1 -1
- package/coverage/lcov-report/api/controllers/view/index.html +9 -9
- package/coverage/lcov-report/api/controllers/view/index.js.html +5 -5
- package/coverage/lcov-report/api/controllers/view/utils.js.html +11 -11
- package/coverage/lcov-report/api/controllers/view/viewBuilder.js.html +3 -3
- package/coverage/lcov-report/api/controllers/webhook.js.html +4 -4
- package/coverage/lcov-report/api/index.html +1 -1
- package/coverage/lcov-report/api/index.js.html +1 -1
- package/coverage/lcov-report/api/routes/analytics.js.html +1 -1
- package/coverage/lcov-report/api/routes/apikeys.js.html +1 -1
- package/coverage/lcov-report/api/routes/application.js.html +1 -1
- package/coverage/lcov-report/api/routes/auth.js.html +1 -1
- package/coverage/lcov-report/api/routes/automation.js.html +1 -1
- package/coverage/lcov-report/api/routes/backup.js.html +1 -1
- package/coverage/lcov-report/api/routes/cloud.js.html +1 -1
- package/coverage/lcov-report/api/routes/component.js.html +1 -1
- package/coverage/lcov-report/api/routes/datasource.js.html +1 -1
- package/coverage/lcov-report/api/routes/deploy.js.html +1 -1
- package/coverage/lcov-report/api/routes/dev.js.html +2 -2
- package/coverage/lcov-report/api/routes/hosting.js.html +1 -1
- package/coverage/lcov-report/api/routes/index.html +4 -4
- package/coverage/lcov-report/api/routes/index.js.html +1 -1
- package/coverage/lcov-report/api/routes/integration.js.html +1 -1
- package/coverage/lcov-report/api/routes/layout.js.html +1 -1
- package/coverage/lcov-report/api/routes/metadata.js.html +1 -1
- package/coverage/lcov-report/api/routes/permission.js.html +1 -1
- package/coverage/lcov-report/api/routes/query.js.html +5 -2
- package/coverage/lcov-report/api/routes/role.js.html +1 -1
- package/coverage/lcov-report/api/routes/routing.js.html +1 -1
- package/coverage/lcov-report/api/routes/row.js.html +1 -1
- package/coverage/lcov-report/api/routes/screen.js.html +1 -1
- package/coverage/lcov-report/api/routes/script.js.html +1 -1
- package/coverage/lcov-report/api/routes/static.js.html +2 -2
- package/coverage/lcov-report/api/routes/table.js.html +1 -1
- package/coverage/lcov-report/api/routes/templates.js.html +1 -1
- package/coverage/lcov-report/api/routes/user.js.html +1 -1
- package/coverage/lcov-report/api/routes/view.js.html +1 -1
- package/coverage/lcov-report/api/routes/webhook.js.html +1 -1
- package/coverage/lcov-report/automations/actions.js.html +2 -2
- package/coverage/lcov-report/automations/automationUtils.js.html +3 -3
- package/coverage/lcov-report/automations/bullboard.js.html +71 -20
- package/coverage/lcov-report/automations/index.html +33 -48
- package/coverage/lcov-report/automations/index.js.html +2 -2
- package/coverage/lcov-report/automations/steps/bash.js.html +1 -1
- package/coverage/lcov-report/automations/steps/createRow.js.html +1 -1
- package/coverage/lcov-report/automations/steps/delay.js.html +1 -1
- package/coverage/lcov-report/automations/steps/deleteRow.js.html +1 -1
- package/coverage/lcov-report/automations/steps/discord.js.html +3 -3
- package/coverage/lcov-report/automations/steps/executeQuery.js.html +1 -1
- package/coverage/lcov-report/automations/steps/executeScript.js.html +3 -3
- package/coverage/lcov-report/automations/steps/filter.js.html +1 -1
- package/coverage/lcov-report/automations/steps/index.html +14 -14
- package/coverage/lcov-report/automations/steps/integromat.js.html +1 -1
- package/coverage/lcov-report/automations/steps/outgoingWebhook.js.html +3 -3
- package/coverage/lcov-report/automations/steps/queryRows.js.html +3 -3
- package/coverage/lcov-report/automations/steps/sendSmtpEmail.js.html +1 -1
- package/coverage/lcov-report/automations/steps/serverLog.js.html +1 -1
- package/coverage/lcov-report/automations/steps/slack.js.html +1 -1
- package/coverage/lcov-report/automations/steps/updateRow.js.html +1 -1
- package/coverage/lcov-report/automations/steps/utils.js.html +7 -7
- package/coverage/lcov-report/automations/steps/zapier.js.html +1 -1
- package/coverage/lcov-report/automations/triggerInfo/app.js.html +2 -2
- package/coverage/lcov-report/automations/triggerInfo/cron.js.html +2 -2
- package/coverage/lcov-report/automations/triggerInfo/index.html +1 -1
- package/coverage/lcov-report/automations/triggerInfo/index.js.html +8 -8
- package/coverage/lcov-report/automations/triggerInfo/rowDeleted.js.html +2 -2
- package/coverage/lcov-report/automations/triggerInfo/rowSaved.js.html +2 -2
- package/coverage/lcov-report/automations/triggerInfo/rowUpdated.js.html +2 -2
- package/coverage/lcov-report/automations/triggerInfo/webhook.js.html +2 -2
- package/coverage/lcov-report/automations/triggers.js.html +22 -10
- package/coverage/lcov-report/automations/utils.js.html +42 -12
- package/coverage/lcov-report/constants/index.html +5 -5
- package/coverage/lcov-report/constants/index.js.html +25 -4
- package/coverage/lcov-report/constants/layouts.js.html +1 -1
- package/coverage/lcov-report/constants/screens.js.html +1 -1
- package/coverage/lcov-report/db/client.js.html +2 -2
- package/coverage/lcov-report/db/inMemoryView.js.html +2 -2
- package/coverage/lcov-report/db/index.html +2 -2
- package/coverage/lcov-report/db/index.js.html +1 -1
- package/coverage/lcov-report/db/linkedRows/LinkController.js.html +6 -6
- package/coverage/lcov-report/db/linkedRows/index.html +10 -10
- package/coverage/lcov-report/db/linkedRows/index.js.html +5 -5
- package/coverage/lcov-report/db/linkedRows/linkUtils.js.html +6 -6
- package/coverage/lcov-report/db/newid.js.html +1 -1
- package/coverage/lcov-report/db/utils.js.html +1 -1
- package/coverage/lcov-report/events/AutomationEmitter.js.html +6 -6
- package/coverage/lcov-report/events/index.html +6 -6
- package/coverage/lcov-report/events/index.js.html +1 -1
- package/coverage/lcov-report/events/utils.js.html +5 -5
- package/coverage/lcov-report/index.html +83 -68
- package/coverage/lcov-report/integrations/tests/TestConfiguration.js.html +1 -1
- package/coverage/lcov-report/integrations/tests/index.html +1 -1
- package/coverage/lcov-report/middleware/appInfo.js.html +2 -2
- package/coverage/lcov-report/middleware/authorized.js.html +4 -4
- package/coverage/lcov-report/middleware/builder.js.html +16 -7
- package/coverage/lcov-report/middleware/currentapp.js.html +6 -6
- package/coverage/lcov-report/middleware/index.html +21 -21
- package/coverage/lcov-report/middleware/joi-validator.js.html +2 -2
- package/coverage/lcov-report/middleware/resourceId.js.html +18 -18
- package/coverage/lcov-report/middleware/selfhost.js.html +2 -2
- package/coverage/lcov-report/middleware/usageQuota.js.html +4 -4
- package/coverage/lcov-report/middleware/utils.js.html +1 -1
- package/coverage/lcov-report/{automations/thread.js.html → threads/automation.js.html} +83 -32
- package/coverage/lcov-report/threads/index.html +141 -0
- package/coverage/lcov-report/threads/index.js.html +260 -0
- package/coverage/lcov-report/threads/query.js.html +269 -0
- package/coverage/lcov-report/utilities/bcrypt.js.html +1 -1
- package/coverage/lcov-report/utilities/budibaseDir.js.html +1 -1
- package/coverage/lcov-report/utilities/centralPath.js.html +1 -1
- package/coverage/lcov-report/utilities/csvParser.js.html +2 -2
- package/coverage/lcov-report/utilities/global.js.html +2 -2
- package/coverage/lcov-report/utilities/index.html +21 -21
- package/coverage/lcov-report/utilities/index.js.html +12 -9
- package/coverage/lcov-report/utilities/queue/inMemoryQueue.js.html +30 -9
- package/coverage/lcov-report/utilities/queue/index.html +14 -14
- package/coverage/lcov-report/utilities/routing/index.html +1 -1
- package/coverage/lcov-report/utilities/routing/index.js.html +1 -1
- package/coverage/lcov-report/utilities/rowProcessor/index.html +4 -4
- package/coverage/lcov-report/utilities/rowProcessor/index.js.html +2 -2
- package/coverage/lcov-report/utilities/rowProcessor/utils.js.html +3 -3
- package/coverage/lcov-report/utilities/scriptRunner.js.html +16 -7
- package/coverage/lcov-report/utilities/security.js.html +3 -3
- package/coverage/lcov-report/utilities/statusCodes.js.html +1 -1
- package/coverage/lcov-report/utilities/users.js.html +1 -1
- package/coverage/lcov-report/utilities/workerRequests.js.html +5 -5
- package/coverage/lcov.info +1136 -972
- package/dist/api/controllers/application.js +11 -1
- package/dist/api/controllers/datasource.js +31 -7
- package/dist/api/controllers/deploy/index.js +2 -0
- package/dist/api/controllers/dev.js +4 -1
- package/dist/api/controllers/query.js +27 -57
- package/dist/api/controllers/user.js +10 -6
- package/dist/api/routes/query.js +1 -1
- package/dist/app.js +2 -11
- package/dist/automations/bullboard.js +37 -10
- package/dist/automations/triggers.js +7 -3
- package/dist/automations/utils.js +11 -2
- package/dist/constants/index.js +6 -0
- package/dist/integrations/base/IntegrationBase.js +2 -0
- package/dist/integrations/base/sql.js +36 -8
- package/dist/integrations/mysql.js +3 -1
- package/dist/integrations/utils.js +7 -1
- package/dist/middleware/builder.js +4 -1
- package/dist/package.json +7 -6
- package/dist/{automations/thread.js → threads/automation.js} +23 -7
- package/dist/threads/index.js +58 -0
- package/dist/threads/query.js +61 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utilities/index.js +1 -0
- package/dist/utilities/queue/inMemoryQueue.js +8 -0
- package/dist/utilities/scriptRunner.js +4 -1
- package/package.json +8 -7
- package/scripts/likeCypress.ts +33 -0
- package/src/api/controllers/application.js +11 -1
- package/src/api/controllers/datasource.js +45 -9
- package/src/api/controllers/deploy/index.js +2 -0
- package/src/api/controllers/dev.js +4 -1
- package/src/api/controllers/query.js +26 -80
- package/src/api/controllers/user.js +15 -10
- package/src/api/routes/query.js +1 -0
- package/src/app.ts +2 -10
- package/src/automations/bullboard.js +28 -11
- package/src/automations/tests/automation.spec.js +3 -4
- package/src/automations/triggers.js +7 -3
- package/src/automations/utils.js +12 -2
- package/src/constants/index.js +7 -0
- package/src/integrations/airtable.ts +2 -1
- package/src/integrations/arangodb.ts +2 -1
- package/src/integrations/base/IntegrationBase.ts +6 -0
- package/src/integrations/base/datasourcePlus.ts +2 -1
- package/src/integrations/base/sql.ts +38 -9
- package/src/integrations/couchdb.ts +2 -1
- package/src/integrations/dynamodb.ts +2 -1
- package/src/integrations/elasticsearch.ts +2 -1
- package/src/integrations/mongodb.ts +2 -1
- package/src/integrations/mysql.ts +1 -1
- package/src/integrations/rest.ts +2 -1
- package/src/integrations/s3.ts +2 -1
- package/src/integrations/utils.ts +6 -1
- package/src/middleware/builder.js +4 -1
- package/src/{automations/thread.js → threads/automation.js} +24 -7
- package/src/threads/index.js +60 -0
- package/src/threads/query.js +63 -0
- package/src/utilities/index.js +1 -0
- package/src/utilities/queue/inMemoryQueue.js +7 -0
- package/src/utilities/scriptRunner.js +4 -1
- package/tsconfig.json +1 -1
- package/builder/assets/index.4feb6e69.js +0 -201
- package/builder/assets/index.a79ec0ef.css +0 -29
- package/builder/assets/vendor.278a305c.js +0 -9
- package/builder/assets/vendor.d75d5eb7.css +0 -1
package/dist/utilities/index.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.objectStoreUrl = () => {
|
|
|
52
52
|
* via a specific endpoint (under /api/assets/client).
|
|
53
53
|
* @param {string} appId In production we need the appId to look up the correct bucket, as the
|
|
54
54
|
* version of the client lib may differ between apps.
|
|
55
|
+
* @param {string} version The version to retrieve.
|
|
55
56
|
* @return {string} The URL to be inserted into appPackage response or server rendered
|
|
56
57
|
* app index file.
|
|
57
58
|
*/
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const fetch = require("node-fetch");
|
|
3
3
|
const { VM, VMScript } = require("vm2");
|
|
4
|
+
const JS_TIMEOUT_MS = 1000;
|
|
4
5
|
class ScriptRunner {
|
|
5
6
|
constructor(script, context) {
|
|
6
7
|
const code = `let fn = () => {\n${script}\n}; results.out = fn();`;
|
|
7
|
-
this.vm = new VM(
|
|
8
|
+
this.vm = new VM({
|
|
9
|
+
timeout: JS_TIMEOUT_MS,
|
|
10
|
+
});
|
|
8
11
|
this.results = { out: "" };
|
|
9
12
|
this.vm.setGlobals(context);
|
|
10
13
|
this.vm.setGlobal("fetch", fetch);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@budibase/server",
|
|
3
3
|
"email": "hi@budibase.com",
|
|
4
|
-
"version": "0.9.185-alpha.
|
|
4
|
+
"version": "0.9.185-alpha.13",
|
|
5
5
|
"description": "Budibase Web Server",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"repository": {
|
|
@@ -68,9 +68,11 @@
|
|
|
68
68
|
"author": "Budibase",
|
|
69
69
|
"license": "AGPL-3.0-or-later",
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@budibase/auth": "^0.9.185-alpha.
|
|
72
|
-
"@budibase/client": "^0.9.185-alpha.
|
|
73
|
-
"@budibase/string-templates": "^0.9.185-alpha.
|
|
71
|
+
"@budibase/auth": "^0.9.185-alpha.13",
|
|
72
|
+
"@budibase/client": "^0.9.185-alpha.13",
|
|
73
|
+
"@budibase/string-templates": "^0.9.185-alpha.13",
|
|
74
|
+
"@bull-board/api": "^3.7.0",
|
|
75
|
+
"@bull-board/koa": "^3.7.0",
|
|
74
76
|
"@elastic/elasticsearch": "7.10.0",
|
|
75
77
|
"@koa/router": "8.0.0",
|
|
76
78
|
"@sendgrid/mail": "7.1.1",
|
|
@@ -80,7 +82,6 @@
|
|
|
80
82
|
"aws-sdk": "^2.767.0",
|
|
81
83
|
"bcryptjs": "2.4.3",
|
|
82
84
|
"bull": "^3.22.4",
|
|
83
|
-
"bull-board": "^2.0.1",
|
|
84
85
|
"chmodr": "1.2.0",
|
|
85
86
|
"csvtojson": "2.0.10",
|
|
86
87
|
"dotenv": "8.2.0",
|
|
@@ -120,6 +121,7 @@
|
|
|
120
121
|
"uuid": "3.3.2",
|
|
121
122
|
"validate.js": "0.13.1",
|
|
122
123
|
"vm2": "^3.9.3",
|
|
124
|
+
"worker-farm": "^1.7.0",
|
|
123
125
|
"yargs": "13.2.4",
|
|
124
126
|
"zlib": "1.0.5"
|
|
125
127
|
},
|
|
@@ -138,7 +140,6 @@
|
|
|
138
140
|
"copyfiles": "^2.4.1",
|
|
139
141
|
"docker-compose": "^0.23.6",
|
|
140
142
|
"eslint": "^6.8.0",
|
|
141
|
-
"express": "^4.17.1",
|
|
142
143
|
"jest": "^27.0.5",
|
|
143
144
|
"nodemon": "^2.0.4",
|
|
144
145
|
"prettier": "^2.3.1",
|
|
@@ -149,5 +150,5 @@
|
|
|
149
150
|
"typescript": "^4.3.5",
|
|
150
151
|
"update-dotenv": "^1.1.1"
|
|
151
152
|
},
|
|
152
|
-
"gitHead": "
|
|
153
|
+
"gitHead": "95fcaeea0609b133eecff0faae3e06bb3d4c4cfa"
|
|
153
154
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/******************************************************
|
|
2
|
+
* This script just makes it easy to re-create *
|
|
3
|
+
* a cypress like environment for testing the backend *
|
|
4
|
+
******************************************************/
|
|
5
|
+
const path = require("path")
|
|
6
|
+
const tmpdir = path.join(require("os").tmpdir(), ".budibase")
|
|
7
|
+
|
|
8
|
+
const MAIN_PORT = "10001"
|
|
9
|
+
const WORKER_PORT = "10002"
|
|
10
|
+
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
process.env.PORT = MAIN_PORT
|
|
13
|
+
process.env.BUDIBASE_API_KEY = "6BE826CB-6B30-4AEC-8777-2E90464633DE"
|
|
14
|
+
process.env.NODE_ENV = "cypress"
|
|
15
|
+
process.env.ENABLE_ANALYTICS = "false"
|
|
16
|
+
process.env.JWT_SECRET = "budibase"
|
|
17
|
+
process.env.COUCH_URL = `leveldb://${tmpdir}/.data/`
|
|
18
|
+
process.env.SELF_HOSTED = "1"
|
|
19
|
+
process.env.WORKER_URL = `http://localhost:${WORKER_PORT}/`
|
|
20
|
+
process.env.MINIO_URL = `http://localhost:${MAIN_PORT}/`
|
|
21
|
+
process.env.MINIO_ACCESS_KEY = "budibase"
|
|
22
|
+
process.env.MINIO_SECRET_KEY = "budibase"
|
|
23
|
+
process.env.COUCH_DB_USER = "budibase"
|
|
24
|
+
process.env.COUCH_DB_PASSWORD = "budibase"
|
|
25
|
+
process.env.INTERNAL_API_KEY = "budibase"
|
|
26
|
+
process.env.ALLOW_DEV_AUTOMATIONS = "1"
|
|
27
|
+
|
|
28
|
+
// don't make this a variable or top level require
|
|
29
|
+
// it will cause environment module to be loaded prematurely
|
|
30
|
+
const server = require("../src/app")
|
|
31
|
+
process.env.PORT = WORKER_PORT
|
|
32
|
+
const worker = require("../../worker/src/index")
|
|
33
|
+
process.env.PORT = MAIN_PORT
|
|
@@ -45,6 +45,8 @@ const {
|
|
|
45
45
|
} = require("../../utilities/fileSystem/clientLibrary")
|
|
46
46
|
const { getTenantId, isMultiTenant } = require("@budibase/auth/tenancy")
|
|
47
47
|
const { syncGlobalUsers } = require("./user")
|
|
48
|
+
const { app: appCache } = require("@budibase/auth/cache")
|
|
49
|
+
const { cleanupAutomations } = require("../../automations/utils")
|
|
48
50
|
|
|
49
51
|
const URL_REGEX_SLASH = /\/|\\/g
|
|
50
52
|
|
|
@@ -254,6 +256,7 @@ exports.create = async ctx => {
|
|
|
254
256
|
await createApp(appId)
|
|
255
257
|
}
|
|
256
258
|
|
|
259
|
+
await appCache.invalidateAppMetadata(appId, newApplication)
|
|
257
260
|
ctx.status = 200
|
|
258
261
|
ctx.body = newApplication
|
|
259
262
|
}
|
|
@@ -317,8 +320,12 @@ exports.delete = async ctx => {
|
|
|
317
320
|
if (!env.isTest() && !ctx.query.unpublish) {
|
|
318
321
|
await deleteApp(ctx.params.appId)
|
|
319
322
|
}
|
|
323
|
+
if (ctx.query && ctx.query.unpublish) {
|
|
324
|
+
await cleanupAutomations(ctx.params.appId)
|
|
325
|
+
}
|
|
320
326
|
// make sure the app/role doesn't stick around after the app has been deleted
|
|
321
327
|
await removeAppFromUserRoles(ctx, ctx.params.appId)
|
|
328
|
+
await appCache.invalidateAppMetadata(ctx.params.appId)
|
|
322
329
|
|
|
323
330
|
ctx.status = 200
|
|
324
331
|
ctx.body = result
|
|
@@ -387,7 +394,10 @@ const updateAppPackage = async (ctx, appPackage, appId) => {
|
|
|
387
394
|
// Redis, shouldn't ever store it
|
|
388
395
|
delete newAppPackage.lockedBy
|
|
389
396
|
|
|
390
|
-
|
|
397
|
+
const response = await db.put(newAppPackage)
|
|
398
|
+
// remove any cached metadata, so that it will be updated
|
|
399
|
+
await appCache.invalidateAppMetadata(appId)
|
|
400
|
+
return response
|
|
391
401
|
}
|
|
392
402
|
|
|
393
403
|
const createEmptyAppPackage = async (ctx, app) => {
|
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
BudibaseInternalDB,
|
|
8
8
|
getTableParams,
|
|
9
9
|
} = require("../../db/utils")
|
|
10
|
-
const { BuildSchemaErrors } = require("../../constants")
|
|
10
|
+
const { BuildSchemaErrors, InvalidColumns } = require("../../constants")
|
|
11
11
|
const { integrations } = require("../../integrations")
|
|
12
12
|
const { getDatasourceAndQuery } = require("./row/utils")
|
|
13
13
|
|
|
@@ -119,8 +119,16 @@ exports.destroy = async function (ctx) {
|
|
|
119
119
|
const db = new CouchDB(ctx.appId)
|
|
120
120
|
|
|
121
121
|
// Delete all queries for the datasource
|
|
122
|
-
const
|
|
123
|
-
|
|
122
|
+
const queries = await db.allDocs(
|
|
123
|
+
getQueryParams(ctx.params.datasourceId, null)
|
|
124
|
+
)
|
|
125
|
+
await db.bulkDocs(
|
|
126
|
+
queries.rows.map(row => ({
|
|
127
|
+
_id: row.id,
|
|
128
|
+
_rev: row.value.rev,
|
|
129
|
+
_deleted: true,
|
|
130
|
+
}))
|
|
131
|
+
)
|
|
124
132
|
|
|
125
133
|
// delete the datasource
|
|
126
134
|
await db.remove(ctx.params.datasourceId, ctx.params.revId)
|
|
@@ -144,6 +152,23 @@ exports.query = async function (ctx) {
|
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
function getErrorTables(errors, errorType) {
|
|
156
|
+
return Object.entries(errors)
|
|
157
|
+
.filter(entry => entry[1] === errorType)
|
|
158
|
+
.map(([name]) => name)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function updateError(error, newError, tables) {
|
|
162
|
+
if (!error) {
|
|
163
|
+
error = ""
|
|
164
|
+
}
|
|
165
|
+
if (error.length > 0) {
|
|
166
|
+
error += "\n"
|
|
167
|
+
}
|
|
168
|
+
error += `${newError} ${tables.join(", ")}`
|
|
169
|
+
return error
|
|
170
|
+
}
|
|
171
|
+
|
|
147
172
|
const buildSchemaHelper = async datasource => {
|
|
148
173
|
const Connector = integrations[datasource.source]
|
|
149
174
|
|
|
@@ -168,12 +193,23 @@ const buildSchemaHelper = async datasource => {
|
|
|
168
193
|
const errors = connector.schemaErrors
|
|
169
194
|
let error = null
|
|
170
195
|
if (errors && Object.keys(errors).length > 0) {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
const noKey = getErrorTables(errors, BuildSchemaErrors.NO_KEY)
|
|
197
|
+
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
|
|
198
|
+
if (noKey.length) {
|
|
199
|
+
error = updateError(
|
|
200
|
+
error,
|
|
201
|
+
"No primary key constraint found for the following:",
|
|
202
|
+
noKey
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
if (invalidCol.length) {
|
|
206
|
+
const invalidCols = Object.values(InvalidColumns).join(", ")
|
|
207
|
+
error = updateError(
|
|
208
|
+
error,
|
|
209
|
+
`Cannot use columns ${invalidCols} found in following:`,
|
|
210
|
+
invalidCol
|
|
211
|
+
)
|
|
212
|
+
}
|
|
177
213
|
}
|
|
178
214
|
return { tables: connector.tables, error }
|
|
179
215
|
}
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
disableAllCrons,
|
|
7
7
|
enableCronTrigger,
|
|
8
8
|
} = require("../../../automations/utils")
|
|
9
|
+
const { app: appCache } = require("@budibase/auth/cache")
|
|
9
10
|
|
|
10
11
|
// the max time we can wait for an invalidation to complete before considering it failed
|
|
11
12
|
const MAX_PENDING_TIME_MS = 30 * 60000
|
|
@@ -103,6 +104,7 @@ async function deployApp(deployment) {
|
|
|
103
104
|
appDoc.appId = productionAppId
|
|
104
105
|
appDoc.instance._id = productionAppId
|
|
105
106
|
await db.put(appDoc)
|
|
107
|
+
await appCache.invalidateAppMetadata(productionAppId)
|
|
106
108
|
console.log("New app doc written successfully.")
|
|
107
109
|
await initDeployedApp(productionAppId)
|
|
108
110
|
console.log("Deployed app initialised, setting deployment to successful")
|
|
@@ -6,6 +6,7 @@ const { request } = require("../../utilities/workerRequests")
|
|
|
6
6
|
const { clearLock } = require("../../utilities/redis")
|
|
7
7
|
const { Replication } = require("@budibase/auth").db
|
|
8
8
|
const { DocumentTypes } = require("../../db/utils")
|
|
9
|
+
const { app: appCache } = require("@budibase/auth/cache")
|
|
9
10
|
|
|
10
11
|
async function redirect(ctx, method, path = "global") {
|
|
11
12
|
const { devPath } = ctx.params
|
|
@@ -24,7 +25,8 @@ async function redirect(ctx, method, path = "global") {
|
|
|
24
25
|
)
|
|
25
26
|
)
|
|
26
27
|
if (response.status !== 200) {
|
|
27
|
-
|
|
28
|
+
const err = await response.text()
|
|
29
|
+
ctx.throw(400, err)
|
|
28
30
|
}
|
|
29
31
|
const cookie = response.headers.get("set-cookie")
|
|
30
32
|
if (cookie) {
|
|
@@ -106,6 +108,7 @@ exports.revert = async ctx => {
|
|
|
106
108
|
appDoc.appId = appId
|
|
107
109
|
appDoc.instance._id = appId
|
|
108
110
|
await db.put(appDoc)
|
|
111
|
+
await appCache.invalidateAppMetadata(appId)
|
|
109
112
|
ctx.body = {
|
|
110
113
|
message: "Reverted changes successfully.",
|
|
111
114
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const { processString } = require("@budibase/string-templates")
|
|
2
2
|
const CouchDB = require("../../db")
|
|
3
3
|
const { generateQueryID, getQueryParams } = require("../../db/utils")
|
|
4
|
-
const { integrations } = require("../../integrations")
|
|
5
4
|
const { BaseQueryVerbs } = require("../../constants")
|
|
6
5
|
const env = require("../../environment")
|
|
7
|
-
const
|
|
6
|
+
const { Thread, ThreadType } = require("../../threads")
|
|
7
|
+
|
|
8
|
+
const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 })
|
|
8
9
|
|
|
9
10
|
// simple function to append "readable" to all read queries
|
|
10
11
|
function enrichQueries(input) {
|
|
@@ -18,47 +19,6 @@ function enrichQueries(input) {
|
|
|
18
19
|
return wasArray ? queries : queries[0]
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function formatResponse(resp) {
|
|
22
|
-
if (typeof resp === "string") {
|
|
23
|
-
try {
|
|
24
|
-
resp = JSON.parse(resp)
|
|
25
|
-
} catch (err) {
|
|
26
|
-
resp = { response: resp }
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return resp
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function runAndTransform(
|
|
33
|
-
integration,
|
|
34
|
-
queryVerb,
|
|
35
|
-
enrichedQuery,
|
|
36
|
-
transformer
|
|
37
|
-
) {
|
|
38
|
-
let rows = formatResponse(await integration[queryVerb](enrichedQuery))
|
|
39
|
-
|
|
40
|
-
// transform as required
|
|
41
|
-
if (transformer) {
|
|
42
|
-
const runner = new ScriptRunner(transformer, { data: rows })
|
|
43
|
-
rows = runner.execute()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// needs to an array for next step
|
|
47
|
-
if (!Array.isArray(rows)) {
|
|
48
|
-
rows = [rows]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// map into JSON if just raw primitive here
|
|
52
|
-
if (rows.find(row => typeof row !== "object")) {
|
|
53
|
-
rows = rows.map(value => ({ value }))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// get all the potential fields in the schema
|
|
57
|
-
let keys = rows.flatMap(Object.keys)
|
|
58
|
-
|
|
59
|
-
return { rows, keys }
|
|
60
|
-
}
|
|
61
|
-
|
|
62
22
|
exports.fetch = async function (ctx) {
|
|
63
23
|
const db = new CouchDB(ctx.appId)
|
|
64
24
|
|
|
@@ -143,30 +103,23 @@ exports.preview = async function (ctx) {
|
|
|
143
103
|
|
|
144
104
|
const datasource = await db.get(ctx.request.body.datasourceId)
|
|
145
105
|
|
|
146
|
-
const Integration = integrations[datasource.source]
|
|
147
|
-
|
|
148
|
-
if (!Integration) {
|
|
149
|
-
ctx.throw(400, "Integration type does not exist.")
|
|
150
|
-
}
|
|
151
|
-
|
|
152
106
|
const { fields, parameters, queryVerb, transformer } = ctx.request.body
|
|
153
107
|
const enrichedQuery = await enrichQueryFields(fields, parameters)
|
|
154
|
-
const integration = new Integration(datasource.config)
|
|
155
108
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
109
|
+
try {
|
|
110
|
+
const { rows, keys } = await Runner.run({
|
|
111
|
+
datasource,
|
|
112
|
+
queryVerb,
|
|
113
|
+
query: enrichedQuery,
|
|
114
|
+
transformer,
|
|
115
|
+
})
|
|
162
116
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
integration.end()
|
|
117
|
+
ctx.body = {
|
|
118
|
+
rows,
|
|
119
|
+
schemaFields: [...new Set(keys)],
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
ctx.throw(400, err)
|
|
170
123
|
}
|
|
171
124
|
}
|
|
172
125
|
|
|
@@ -176,29 +129,22 @@ exports.execute = async function (ctx) {
|
|
|
176
129
|
const query = await db.get(ctx.params.queryId)
|
|
177
130
|
const datasource = await db.get(query.datasourceId)
|
|
178
131
|
|
|
179
|
-
const Integration = integrations[datasource.source]
|
|
180
|
-
|
|
181
|
-
if (!Integration) {
|
|
182
|
-
ctx.throw(400, "Integration type does not exist.")
|
|
183
|
-
}
|
|
184
|
-
|
|
185
132
|
const enrichedQuery = await enrichQueryFields(
|
|
186
133
|
query.fields,
|
|
187
134
|
ctx.request.body.parameters
|
|
188
135
|
)
|
|
189
|
-
const integration = new Integration(datasource.config)
|
|
190
136
|
|
|
191
137
|
// call the relevant CRUD method on the integration class
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
138
|
+
try {
|
|
139
|
+
const { rows } = await Runner.run({
|
|
140
|
+
datasource,
|
|
141
|
+
queryVerb: query.queryVerb,
|
|
142
|
+
query: enrichedQuery,
|
|
143
|
+
transformer: query.transformer,
|
|
144
|
+
})
|
|
145
|
+
ctx.body = rows
|
|
146
|
+
} catch (err) {
|
|
147
|
+
ctx.throw(400, err)
|
|
202
148
|
}
|
|
203
149
|
}
|
|
204
150
|
|
|
@@ -97,6 +97,7 @@ exports.syncUser = async function (ctx) {
|
|
|
97
97
|
.map(([appId]) => appId)
|
|
98
98
|
}
|
|
99
99
|
for (let prodAppId of prodAppIds) {
|
|
100
|
+
const roleId = roles[prodAppId]
|
|
100
101
|
const devAppId = getDevelopmentAppID(prodAppId)
|
|
101
102
|
for (let appId of [prodAppId, devAppId]) {
|
|
102
103
|
if (!(await doesDatabaseExist(appId))) {
|
|
@@ -115,17 +116,21 @@ exports.syncUser = async function (ctx) {
|
|
|
115
116
|
tableId: InternalTables.USER_METADATA,
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
// assign the roleId for the metadata doc
|
|
120
|
+
if (roleId) {
|
|
121
|
+
metadata.roleId = roleId
|
|
122
|
+
}
|
|
123
|
+
let combined = !deleting
|
|
124
|
+
? combineMetadataAndUser(user, metadata)
|
|
125
|
+
: {
|
|
126
|
+
...metadata,
|
|
127
|
+
status: UserStatus.INACTIVE,
|
|
128
|
+
metadata: BUILTIN_ROLE_IDS.PUBLIC,
|
|
129
|
+
}
|
|
130
|
+
// if its null then there was no updates required
|
|
131
|
+
if (combined) {
|
|
132
|
+
await db.put(combined)
|
|
127
133
|
}
|
|
128
|
-
await db.put(combined)
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
ctx.body = {
|
package/src/api/routes/query.js
CHANGED
package/src/app.ts
CHANGED
|
@@ -41,16 +41,8 @@ app.use(
|
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
if (!env.isTest()) {
|
|
44
|
-
const
|
|
45
|
-
app.use(
|
|
46
|
-
if (ctx.path.startsWith(bullboard.pathPrefix)) {
|
|
47
|
-
ctx.status = 200
|
|
48
|
-
ctx.respond = false
|
|
49
|
-
bullApp(ctx.req, ctx.res)
|
|
50
|
-
} else {
|
|
51
|
-
await next()
|
|
52
|
-
}
|
|
53
|
-
})
|
|
44
|
+
const plugin = bullboard.init()
|
|
45
|
+
app.use(plugin)
|
|
54
46
|
}
|
|
55
47
|
|
|
56
48
|
app.context.eventEmitter = eventEmitter
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const { createBullBoard } = require("bull-board")
|
|
2
|
-
const { BullAdapter } = require("bull-board/bullAdapter")
|
|
3
|
-
const
|
|
1
|
+
const { createBullBoard } = require("@bull-board/api")
|
|
2
|
+
const { BullAdapter } = require("@bull-board/api/bullAdapter")
|
|
3
|
+
const { KoaAdapter } = require("@bull-board/koa")
|
|
4
4
|
const env = require("../environment")
|
|
5
5
|
const Queue = env.isTest()
|
|
6
6
|
? require("../utilities/queue/inMemoryQueue")
|
|
@@ -9,23 +9,40 @@ const { JobQueues } = require("../constants")
|
|
|
9
9
|
const { utils } = require("@budibase/auth/redis")
|
|
10
10
|
const { opts, redisProtocolUrl } = utils.getRedisOptions()
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
|
|
12
|
+
const CLEANUP_PERIOD_MS = 60 * 1000
|
|
13
|
+
const queueConfig = redisProtocolUrl || { redis: opts }
|
|
14
|
+
let cleanupInternal = null
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
let automationQueue = new Queue(JobQueues.AUTOMATIONS, queueConfig)
|
|
17
|
+
|
|
18
|
+
async function cleanup() {
|
|
19
|
+
await automationQueue.clean(CLEANUP_PERIOD_MS, "completed")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PATH_PREFIX = "/bulladmin"
|
|
16
23
|
|
|
17
24
|
exports.init = () => {
|
|
18
|
-
|
|
25
|
+
// cleanup the events every 5 minutes
|
|
26
|
+
if (!cleanupInternal) {
|
|
27
|
+
cleanupInternal = setInterval(cleanup, CLEANUP_PERIOD_MS)
|
|
28
|
+
// fire off an initial cleanup
|
|
29
|
+
cleanup().catch(err => {
|
|
30
|
+
console.error(`Unable to cleanup automation queue initially - ${err}`)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
19
33
|
// Set up queues for bull board admin
|
|
20
34
|
const queues = [automationQueue]
|
|
21
35
|
const adapters = []
|
|
36
|
+
const serverAdapter = new KoaAdapter()
|
|
22
37
|
for (let queue of queues) {
|
|
23
38
|
adapters.push(new BullAdapter(queue))
|
|
24
39
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
createBullBoard({
|
|
41
|
+
queues: adapters,
|
|
42
|
+
serverAdapter,
|
|
43
|
+
})
|
|
44
|
+
serverAdapter.setBasePath(PATH_PREFIX)
|
|
45
|
+
return serverAdapter.registerPlugin()
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
exports.queue = automationQueue
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
jest.mock("../../utilities/usageQuota")
|
|
2
|
-
jest.mock("
|
|
2
|
+
jest.mock("../../threads/automation")
|
|
3
3
|
jest.mock("../../utilities/redis", () => ({
|
|
4
4
|
init: jest.fn(),
|
|
5
5
|
checkTestFlag: () => {
|
|
@@ -11,8 +11,7 @@ jest.spyOn(global.console, "error")
|
|
|
11
11
|
|
|
12
12
|
require("../../environment")
|
|
13
13
|
const automation = require("../index")
|
|
14
|
-
const
|
|
15
|
-
const thread = require("../thread")
|
|
14
|
+
const thread = require("../../threads/automation")
|
|
16
15
|
const triggers = require("../triggers")
|
|
17
16
|
const { basicAutomation } = require("../../tests/utilities/structures")
|
|
18
17
|
const { wait } = require("../../utilities")
|
|
@@ -62,7 +61,7 @@ describe("Run through some parts of the automations system", () => {
|
|
|
62
61
|
}
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
|
-
}))
|
|
64
|
+
}), expect.any(Function))
|
|
66
65
|
})
|
|
67
66
|
|
|
68
67
|
it("should be able to clean inputs with the utilities", () => {
|
|
@@ -11,6 +11,10 @@ const utils = require("./utils")
|
|
|
11
11
|
const env = require("../environment")
|
|
12
12
|
|
|
13
13
|
const TRIGGER_DEFINITIONS = definitions
|
|
14
|
+
const JOB_OPTS = {
|
|
15
|
+
removeOnComplete: true,
|
|
16
|
+
removeOnFail: true,
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
async function queueRelevantRowAutomations(event, eventType) {
|
|
16
20
|
if (event.appId == null) {
|
|
@@ -47,7 +51,7 @@ async function queueRelevantRowAutomations(event, eventType) {
|
|
|
47
51
|
automationTrigger.inputs &&
|
|
48
52
|
automationTrigger.inputs.tableId === event.row.tableId
|
|
49
53
|
) {
|
|
50
|
-
await queue.add({ automation, event })
|
|
54
|
+
await queue.add({ automation, event }, JOB_OPTS)
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
}
|
|
@@ -86,7 +90,7 @@ exports.externalTrigger = async function (
|
|
|
86
90
|
automation.definition.trigger != null &&
|
|
87
91
|
automation.definition.trigger.stepId === definitions.APP.stepId &&
|
|
88
92
|
automation.definition.trigger.stepId === "APP" &&
|
|
89
|
-
!checkTestFlag(automation._id)
|
|
93
|
+
!(await checkTestFlag(automation._id))
|
|
90
94
|
) {
|
|
91
95
|
// values are likely to be submitted as strings, so we shall convert to correct type
|
|
92
96
|
const coercedFields = {}
|
|
@@ -100,7 +104,7 @@ exports.externalTrigger = async function (
|
|
|
100
104
|
if (getResponses) {
|
|
101
105
|
return utils.processEvent({ data })
|
|
102
106
|
} else {
|
|
103
|
-
return queue.add(data)
|
|
107
|
+
return queue.add(data, JOB_OPTS)
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
package/src/automations/utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const { Thread, ThreadType } = require("../threads")
|
|
2
2
|
const { definitions } = require("./triggerInfo")
|
|
3
3
|
const webhooks = require("../api/controllers/webhook")
|
|
4
4
|
const CouchDB = require("../db")
|
|
@@ -10,11 +10,12 @@ const { getDeployedAppID } = require("@budibase/auth/db")
|
|
|
10
10
|
|
|
11
11
|
const WH_STEP_ID = definitions.WEBHOOK.stepId
|
|
12
12
|
const CRON_STEP_ID = definitions.CRON.stepId
|
|
13
|
+
const Runner = new Thread(ThreadType.AUTOMATION)
|
|
13
14
|
|
|
14
15
|
exports.processEvent = async job => {
|
|
15
16
|
try {
|
|
16
17
|
// need to actually await these so that an error can be captured properly
|
|
17
|
-
return await
|
|
18
|
+
return await Runner.run(job)
|
|
18
19
|
} catch (err) {
|
|
19
20
|
console.error(
|
|
20
21
|
`${job.data.automation.appId} automation ${job.data.automation._id} was unable to run - ${err}`
|
|
@@ -162,3 +163,12 @@ exports.checkForWebhooks = async ({ appId, oldAuto, newAuto }) => {
|
|
|
162
163
|
}
|
|
163
164
|
return newAuto
|
|
164
165
|
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* When removing an app/unpublishing it need to make sure automations are cleaned up (cron).
|
|
169
|
+
* @param appId {string} the app that is being removed.
|
|
170
|
+
* @return {Promise<void>} clean is complete if this succeeds.
|
|
171
|
+
*/
|
|
172
|
+
exports.cleanupAutomations = async appId => {
|
|
173
|
+
await exports.disableAllCrons(appId)
|
|
174
|
+
}
|