@budibase/server 1.0.27-alpha.1 → 1.0.27-alpha.10

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.
Files changed (212) hide show
  1. package/__mocks__/node-fetch.ts +18 -0
  2. package/builder/assets/{index.4a13c4cb.css → index.0dce895b.css} +1 -1
  3. package/builder/assets/index.39751e34.js +1348 -0
  4. package/builder/index.html +2 -2
  5. package/coverage/clover.xml +787 -637
  6. package/coverage/coverage-final.json +27 -27
  7. package/coverage/lcov-report/api/controllers/analytics.js.html +1 -1
  8. package/coverage/lcov-report/api/controllers/apikeys.js.html +1 -1
  9. package/coverage/lcov-report/api/controllers/application.js.html +54 -54
  10. package/coverage/lcov-report/api/controllers/auth.js.html +1 -1
  11. package/coverage/lcov-report/api/controllers/automation.js.html +1 -1
  12. package/coverage/lcov-report/api/controllers/backup.js.html +1 -1
  13. package/coverage/lcov-report/api/controllers/cloud.js.html +1 -1
  14. package/coverage/lcov-report/api/controllers/component.js.html +4 -4
  15. package/coverage/lcov-report/api/controllers/datasource.js.html +49 -49
  16. package/coverage/lcov-report/api/controllers/dev.js.html +1 -1
  17. package/coverage/lcov-report/api/controllers/hosting.js.html +1 -1
  18. package/coverage/lcov-report/api/controllers/index.html +20 -20
  19. package/coverage/lcov-report/api/controllers/integration.js.html +1 -1
  20. package/coverage/lcov-report/api/controllers/layout.js.html +1 -1
  21. package/coverage/lcov-report/api/controllers/metadata.js.html +1 -1
  22. package/coverage/lcov-report/api/controllers/permission.js.html +1 -1
  23. package/coverage/lcov-report/api/controllers/query/index.html +19 -19
  24. package/coverage/lcov-report/api/controllers/query/index.js.html +100 -121
  25. package/coverage/lcov-report/api/controllers/query/validation.js.html +6 -3
  26. package/coverage/lcov-report/api/controllers/role.js.html +1 -1
  27. package/coverage/lcov-report/api/controllers/routing.js.html +1 -1
  28. package/coverage/lcov-report/api/controllers/row/external.js.html +1 -1
  29. package/coverage/lcov-report/api/controllers/row/index.html +22 -22
  30. package/coverage/lcov-report/api/controllers/row/index.js.html +1 -1
  31. package/coverage/lcov-report/api/controllers/row/internal.js.html +38 -11
  32. package/coverage/lcov-report/api/controllers/row/internalSearch.js.html +7 -4
  33. package/coverage/lcov-report/api/controllers/row/utils.js.html +29 -20
  34. package/coverage/lcov-report/api/controllers/screen.js.html +1 -1
  35. package/coverage/lcov-report/api/controllers/script.js.html +1 -1
  36. package/coverage/lcov-report/api/controllers/table/external.js.html +1 -1
  37. package/coverage/lcov-report/api/controllers/table/index.html +1 -1
  38. package/coverage/lcov-report/api/controllers/table/index.js.html +1 -1
  39. package/coverage/lcov-report/api/controllers/table/internal.js.html +1 -1
  40. package/coverage/lcov-report/api/controllers/table/utils.js.html +1 -1
  41. package/coverage/lcov-report/api/controllers/templates.js.html +1 -1
  42. package/coverage/lcov-report/api/controllers/user.js.html +1 -1
  43. package/coverage/lcov-report/api/controllers/view/exporters.js.html +1 -1
  44. package/coverage/lcov-report/api/controllers/view/index.html +1 -1
  45. package/coverage/lcov-report/api/controllers/view/index.js.html +1 -1
  46. package/coverage/lcov-report/api/controllers/view/utils.js.html +1 -1
  47. package/coverage/lcov-report/api/controllers/view/viewBuilder.js.html +1 -1
  48. package/coverage/lcov-report/api/controllers/webhook.js.html +1 -1
  49. package/coverage/lcov-report/api/index.html +1 -1
  50. package/coverage/lcov-report/api/index.js.html +5 -5
  51. package/coverage/lcov-report/api/routes/analytics.js.html +1 -1
  52. package/coverage/lcov-report/api/routes/apikeys.js.html +1 -1
  53. package/coverage/lcov-report/api/routes/application.js.html +1 -1
  54. package/coverage/lcov-report/api/routes/auth.js.html +1 -1
  55. package/coverage/lcov-report/api/routes/automation.js.html +1 -1
  56. package/coverage/lcov-report/api/routes/backup.js.html +1 -1
  57. package/coverage/lcov-report/api/routes/cloud.js.html +1 -1
  58. package/coverage/lcov-report/api/routes/component.js.html +1 -1
  59. package/coverage/lcov-report/api/routes/datasource.js.html +1 -1
  60. package/coverage/lcov-report/api/routes/deploy.js.html +1 -1
  61. package/coverage/lcov-report/api/routes/dev.js.html +1 -1
  62. package/coverage/lcov-report/api/routes/hosting.js.html +1 -1
  63. package/coverage/lcov-report/api/routes/index.html +1 -1
  64. package/coverage/lcov-report/api/routes/index.js.html +1 -1
  65. package/coverage/lcov-report/api/routes/integration.js.html +1 -1
  66. package/coverage/lcov-report/api/routes/layout.js.html +1 -1
  67. package/coverage/lcov-report/api/routes/metadata.js.html +1 -1
  68. package/coverage/lcov-report/api/routes/permission.js.html +1 -1
  69. package/coverage/lcov-report/api/routes/query.js.html +24 -3
  70. package/coverage/lcov-report/api/routes/role.js.html +1 -1
  71. package/coverage/lcov-report/api/routes/routing.js.html +1 -1
  72. package/coverage/lcov-report/api/routes/row.js.html +1 -1
  73. package/coverage/lcov-report/api/routes/screen.js.html +1 -1
  74. package/coverage/lcov-report/api/routes/script.js.html +1 -1
  75. package/coverage/lcov-report/api/routes/static.js.html +1 -1
  76. package/coverage/lcov-report/api/routes/table.js.html +1 -1
  77. package/coverage/lcov-report/api/routes/templates.js.html +1 -1
  78. package/coverage/lcov-report/api/routes/user.js.html +1 -1
  79. package/coverage/lcov-report/api/routes/view.js.html +1 -1
  80. package/coverage/lcov-report/api/routes/webhook.js.html +1 -1
  81. package/coverage/lcov-report/automations/actions.js.html +1 -1
  82. package/coverage/lcov-report/automations/automationUtils.js.html +1 -1
  83. package/coverage/lcov-report/automations/bullboard.js.html +1 -1
  84. package/coverage/lcov-report/automations/index.html +1 -1
  85. package/coverage/lcov-report/automations/index.js.html +1 -1
  86. package/coverage/lcov-report/automations/steps/bash.js.html +1 -1
  87. package/coverage/lcov-report/automations/steps/createRow.js.html +1 -1
  88. package/coverage/lcov-report/automations/steps/delay.js.html +1 -1
  89. package/coverage/lcov-report/automations/steps/deleteRow.js.html +1 -1
  90. package/coverage/lcov-report/automations/steps/discord.js.html +1 -1
  91. package/coverage/lcov-report/automations/steps/executeQuery.js.html +31 -7
  92. package/coverage/lcov-report/automations/steps/executeScript.js.html +1 -1
  93. package/coverage/lcov-report/automations/steps/filter.js.html +1 -1
  94. package/coverage/lcov-report/automations/steps/index.html +11 -11
  95. package/coverage/lcov-report/automations/steps/integromat.js.html +1 -1
  96. package/coverage/lcov-report/automations/steps/outgoingWebhook.js.html +1 -1
  97. package/coverage/lcov-report/automations/steps/queryRows.js.html +1 -1
  98. package/coverage/lcov-report/automations/steps/sendSmtpEmail.js.html +1 -1
  99. package/coverage/lcov-report/automations/steps/serverLog.js.html +1 -1
  100. package/coverage/lcov-report/automations/steps/slack.js.html +1 -1
  101. package/coverage/lcov-report/automations/steps/updateRow.js.html +1 -1
  102. package/coverage/lcov-report/automations/steps/utils.js.html +1 -1
  103. package/coverage/lcov-report/automations/steps/zapier.js.html +1 -1
  104. package/coverage/lcov-report/automations/triggerInfo/app.js.html +1 -1
  105. package/coverage/lcov-report/automations/triggerInfo/cron.js.html +1 -1
  106. package/coverage/lcov-report/automations/triggerInfo/index.html +1 -1
  107. package/coverage/lcov-report/automations/triggerInfo/index.js.html +1 -1
  108. package/coverage/lcov-report/automations/triggerInfo/rowDeleted.js.html +1 -1
  109. package/coverage/lcov-report/automations/triggerInfo/rowSaved.js.html +1 -1
  110. package/coverage/lcov-report/automations/triggerInfo/rowUpdated.js.html +1 -1
  111. package/coverage/lcov-report/automations/triggerInfo/webhook.js.html +1 -1
  112. package/coverage/lcov-report/automations/triggers.js.html +1 -1
  113. package/coverage/lcov-report/automations/utils.js.html +5 -5
  114. package/coverage/lcov-report/constants/index.html +1 -1
  115. package/coverage/lcov-report/constants/index.js.html +1 -1
  116. package/coverage/lcov-report/constants/layouts.js.html +1 -1
  117. package/coverage/lcov-report/constants/screens.js.html +1 -1
  118. package/coverage/lcov-report/db/client.js.html +1 -1
  119. package/coverage/lcov-report/db/inMemoryView.js.html +1 -1
  120. package/coverage/lcov-report/db/index.html +1 -1
  121. package/coverage/lcov-report/db/index.js.html +1 -1
  122. package/coverage/lcov-report/db/linkedRows/LinkController.js.html +1 -1
  123. package/coverage/lcov-report/db/linkedRows/index.html +1 -1
  124. package/coverage/lcov-report/db/linkedRows/index.js.html +1 -1
  125. package/coverage/lcov-report/db/linkedRows/linkUtils.js.html +1 -1
  126. package/coverage/lcov-report/db/newid.js.html +2 -2
  127. package/coverage/lcov-report/db/utils.js.html +18 -18
  128. package/coverage/lcov-report/events/AutomationEmitter.js.html +1 -1
  129. package/coverage/lcov-report/events/index.html +1 -1
  130. package/coverage/lcov-report/events/index.js.html +1 -1
  131. package/coverage/lcov-report/events/utils.js.html +1 -1
  132. package/coverage/lcov-report/index.html +63 -63
  133. package/coverage/lcov-report/integrations/tests/TestConfiguration.js.html +1 -1
  134. package/coverage/lcov-report/integrations/tests/index.html +1 -1
  135. package/coverage/lcov-report/middleware/appInfo.js.html +1 -1
  136. package/coverage/lcov-report/middleware/authorized.js.html +10 -10
  137. package/coverage/lcov-report/middleware/builder.js.html +8 -8
  138. package/coverage/lcov-report/middleware/currentapp.js.html +28 -28
  139. package/coverage/lcov-report/middleware/index.html +1 -1
  140. package/coverage/lcov-report/middleware/joi-validator.js.html +9 -9
  141. package/coverage/lcov-report/middleware/resourceId.js.html +17 -17
  142. package/coverage/lcov-report/middleware/selfhost.js.html +1 -1
  143. package/coverage/lcov-report/middleware/usageQuota.js.html +1 -1
  144. package/coverage/lcov-report/middleware/utils.js.html +2 -2
  145. package/coverage/lcov-report/threads/automation.js.html +1 -1
  146. package/coverage/lcov-report/threads/index.html +31 -31
  147. package/coverage/lcov-report/threads/index.js.html +11 -11
  148. package/coverage/lcov-report/threads/query.js.html +367 -91
  149. package/coverage/lcov-report/threads/utils.js.html +356 -11
  150. package/coverage/lcov-report/utilities/bcrypt.js.html +1 -1
  151. package/coverage/lcov-report/utilities/budibaseDir.js.html +1 -1
  152. package/coverage/lcov-report/utilities/centralPath.js.html +1 -1
  153. package/coverage/lcov-report/utilities/csvParser.js.html +1 -1
  154. package/coverage/lcov-report/utilities/global.js.html +13 -13
  155. package/coverage/lcov-report/utilities/index.html +1 -1
  156. package/coverage/lcov-report/utilities/index.js.html +9 -9
  157. package/coverage/lcov-report/utilities/queue/inMemoryQueue.js.html +2 -2
  158. package/coverage/lcov-report/utilities/queue/index.html +1 -1
  159. package/coverage/lcov-report/utilities/routing/index.html +1 -1
  160. package/coverage/lcov-report/utilities/routing/index.js.html +1 -1
  161. package/coverage/lcov-report/utilities/rowProcessor/index.html +19 -19
  162. package/coverage/lcov-report/utilities/rowProcessor/index.js.html +235 -10
  163. package/coverage/lcov-report/utilities/rowProcessor/utils.js.html +1 -1
  164. package/coverage/lcov-report/utilities/scriptRunner.js.html +1 -1
  165. package/coverage/lcov-report/utilities/security.js.html +1 -1
  166. package/coverage/lcov-report/utilities/statusCodes.js.html +1 -1
  167. package/coverage/lcov-report/utilities/users.js.html +1 -1
  168. package/coverage/lcov-report/utilities/workerRequests.js.html +1 -1
  169. package/coverage/lcov.info +1348 -1077
  170. package/dist/api/controllers/query/import/sources/base/index.js +2 -1
  171. package/dist/api/controllers/query/import/sources/curl.js +11 -5
  172. package/dist/api/controllers/query/import/sources/openapi2.js +8 -5
  173. package/dist/api/controllers/query/index.js +45 -47
  174. package/dist/api/controllers/query/validation.js +2 -1
  175. package/dist/api/controllers/row/internal.js +14 -6
  176. package/dist/api/controllers/row/internalSearch.js +2 -1
  177. package/dist/api/controllers/row/utils.js +13 -6
  178. package/dist/api/routes/query.js +3 -1
  179. package/dist/automations/steps/executeQuery.js +8 -1
  180. package/dist/integrations/base/sqlTable.js +1 -1
  181. package/dist/integrations/rest.js +3 -3
  182. package/dist/package.json +5 -4
  183. package/dist/tests/utilities/TestConfiguration.js +7 -0
  184. package/dist/threads/query.js +148 -52
  185. package/dist/threads/utils.js +105 -0
  186. package/dist/tsconfig.tsbuildinfo +1 -1
  187. package/dist/utilities/fileSystem/utilities.js +2 -1
  188. package/dist/utilities/rowProcessor/index.js +76 -0
  189. package/package.json +6 -5
  190. package/src/api/controllers/query/import/sources/base/index.ts +3 -1
  191. package/src/api/controllers/query/import/sources/curl.ts +12 -4
  192. package/src/api/controllers/query/import/sources/openapi2.ts +9 -5
  193. package/src/api/controllers/query/import/sources/tests/curl/curl.spec.js +2 -3
  194. package/src/api/controllers/query/import/sources/tests/openapi2/openapi2.spec.js +6 -7
  195. package/src/api/controllers/query/import/tests/index.spec.js +0 -6
  196. package/src/api/controllers/query/index.js +48 -55
  197. package/src/api/controllers/query/validation.js +2 -1
  198. package/src/api/controllers/row/internal.js +14 -5
  199. package/src/api/controllers/row/internalSearch.js +2 -1
  200. package/src/api/controllers/row/utils.js +11 -8
  201. package/src/api/routes/query.js +8 -1
  202. package/src/api/routes/tests/query.spec.js +88 -0
  203. package/src/automations/steps/executeQuery.js +9 -1
  204. package/src/definitions/datasource.ts +10 -0
  205. package/src/integrations/base/sqlTable.ts +1 -1
  206. package/src/integrations/rest.ts +6 -3
  207. package/src/tests/utilities/TestConfiguration.js +10 -0
  208. package/src/threads/query.js +148 -56
  209. package/src/threads/utils.js +115 -0
  210. package/src/utilities/fileSystem/utilities.js +2 -0
  211. package/src/utilities/rowProcessor/index.js +75 -0
  212. package/builder/assets/index.00c4135e.js +0 -1320
@@ -1,5 +1,6 @@
1
1
  // Mock out postgres for this
2
2
  jest.mock("pg")
3
+ jest.mock("node-fetch")
3
4
 
4
5
  // Mock isProdAppID to we can later mock the implementation and pretend we are
5
6
  // using prod app IDs
@@ -10,6 +11,7 @@ authDb.isProdAppID = mockIsProdAppID
10
11
 
11
12
  const setup = require("./utilities")
12
13
  const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
14
+ const { checkCacheForDynamicVariable } = require("../../../threads/utils")
13
15
  const { basicQuery, basicDatasource } = setup.structures
14
16
 
15
17
  describe("/queries", () => {
@@ -226,4 +228,90 @@ describe("/queries", () => {
226
228
  .expect(400)
227
229
  })
228
230
  })
231
+
232
+ describe("test variables", () => {
233
+ async function restDatasource(cfg) {
234
+ return await config.createDatasource({
235
+ datasource: {
236
+ ...basicDatasource().datasource,
237
+ source: "REST",
238
+ config: cfg || {},
239
+ },
240
+ })
241
+ }
242
+
243
+ async function dynamicVariableDatasource() {
244
+ const datasource = await restDatasource()
245
+ const basedOnQuery = await config.createQuery({
246
+ ...basicQuery(datasource._id),
247
+ fields: {
248
+ path: "www.google.com",
249
+ },
250
+ })
251
+ await config.updateDatasource({
252
+ ...datasource,
253
+ config: {
254
+ dynamicVariables: [
255
+ { queryId: basedOnQuery._id, name: "variable3", value: "{{ data.0.[value] }}" }
256
+ ]
257
+ }
258
+ })
259
+ return { datasource, query: basedOnQuery }
260
+ }
261
+
262
+ async function preview(datasource, fields) {
263
+ return await request
264
+ .post(`/api/queries/preview`)
265
+ .send({
266
+ datasourceId: datasource._id,
267
+ parameters: {},
268
+ fields,
269
+ queryVerb: "read",
270
+ })
271
+ .set(config.defaultHeaders())
272
+ .expect("Content-Type", /json/)
273
+ .expect(200)
274
+ }
275
+
276
+ it("should work with static variables", async () => {
277
+ const datasource = await restDatasource({
278
+ staticVariables: {
279
+ variable: "google",
280
+ variable2: "1",
281
+ },
282
+ })
283
+ const res = await preview(datasource, {
284
+ path: "www.{{ variable }}.com",
285
+ queryString: "test={{ variable2 }}",
286
+ })
287
+ // these responses come from the mock
288
+ expect(res.body.schemaFields).toEqual(["url", "opts", "value"])
289
+ expect(res.body.rows[0].url).toEqual("http://www.google.com?test=1")
290
+ })
291
+
292
+ it("should work with dynamic variables", async () => {
293
+ const { datasource } = await dynamicVariableDatasource()
294
+ const res = await preview(datasource, {
295
+ path: "www.google.com",
296
+ queryString: "test={{ variable3 }}",
297
+ })
298
+ expect(res.body.schemaFields).toEqual(["url", "opts", "value"])
299
+ expect(res.body.rows[0].url).toContain("doctype html")
300
+ })
301
+
302
+ it("check that it automatically retries on fail with cached dynamics", async () => {
303
+ const { datasource, query: base } = await dynamicVariableDatasource()
304
+ // preview once to cache
305
+ await preview(datasource, { path: "www.google.com", queryString: "test={{ variable3 }}" })
306
+ // check its in cache
307
+ const contents = await checkCacheForDynamicVariable(base._id, "variable3")
308
+ expect(contents.rows.length).toEqual(1)
309
+ const res = await preview(datasource, {
310
+ path: "www.failonce.com",
311
+ queryString: "test={{ variable3 }}",
312
+ })
313
+ expect(res.body.schemaFields).toEqual(["fails", "url", "opts"])
314
+ expect(res.body.rows[0].fails).toEqual(1)
315
+ })
316
+ })
229
317
  })
@@ -35,6 +35,11 @@ exports.definition = {
35
35
  type: "object",
36
36
  description: "The response from the datasource execution",
37
37
  },
38
+ info: {
39
+ type: "object",
40
+ description:
41
+ "Some query types may return extra data, like headers from a REST query",
42
+ },
38
43
  success: {
39
44
  type: "boolean",
40
45
  description: "Whether the action was successful",
@@ -68,13 +73,16 @@ exports.run = async function ({ inputs, appId, emitter }) {
68
73
 
69
74
  try {
70
75
  await queryController.execute(ctx)
76
+ const { data, ...rest } = ctx.body
71
77
  return {
72
- response: ctx.body,
78
+ response: data,
79
+ info: rest,
73
80
  success: true,
74
81
  }
75
82
  } catch (err) {
76
83
  return {
77
84
  success: false,
85
+ info: {},
78
86
  response: automationUtils.getError(err),
79
87
  }
80
88
  }
@@ -240,6 +240,16 @@ export interface RestConfig {
240
240
  [key: string]: any
241
241
  }
242
242
  authConfigs: AuthConfig[]
243
+ staticVariables: {
244
+ [key: string]: string
245
+ }
246
+ dynamicVariables: [
247
+ {
248
+ name: string
249
+ queryId: string
250
+ value: string
251
+ }
252
+ ]
243
253
  }
244
254
 
245
255
  export interface Query {
@@ -36,7 +36,7 @@ function generateSchema(
36
36
  case FieldTypes.STRING:
37
37
  case FieldTypes.OPTIONS:
38
38
  case FieldTypes.LONGFORM:
39
- schema.string(key)
39
+ schema.text(key)
40
40
  break
41
41
  case FieldTypes.NUMBER:
42
42
  // if meta is specified then this is a junction table entry
@@ -48,7 +48,10 @@ module RestModule {
48
48
  const { performance } = require("perf_hooks")
49
49
  const FormData = require("form-data")
50
50
  const { URLSearchParams } = require("url")
51
- const { parseStringPromise: xmlParser, Builder: XmlBuilder } = require("xml2js")
51
+ const {
52
+ parseStringPromise: xmlParser,
53
+ Builder: XmlBuilder,
54
+ } = require("xml2js")
52
55
 
53
56
  const SCHEMA: Integration = {
54
57
  docs: "https://github.com/node-fetch/node-fetch",
@@ -168,7 +171,7 @@ module RestModule {
168
171
  getUrl(path: string, queryString: string): string {
169
172
  const main = `${path}?${queryString}`
170
173
  let complete = main
171
- if (this.config.url && !main.startsWith(this.config.url)) {
174
+ if (this.config.url && !main.startsWith("http")) {
172
175
  complete = !this.config.url ? main : `${this.config.url}/${main}`
173
176
  }
174
177
  if (!complete.startsWith("http")) {
@@ -211,7 +214,7 @@ module RestModule {
211
214
  break
212
215
  case BodyTypes.XML:
213
216
  if (object != null) {
214
- string = (new XmlBuilder()).buildObject(object)
217
+ string = new XmlBuilder().buildObject(object)
215
218
  }
216
219
  input.body = string
217
220
  input.headers["Content-Type"] = "application/xml"
@@ -316,6 +316,16 @@ class TestConfiguration {
316
316
  return this.datasource
317
317
  }
318
318
 
319
+ async updateDatasource(datasource) {
320
+ const response = await this._req(
321
+ datasource,
322
+ { datasourceId: datasource._id },
323
+ controllers.datasource.update
324
+ )
325
+ this.datasource = response.datasource
326
+ return this.datasource
327
+ }
328
+
319
329
  async createQuery(config = null) {
320
330
  if (!this.datasource && !config) {
321
331
  throw "No data source created for query."
@@ -1,77 +1,169 @@
1
- require("./utils").threadSetup()
1
+ const threadUtils = require("./utils")
2
+ threadUtils.threadSetup()
2
3
  const ScriptRunner = require("../utilities/scriptRunner")
3
4
  const { integrations } = require("../integrations")
5
+ const { processStringSync } = require("@budibase/string-templates")
6
+ const CouchDB = require("../db")
4
7
 
5
- function formatResponse(resp) {
6
- if (typeof resp === "string") {
7
- try {
8
- resp = JSON.parse(resp)
9
- } catch (err) {
10
- resp = { response: resp }
11
- }
8
+ class QueryRunner {
9
+ constructor(input, flags = { noRecursiveQuery: false }) {
10
+ this.appId = input.appId
11
+ this.datasource = input.datasource
12
+ this.queryVerb = input.queryVerb
13
+ this.fields = input.fields
14
+ this.parameters = input.parameters
15
+ this.transformer = input.transformer
16
+ this.queryId = input.queryId
17
+ this.noRecursiveQuery = flags.noRecursiveQuery
18
+ this.cachedVariables = []
19
+ // allows the response from a query to be stored throughout this
20
+ // execution so that if it needs to be re-used for another variable
21
+ // it can be
22
+ this.queryResponse = {}
23
+ this.hasRerun = false
12
24
  }
13
- return resp
14
- }
15
25
 
16
- function hasExtraData(response) {
17
- return (
18
- typeof response === "object" &&
19
- !Array.isArray(response) &&
20
- response.data != null &&
21
- response.info != null
22
- )
23
- }
26
+ async execute() {
27
+ let { datasource, fields, queryVerb, transformer } = this
28
+ // pre-query, make sure datasource variables are added to parameters
29
+ const parameters = await this.addDatasourceVariables()
30
+ const query = threadUtils.enrichQueryFields(fields, parameters)
31
+ const Integration = integrations[datasource.source]
32
+ if (!Integration) {
33
+ throw "Integration type does not exist."
34
+ }
35
+ const integration = new Integration(datasource.config)
24
36
 
25
- async function runAndTransform(datasource, queryVerb, query, transformer) {
26
- const Integration = integrations[datasource.source]
27
- if (!Integration) {
28
- throw "Integration type does not exist."
29
- }
30
- const integration = new Integration(datasource.config)
37
+ let output = threadUtils.formatResponse(await integration[queryVerb](query))
38
+ let rows = output,
39
+ info = undefined,
40
+ extra = undefined
41
+ if (threadUtils.hasExtraData(output)) {
42
+ rows = output.data
43
+ info = output.info
44
+ extra = output.extra
45
+ }
31
46
 
32
- let output = formatResponse(await integration[queryVerb](query))
33
- let rows = output,
34
- info = undefined,
35
- extra = undefined
36
- if (hasExtraData(output)) {
37
- rows = output.data
38
- info = output.info
39
- extra = output.extra
40
- }
47
+ // transform as required
48
+ if (transformer) {
49
+ const runner = new ScriptRunner(transformer, {
50
+ data: rows,
51
+ params: parameters,
52
+ })
53
+ rows = runner.execute()
54
+ }
41
55
 
42
- // transform as required
43
- if (transformer) {
44
- const runner = new ScriptRunner(transformer, { data: rows })
45
- rows = runner.execute()
46
- }
56
+ // if the request fails we retry once, invalidating the cached value
57
+ if (
58
+ info &&
59
+ info.code >= 400 &&
60
+ this.cachedVariables.length > 0 &&
61
+ !this.hasRerun
62
+ ) {
63
+ this.hasRerun = true
64
+ // invalidate the cache value
65
+ await threadUtils.invalidateDynamicVariables(this.cachedVariables)
66
+ return this.execute()
67
+ }
47
68
 
48
- // needs to an array for next step
49
- if (!Array.isArray(rows)) {
50
- rows = [rows]
51
- }
69
+ // needs to an array for next step
70
+ if (!Array.isArray(rows)) {
71
+ rows = [rows]
72
+ }
52
73
 
53
- // map into JSON if just raw primitive here
54
- if (rows.find(row => typeof row !== "object")) {
55
- rows = rows.map(value => ({ value }))
74
+ // map into JSON if just raw primitive here
75
+ if (rows.find(row => typeof row !== "object")) {
76
+ rows = rows.map(value => ({ value }))
77
+ }
78
+
79
+ // get all the potential fields in the schema
80
+ let keys = rows.flatMap(Object.keys)
81
+
82
+ if (integration.end) {
83
+ integration.end()
84
+ }
85
+
86
+ return { rows, keys, info, extra }
56
87
  }
57
88
 
58
- // get all the potential fields in the schema
59
- let keys = rows.flatMap(Object.keys)
89
+ async runAnotherQuery(queryId, parameters) {
90
+ const db = new CouchDB(this.appId)
91
+ const query = await db.get(queryId)
92
+ const datasource = await db.get(query.datasourceId)
93
+ return new QueryRunner(
94
+ {
95
+ appId: this.appId,
96
+ datasource,
97
+ queryVerb: query.queryVerb,
98
+ fields: query.fields,
99
+ parameters,
100
+ transformer: query.transformer,
101
+ },
102
+ { noRecursiveQuery: true }
103
+ ).execute()
104
+ }
60
105
 
61
- if (integration.end) {
62
- integration.end()
106
+ async getDynamicVariable(variable) {
107
+ let { parameters } = this
108
+ const queryId = variable.queryId,
109
+ name = variable.name
110
+ let value = await threadUtils.checkCacheForDynamicVariable(queryId, name)
111
+ if (!value) {
112
+ value = this.queryResponse[queryId]
113
+ ? this.queryResponse[queryId]
114
+ : await this.runAnotherQuery(queryId, parameters)
115
+ // store incase this query is to be called again
116
+ this.queryResponse[queryId] = value
117
+ await threadUtils.storeDynamicVariable(queryId, name, value)
118
+ } else {
119
+ this.cachedVariables.push({ queryId, name })
120
+ }
121
+ return value
63
122
  }
64
123
 
65
- return { rows, keys, info, extra }
124
+ async addDatasourceVariables() {
125
+ let { datasource, parameters, fields } = this
126
+ if (!datasource || !datasource.config) {
127
+ return parameters
128
+ }
129
+ const staticVars = datasource.config.staticVariables || {}
130
+ const dynamicVars = datasource.config.dynamicVariables || []
131
+ for (let [key, value] of Object.entries(staticVars)) {
132
+ if (!parameters[key]) {
133
+ parameters[key] = value
134
+ }
135
+ }
136
+ if (!this.noRecursiveQuery) {
137
+ // need to see if this uses any variables
138
+ const stringFields = JSON.stringify(fields)
139
+ const foundVars = dynamicVars.filter(variable => {
140
+ // don't allow a query to use its own dynamic variable (loop)
141
+ if (variable.queryId === this.queryId) {
142
+ return false
143
+ }
144
+ // look for {{ variable }} but allow spaces between handlebars
145
+ const regex = new RegExp(`{{[ ]*${variable.name}[ ]*}}`)
146
+ return regex.test(stringFields)
147
+ })
148
+ const dynamics = foundVars.map(dynVar => this.getDynamicVariable(dynVar))
149
+ const responses = await Promise.all(dynamics)
150
+ for (let i = 0; i < foundVars.length; i++) {
151
+ const variable = foundVars[i]
152
+ parameters[variable.name] = processStringSync(variable.value, {
153
+ data: responses[i].rows,
154
+ info: responses[i].extra,
155
+ })
156
+ // make sure its known that this uses dynamic variables in case it fails
157
+ this.hasDynamicVariables = true
158
+ }
159
+ }
160
+ return parameters
161
+ }
66
162
  }
67
163
 
68
164
  module.exports = (input, callback) => {
69
- runAndTransform(
70
- input.datasource,
71
- input.queryVerb,
72
- input.query,
73
- input.transformer
74
- )
165
+ const Runner = new QueryRunner(input)
166
+ Runner.execute()
75
167
  .then(response => {
76
168
  callback(null, response)
77
169
  })
@@ -1,6 +1,26 @@
1
1
  const env = require("../environment")
2
2
  const CouchDB = require("../db")
3
3
  const { init } = require("@budibase/auth")
4
+ const redis = require("@budibase/auth/redis")
5
+ const { SEPARATOR } = require("@budibase/auth/db")
6
+ const { processStringSync } = require("@budibase/string-templates")
7
+
8
+ const VARIABLE_TTL_SECONDS = 3600
9
+ let client
10
+
11
+ const IS_TRIPLE_BRACE = new RegExp(/^{{3}.*}{3}$/)
12
+ const IS_HANDLEBARS = new RegExp(/^{{2}.*}{2}$/)
13
+
14
+ async function getClient() {
15
+ if (!client) {
16
+ client = await new redis.Client(redis.utils.Databases.QUERY_VARS).init()
17
+ }
18
+ return client
19
+ }
20
+
21
+ process.on("exit", async () => {
22
+ if (client) await client.finish()
23
+ })
4
24
 
5
25
  exports.threadSetup = () => {
6
26
  // don't run this if not threading
@@ -11,3 +31,98 @@ exports.threadSetup = () => {
11
31
  env.setInThread()
12
32
  init(CouchDB)
13
33
  }
34
+
35
+ function makeVariableKey(queryId, variable) {
36
+ return `${queryId}${SEPARATOR}${variable}`
37
+ }
38
+
39
+ exports.checkCacheForDynamicVariable = async (queryId, variable) => {
40
+ const cache = await getClient()
41
+ return cache.get(makeVariableKey(queryId, variable))
42
+ }
43
+
44
+ exports.invalidateDynamicVariables = async cachedVars => {
45
+ const cache = await getClient()
46
+ let promises = []
47
+ for (let variable of cachedVars) {
48
+ promises.push(
49
+ cache.delete(makeVariableKey(variable.queryId, variable.name))
50
+ )
51
+ }
52
+ await Promise.all(promises)
53
+ }
54
+
55
+ exports.storeDynamicVariable = async (queryId, variable, value) => {
56
+ const cache = await getClient()
57
+ await cache.store(
58
+ makeVariableKey(queryId, variable),
59
+ value,
60
+ VARIABLE_TTL_SECONDS
61
+ )
62
+ }
63
+
64
+ exports.formatResponse = resp => {
65
+ if (typeof resp === "string") {
66
+ try {
67
+ resp = JSON.parse(resp)
68
+ } catch (err) {
69
+ resp = { response: resp }
70
+ }
71
+ }
72
+ return resp
73
+ }
74
+
75
+ exports.hasExtraData = response => {
76
+ return (
77
+ typeof response === "object" &&
78
+ !Array.isArray(response) &&
79
+ response.data != null &&
80
+ response.info != null
81
+ )
82
+ }
83
+
84
+ exports.enrichQueryFields = (fields, parameters = {}) => {
85
+ const enrichedQuery = {}
86
+
87
+ // enrich the fields with dynamic parameters
88
+ for (let key of Object.keys(fields)) {
89
+ if (fields[key] == null) {
90
+ continue
91
+ }
92
+ if (typeof fields[key] === "object") {
93
+ // enrich nested fields object
94
+ enrichedQuery[key] = this.enrichQueryFields(fields[key], parameters)
95
+ } else if (typeof fields[key] === "string") {
96
+ // enrich string value as normal
97
+ let value = fields[key]
98
+ // add triple brace to avoid escaping e.g. '=' in cookie header
99
+ if (IS_HANDLEBARS.test(value) && !IS_TRIPLE_BRACE.test(value)) {
100
+ value = `{${value}}`
101
+ }
102
+ enrichedQuery[key] = processStringSync(value, parameters, {
103
+ noHelpers: true,
104
+ })
105
+ } else {
106
+ enrichedQuery[key] = fields[key]
107
+ }
108
+ }
109
+
110
+ if (
111
+ enrichedQuery.json ||
112
+ enrichedQuery.customData ||
113
+ enrichedQuery.requestBody
114
+ ) {
115
+ try {
116
+ enrichedQuery.json = JSON.parse(
117
+ enrichedQuery.json ||
118
+ enrichedQuery.customData ||
119
+ enrichedQuery.requestBody
120
+ )
121
+ } catch (err) {
122
+ // no json found, ignore
123
+ }
124
+ delete enrichedQuery.customData
125
+ }
126
+
127
+ return enrichedQuery
128
+ }
@@ -2,6 +2,7 @@ const {
2
2
  ObjectStore,
3
3
  makeSureBucketExists,
4
4
  upload,
5
+ deleteFiles,
5
6
  streamUpload,
6
7
  retrieve,
7
8
  retrieveToTmp,
@@ -28,3 +29,4 @@ exports.retrieveToTmp = retrieveToTmp
28
29
  exports.deleteFolder = deleteFolder
29
30
  exports.uploadDirectory = uploadDirectory
30
31
  exports.downloadTarball = downloadTarball
32
+ exports.deleteFiles = deleteFiles
@@ -3,6 +3,10 @@ const { cloneDeep } = require("lodash/fp")
3
3
  const { FieldTypes, AutoFieldSubTypes } = require("../../constants")
4
4
  const { attachmentsRelativeURL } = require("../index")
5
5
  const { processFormulas } = require("./utils")
6
+ const { deleteFiles } = require("../../utilities/fileSystem/utilities")
7
+ const { ObjectStoreBuckets } = require("../../constants")
8
+ const { isProdAppID, getDeployedAppID, dbExists } = require("@budibase/auth/db")
9
+ const CouchDB = require("../../db")
6
10
 
7
11
  const BASE_AUTO_ID = 1
8
12
 
@@ -81,6 +85,35 @@ const TYPE_TRANSFORM_MAP = {
81
85
  [FieldTypes.AUTO]: {
82
86
  parse: () => undefined,
83
87
  },
88
+ [FieldTypes.JSON]: {
89
+ parse: input => {
90
+ try {
91
+ if (input === "") {
92
+ return undefined
93
+ }
94
+ return JSON.parse(input)
95
+ } catch (err) {
96
+ return input
97
+ }
98
+ },
99
+ },
100
+ }
101
+
102
+ /**
103
+ * Given the old state of the row and the new one after an update, this will
104
+ * find the keys that have been removed in the updated row.
105
+ */
106
+ function getRemovedAttachmentKeys(oldRow, row, attachmentKey) {
107
+ if (!oldRow[attachmentKey]) {
108
+ return []
109
+ }
110
+ const oldKeys = oldRow[attachmentKey].map(attachment => attachment.key)
111
+ // no attachments in new row, all removed
112
+ if (!row[attachmentKey]) {
113
+ return oldKeys
114
+ }
115
+ const newKeys = row[attachmentKey].map(attachment => attachment.key)
116
+ return oldKeys.filter(key => newKeys.indexOf(key) === -1)
84
117
  }
85
118
 
86
119
  /**
@@ -260,3 +293,45 @@ exports.outputProcessing = async (
260
293
  }
261
294
  return wasArray ? enriched : enriched[0]
262
295
  }
296
+
297
+ /**
298
+ * Clean up any attachments that were attached to a row.
299
+ * @param {string} appId The ID of the app from which a row is being deleted.
300
+ * @param {object} table The table from which a row is being removed.
301
+ * @param {any} row optional - the row being removed.
302
+ * @param {any} rows optional - if multiple rows being deleted can do this in bulk.
303
+ * @param {any} oldRow optional - if updating a row this will determine the difference.
304
+ * @return {Promise<void>} When all attachments have been removed this will return.
305
+ */
306
+ exports.cleanupAttachments = async (appId, table, { row, rows, oldRow }) => {
307
+ if (!isProdAppID(appId)) {
308
+ const prodAppId = getDeployedAppID(appId)
309
+ // if prod exists, then don't allow deleting
310
+ const exists = await dbExists(CouchDB, prodAppId)
311
+ if (exists) {
312
+ return
313
+ }
314
+ }
315
+ let files = []
316
+ function addFiles(row, key) {
317
+ if (row[key]) {
318
+ files = files.concat(row[key].map(attachment => attachment.key))
319
+ }
320
+ }
321
+ for (let [key, schema] of Object.entries(table.schema)) {
322
+ if (schema.type !== FieldTypes.ATTACHMENT) {
323
+ continue
324
+ }
325
+ // if updating, need to manage the differences
326
+ if (oldRow && row) {
327
+ files = files.concat(getRemovedAttachmentKeys(oldRow, row, key))
328
+ } else if (row) {
329
+ addFiles(row, key)
330
+ } else if (rows) {
331
+ rows.forEach(row => addFiles(row, key))
332
+ }
333
+ }
334
+ if (files.length > 0) {
335
+ return deleteFiles(ObjectStoreBuckets.APPS, files)
336
+ }
337
+ }