@budibase/backend-core 2.9.40-alpha.6 → 2.10.1

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 (252) hide show
  1. package/dist/index.js +5 -4
  2. package/dist/index.js.map +2 -2
  3. package/dist/index.js.meta.json +1 -1
  4. package/dist/package.json +6 -6
  5. package/dist/src/cache/appMetadata.js +1 -1
  6. package/dist/src/cache/appMetadata.js.map +1 -1
  7. package/dist/src/constants/misc.d.ts +0 -2
  8. package/dist/src/constants/misc.js +0 -2
  9. package/dist/src/constants/misc.js.map +1 -1
  10. package/dist/src/environment.js +5 -4
  11. package/dist/src/environment.js.map +1 -1
  12. package/dist/src/logging/system.d.ts +1 -1
  13. package/dist/src/timers/timers.d.ts +1 -1
  14. package/package.json +6 -6
  15. package/src/accounts/accounts.ts +82 -0
  16. package/src/accounts/api.ts +59 -0
  17. package/src/accounts/index.ts +1 -0
  18. package/src/auth/auth.ts +208 -0
  19. package/src/auth/index.ts +1 -0
  20. package/src/auth/tests/auth.spec.ts +14 -0
  21. package/src/blacklist/blacklist.ts +54 -0
  22. package/src/blacklist/index.ts +1 -0
  23. package/src/blacklist/tests/blacklist.spec.ts +46 -0
  24. package/src/cache/appMetadata.ts +88 -0
  25. package/src/cache/base/index.ts +92 -0
  26. package/src/cache/generic.ts +30 -0
  27. package/src/cache/index.ts +5 -0
  28. package/src/cache/tests/writethrough.spec.ts +138 -0
  29. package/src/cache/user.ts +83 -0
  30. package/src/cache/writethrough.ts +133 -0
  31. package/src/configs/configs.ts +257 -0
  32. package/src/configs/index.ts +1 -0
  33. package/src/configs/tests/configs.spec.ts +184 -0
  34. package/src/constants/db.ts +63 -0
  35. package/src/constants/index.ts +2 -0
  36. package/src/constants/misc.ts +50 -0
  37. package/src/context/Context.ts +14 -0
  38. package/src/context/identity.ts +58 -0
  39. package/src/context/index.ts +3 -0
  40. package/src/context/mainContext.ts +310 -0
  41. package/src/context/tests/index.spec.ts +147 -0
  42. package/src/context/types.ts +11 -0
  43. package/src/db/Replication.ts +84 -0
  44. package/src/db/constants.ts +10 -0
  45. package/src/db/couch/DatabaseImpl.ts +238 -0
  46. package/src/db/couch/connections.ts +77 -0
  47. package/src/db/couch/index.ts +5 -0
  48. package/src/db/couch/pouchDB.ts +97 -0
  49. package/src/db/couch/pouchDump.ts +0 -0
  50. package/src/db/couch/utils.ts +50 -0
  51. package/src/db/db.ts +43 -0
  52. package/src/db/errors.ts +14 -0
  53. package/src/db/index.ts +12 -0
  54. package/src/db/lucene.ts +750 -0
  55. package/src/db/searchIndexes/index.ts +1 -0
  56. package/src/db/searchIndexes/searchIndexes.ts +62 -0
  57. package/src/db/tests/index.spec.js +25 -0
  58. package/src/db/tests/lucene.spec.ts +368 -0
  59. package/src/db/tests/pouch.spec.js +62 -0
  60. package/src/db/tests/utils.spec.ts +63 -0
  61. package/src/db/utils.ts +207 -0
  62. package/src/db/views.ts +241 -0
  63. package/src/docIds/conversions.ts +59 -0
  64. package/src/docIds/ids.ts +113 -0
  65. package/src/docIds/index.ts +2 -0
  66. package/src/docIds/newid.ts +5 -0
  67. package/src/docIds/params.ts +174 -0
  68. package/src/docUpdates/index.ts +29 -0
  69. package/src/environment.ts +201 -0
  70. package/src/errors/errors.ts +119 -0
  71. package/src/errors/index.ts +1 -0
  72. package/src/events/analytics.ts +6 -0
  73. package/src/events/asyncEvents/index.ts +2 -0
  74. package/src/events/asyncEvents/publisher.ts +12 -0
  75. package/src/events/asyncEvents/queue.ts +22 -0
  76. package/src/events/backfill.ts +183 -0
  77. package/src/events/documentId.ts +56 -0
  78. package/src/events/events.ts +40 -0
  79. package/src/events/identification.ts +310 -0
  80. package/src/events/index.ts +14 -0
  81. package/src/events/processors/AnalyticsProcessor.ts +64 -0
  82. package/src/events/processors/AuditLogsProcessor.ts +93 -0
  83. package/src/events/processors/LoggingProcessor.ts +37 -0
  84. package/src/events/processors/Processors.ts +52 -0
  85. package/src/events/processors/async/DocumentUpdateProcessor.ts +43 -0
  86. package/src/events/processors/index.ts +19 -0
  87. package/src/events/processors/posthog/PosthogProcessor.ts +118 -0
  88. package/src/events/processors/posthog/index.ts +2 -0
  89. package/src/events/processors/posthog/rateLimiting.ts +106 -0
  90. package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +168 -0
  91. package/src/events/processors/types.ts +1 -0
  92. package/src/events/publishers/account.ts +35 -0
  93. package/src/events/publishers/app.ts +155 -0
  94. package/src/events/publishers/auditLog.ts +26 -0
  95. package/src/events/publishers/auth.ts +73 -0
  96. package/src/events/publishers/automation.ts +110 -0
  97. package/src/events/publishers/backfill.ts +74 -0
  98. package/src/events/publishers/backup.ts +42 -0
  99. package/src/events/publishers/datasource.ts +48 -0
  100. package/src/events/publishers/email.ts +17 -0
  101. package/src/events/publishers/environmentVariable.ts +38 -0
  102. package/src/events/publishers/group.ts +99 -0
  103. package/src/events/publishers/index.ts +24 -0
  104. package/src/events/publishers/installation.ts +38 -0
  105. package/src/events/publishers/layout.ts +26 -0
  106. package/src/events/publishers/license.ts +84 -0
  107. package/src/events/publishers/org.ts +37 -0
  108. package/src/events/publishers/plugin.ts +47 -0
  109. package/src/events/publishers/query.ts +88 -0
  110. package/src/events/publishers/role.ts +62 -0
  111. package/src/events/publishers/rows.ts +29 -0
  112. package/src/events/publishers/screen.ts +36 -0
  113. package/src/events/publishers/serve.ts +43 -0
  114. package/src/events/publishers/table.ts +70 -0
  115. package/src/events/publishers/user.ts +202 -0
  116. package/src/events/publishers/view.ts +107 -0
  117. package/src/features/index.ts +78 -0
  118. package/src/features/installation.ts +17 -0
  119. package/src/features/tests/featureFlags.spec.ts +85 -0
  120. package/src/helpers.ts +9 -0
  121. package/src/index.ts +54 -0
  122. package/src/installation.ts +107 -0
  123. package/src/logging/alerts.ts +26 -0
  124. package/src/logging/correlation/correlation.ts +13 -0
  125. package/src/logging/correlation/index.ts +1 -0
  126. package/src/logging/correlation/middleware.ts +17 -0
  127. package/src/logging/index.ts +4 -0
  128. package/src/logging/pino/logger.ts +232 -0
  129. package/src/logging/pino/middleware.ts +45 -0
  130. package/src/logging/system.ts +81 -0
  131. package/src/logging/tests/system.spec.ts +61 -0
  132. package/src/middleware/adminOnly.ts +9 -0
  133. package/src/middleware/auditLog.ts +6 -0
  134. package/src/middleware/authenticated.ts +193 -0
  135. package/src/middleware/builderOnly.ts +21 -0
  136. package/src/middleware/builderOrAdmin.ts +21 -0
  137. package/src/middleware/csrf.ts +81 -0
  138. package/src/middleware/errorHandling.ts +29 -0
  139. package/src/middleware/index.ts +21 -0
  140. package/src/middleware/internalApi.ts +23 -0
  141. package/src/middleware/joi-validator.ts +45 -0
  142. package/src/middleware/matchers.ts +47 -0
  143. package/src/middleware/passport/datasource/google.ts +95 -0
  144. package/src/middleware/passport/local.ts +54 -0
  145. package/src/middleware/passport/sso/google.ts +77 -0
  146. package/src/middleware/passport/sso/oidc.ts +154 -0
  147. package/src/middleware/passport/sso/sso.ts +165 -0
  148. package/src/middleware/passport/sso/tests/google.spec.ts +67 -0
  149. package/src/middleware/passport/sso/tests/oidc.spec.ts +152 -0
  150. package/src/middleware/passport/sso/tests/sso.spec.ts +197 -0
  151. package/src/middleware/passport/utils.ts +38 -0
  152. package/src/middleware/querystringToBody.ts +28 -0
  153. package/src/middleware/tenancy.ts +36 -0
  154. package/src/middleware/tests/builder.spec.ts +180 -0
  155. package/src/middleware/tests/matchers.spec.ts +134 -0
  156. package/src/migrations/definitions.ts +40 -0
  157. package/src/migrations/index.ts +2 -0
  158. package/src/migrations/migrations.ts +191 -0
  159. package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +11 -0
  160. package/src/migrations/tests/migrations.spec.ts +64 -0
  161. package/src/objectStore/buckets/app.ts +40 -0
  162. package/src/objectStore/buckets/global.ts +29 -0
  163. package/src/objectStore/buckets/index.ts +3 -0
  164. package/src/objectStore/buckets/plugins.ts +71 -0
  165. package/src/objectStore/buckets/tests/app.spec.ts +171 -0
  166. package/src/objectStore/buckets/tests/global.spec.ts +74 -0
  167. package/src/objectStore/buckets/tests/plugins.spec.ts +111 -0
  168. package/src/objectStore/cloudfront.ts +41 -0
  169. package/src/objectStore/index.ts +3 -0
  170. package/src/objectStore/objectStore.ts +440 -0
  171. package/src/objectStore/utils.ts +27 -0
  172. package/src/platform/index.ts +3 -0
  173. package/src/platform/platformDb.ts +6 -0
  174. package/src/platform/tenants.ts +101 -0
  175. package/src/platform/tests/tenants.spec.ts +26 -0
  176. package/src/platform/users.ts +90 -0
  177. package/src/plugin/index.ts +1 -0
  178. package/src/plugin/tests/validation.spec.ts +83 -0
  179. package/src/plugin/utils.ts +156 -0
  180. package/src/queue/constants.ts +6 -0
  181. package/src/queue/inMemoryQueue.ts +141 -0
  182. package/src/queue/index.ts +2 -0
  183. package/src/queue/listeners.ts +195 -0
  184. package/src/queue/queue.ts +54 -0
  185. package/src/redis/index.ts +6 -0
  186. package/src/redis/init.ts +86 -0
  187. package/src/redis/redis.ts +308 -0
  188. package/src/redis/redlockImpl.ts +139 -0
  189. package/src/redis/utils.ts +117 -0
  190. package/src/security/encryption.ts +179 -0
  191. package/src/security/permissions.ts +158 -0
  192. package/src/security/roles.ts +389 -0
  193. package/src/security/sessions.ts +120 -0
  194. package/src/security/tests/encryption.spec.ts +31 -0
  195. package/src/security/tests/permissions.spec.ts +145 -0
  196. package/src/security/tests/sessions.spec.ts +12 -0
  197. package/src/tenancy/db.ts +6 -0
  198. package/src/tenancy/index.ts +2 -0
  199. package/src/tenancy/tenancy.ts +140 -0
  200. package/src/tenancy/tests/tenancy.spec.ts +184 -0
  201. package/src/timers/index.ts +1 -0
  202. package/src/timers/timers.ts +22 -0
  203. package/src/users/db.ts +484 -0
  204. package/src/users/events.ts +176 -0
  205. package/src/users/index.ts +4 -0
  206. package/src/users/lookup.ts +102 -0
  207. package/src/users/users.ts +276 -0
  208. package/src/users/utils.ts +55 -0
  209. package/src/utils/hashing.ts +14 -0
  210. package/src/utils/index.ts +3 -0
  211. package/src/utils/stringUtils.ts +8 -0
  212. package/src/utils/tests/utils.spec.ts +191 -0
  213. package/src/utils/utils.ts +239 -0
  214. package/tests/core/logging.ts +34 -0
  215. package/tests/core/utilities/index.ts +6 -0
  216. package/tests/core/utilities/jestUtils.ts +30 -0
  217. package/tests/core/utilities/mocks/alerts.ts +3 -0
  218. package/tests/core/utilities/mocks/date.ts +2 -0
  219. package/tests/core/utilities/mocks/events.ts +131 -0
  220. package/tests/core/utilities/mocks/fetch.ts +17 -0
  221. package/tests/core/utilities/mocks/index.ts +10 -0
  222. package/tests/core/utilities/mocks/licenses.ts +115 -0
  223. package/tests/core/utilities/mocks/posthog.ts +7 -0
  224. package/tests/core/utilities/structures/Chance.ts +20 -0
  225. package/tests/core/utilities/structures/accounts.ts +115 -0
  226. package/tests/core/utilities/structures/apps.ts +21 -0
  227. package/tests/core/utilities/structures/common.ts +7 -0
  228. package/tests/core/utilities/structures/db.ts +12 -0
  229. package/tests/core/utilities/structures/documents/index.ts +1 -0
  230. package/tests/core/utilities/structures/documents/platform/index.ts +1 -0
  231. package/tests/core/utilities/structures/documents/platform/installation.ts +12 -0
  232. package/tests/core/utilities/structures/generator.ts +2 -0
  233. package/tests/core/utilities/structures/index.ts +15 -0
  234. package/tests/core/utilities/structures/koa.ts +16 -0
  235. package/tests/core/utilities/structures/licenses.ts +167 -0
  236. package/tests/core/utilities/structures/plugins.ts +19 -0
  237. package/tests/core/utilities/structures/quotas.ts +67 -0
  238. package/tests/core/utilities/structures/scim.ts +80 -0
  239. package/tests/core/utilities/structures/shared.ts +19 -0
  240. package/tests/core/utilities/structures/sso.ts +119 -0
  241. package/tests/core/utilities/structures/tenants.ts +5 -0
  242. package/tests/core/utilities/structures/userGroups.ts +10 -0
  243. package/tests/core/utilities/structures/users.ts +73 -0
  244. package/tests/core/utilities/testContainerUtils.ts +85 -0
  245. package/tests/core/utilities/utils/index.ts +1 -0
  246. package/tests/core/utilities/utils/time.ts +3 -0
  247. package/tests/extra/DBTestConfiguration.ts +36 -0
  248. package/tests/extra/index.ts +2 -0
  249. package/tests/extra/testEnv.ts +95 -0
  250. package/tests/index.ts +1 -0
  251. package/tests/jestEnv.ts +6 -0
  252. package/tests/jestSetup.ts +28 -0
@@ -0,0 +1,88 @@
1
+ import { getAppClient } from "../redis/init"
2
+ import { doWithDB, DocumentType } from "../db"
3
+ import { Database, App } from "@budibase/types"
4
+
5
+ export enum AppState {
6
+ INVALID = "invalid",
7
+ }
8
+
9
+ export interface DeletedApp {
10
+ state: AppState
11
+ }
12
+
13
+ const EXPIRY_SECONDS = 3600
14
+
15
+ /**
16
+ * The default populate app metadata function
17
+ */
18
+ async function populateFromDB(appId: string) {
19
+ return doWithDB(
20
+ appId,
21
+ (db: Database) => {
22
+ return db.get(DocumentType.APP_METADATA)
23
+ },
24
+ { skip_setup: true }
25
+ )
26
+ }
27
+
28
+ function isInvalid(metadata?: { state: string }) {
29
+ return !metadata || metadata.state === AppState.INVALID
30
+ }
31
+
32
+ /**
33
+ * Get the requested app metadata by id.
34
+ * Use redis cache to first read the app metadata.
35
+ * If not present fallback to loading the app metadata directly and re-caching.
36
+ * @param {string} appId the id of the app to get metadata from.
37
+ * @returns {object} the app metadata.
38
+ */
39
+ export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
40
+ const client = await getAppClient()
41
+ // try cache
42
+ let metadata = await client.get(appId)
43
+ if (!metadata) {
44
+ let expiry: number | undefined = EXPIRY_SECONDS
45
+ try {
46
+ metadata = await populateFromDB(appId)
47
+ } catch (err: any) {
48
+ // app DB left around, but no metadata, it is invalid
49
+ if (err && err.status === 404) {
50
+ metadata = { state: AppState.INVALID }
51
+ // don't expire the reference to an invalid app, it'll only be
52
+ // updated if a metadata doc actually gets stored (app is remade/reverted)
53
+ expiry = undefined
54
+ } else {
55
+ throw err
56
+ }
57
+ }
58
+ // needed for cypress/some scenarios where the caching happens
59
+ // so quickly the requests can get slightly out of sync
60
+ // might store its invalid just before it stores its valid
61
+ if (isInvalid(metadata)) {
62
+ const temp = await client.get(appId)
63
+ if (temp) {
64
+ metadata = temp
65
+ }
66
+ }
67
+ await client.store(appId, metadata, expiry)
68
+ }
69
+
70
+ return metadata
71
+ }
72
+
73
+ /**
74
+ * Invalidate/reset the cached metadata when a change occurs in the db.
75
+ * @param appId {string} the cache key to bust/update.
76
+ * @param newMetadata {object|undefined} optional - can simply provide the new metadata to update with.
77
+ * @return {Promise<void>} will respond with success when cache is updated.
78
+ */
79
+ export async function invalidateAppMetadata(appId: string, newMetadata?: any) {
80
+ if (!appId) {
81
+ throw "Cannot invalidate if no app ID provided."
82
+ }
83
+ const client = await getAppClient()
84
+ await client.delete(appId)
85
+ if (newMetadata) {
86
+ await client.store(appId, newMetadata, EXPIRY_SECONDS)
87
+ }
88
+ }
@@ -0,0 +1,92 @@
1
+ import { getTenantId } from "../../context"
2
+ import * as redis from "../../redis/init"
3
+ import { Client } from "../../redis"
4
+
5
+ function generateTenantKey(key: string) {
6
+ const tenantId = getTenantId()
7
+ return `${key}:${tenantId}`
8
+ }
9
+
10
+ export default class BaseCache {
11
+ client: Client | undefined
12
+
13
+ constructor(client: Client | undefined = undefined) {
14
+ this.client = client
15
+ }
16
+
17
+ async getClient() {
18
+ return !this.client ? await redis.getCacheClient() : this.client
19
+ }
20
+
21
+ async keys(pattern: string) {
22
+ const client = await this.getClient()
23
+ return client.keys(pattern)
24
+ }
25
+
26
+ /**
27
+ * Read only from the cache.
28
+ */
29
+ async get(key: string, opts = { useTenancy: true }) {
30
+ key = opts.useTenancy ? generateTenantKey(key) : key
31
+ const client = await this.getClient()
32
+ return client.get(key)
33
+ }
34
+
35
+ /**
36
+ * Write to the cache.
37
+ */
38
+ async store(
39
+ key: string,
40
+ value: any,
41
+ ttl: number | null = null,
42
+ opts = { useTenancy: true }
43
+ ) {
44
+ key = opts.useTenancy ? generateTenantKey(key) : key
45
+ const client = await this.getClient()
46
+ await client.store(key, value, ttl)
47
+ }
48
+
49
+ /**
50
+ * Remove from cache.
51
+ */
52
+ async delete(key: string, opts = { useTenancy: true }) {
53
+ key = opts.useTenancy ? generateTenantKey(key) : key
54
+ const client = await this.getClient()
55
+ return client.delete(key)
56
+ }
57
+
58
+ /**
59
+ * Read from the cache. Write to the cache if not exists.
60
+ */
61
+ async withCache(
62
+ key: string,
63
+ ttl: number,
64
+ fetchFn: any,
65
+ opts = { useTenancy: true }
66
+ ) {
67
+ const cachedValue = await this.get(key, opts)
68
+ if (cachedValue) {
69
+ return cachedValue
70
+ }
71
+
72
+ try {
73
+ const fetchedValue = await fetchFn()
74
+
75
+ await this.store(key, fetchedValue, ttl, opts)
76
+ return fetchedValue
77
+ } catch (err) {
78
+ console.error("Error fetching before cache - ", err)
79
+ throw err
80
+ }
81
+ }
82
+
83
+ async bustCache(key: string, opts = { client: null }) {
84
+ const client = await this.getClient()
85
+ try {
86
+ await client.delete(generateTenantKey(key))
87
+ } catch (err) {
88
+ console.error("Error busting cache - ", err)
89
+ throw err
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,30 @@
1
+ const BaseCache = require("./base")
2
+
3
+ const GENERIC = new BaseCache.default()
4
+
5
+ export enum CacheKey {
6
+ CHECKLIST = "checklist",
7
+ INSTALLATION = "installation",
8
+ ANALYTICS_ENABLED = "analyticsEnabled",
9
+ UNIQUE_TENANT_ID = "uniqueTenantId",
10
+ EVENTS = "events",
11
+ BACKFILL_METADATA = "backfillMetadata",
12
+ EVENTS_RATE_LIMIT = "eventsRateLimit",
13
+ }
14
+
15
+ export enum TTL {
16
+ ONE_MINUTE = 600,
17
+ ONE_HOUR = 3600,
18
+ ONE_DAY = 86400,
19
+ }
20
+
21
+ function performExport(funcName: string) {
22
+ return (...args: any) => GENERIC[funcName](...args)
23
+ }
24
+
25
+ export const keys = performExport("keys")
26
+ export const get = performExport("get")
27
+ export const store = performExport("store")
28
+ export const destroy = performExport("delete")
29
+ export const withCache = performExport("withCache")
30
+ export const bustCache = performExport("bustCache")
@@ -0,0 +1,5 @@
1
+ export * as generic from "./generic"
2
+ export * as user from "./user"
3
+ export * as app from "./appMetadata"
4
+ export * as writethrough from "./writethrough"
5
+ export * from "./generic"
@@ -0,0 +1,138 @@
1
+ import { DBTestConfiguration } from "../../../tests/extra"
2
+ import {
3
+ structures,
4
+ expectFunctionWasCalledTimesWith,
5
+ mocks,
6
+ } from "../../../tests"
7
+ import { Writethrough } from "../writethrough"
8
+ import { getDB } from "../../db"
9
+ import tk from "timekeeper"
10
+
11
+ tk.freeze(Date.now())
12
+
13
+ const DELAY = 5000
14
+
15
+ describe("writethrough", () => {
16
+ const config = new DBTestConfiguration()
17
+
18
+ const db = getDB(structures.db.id())
19
+ const db2 = getDB(structures.db.id())
20
+
21
+ const writethrough = new Writethrough(db, DELAY)
22
+ const writethrough2 = new Writethrough(db2, DELAY)
23
+
24
+ const docId = structures.uuid()
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks()
28
+ })
29
+
30
+ describe("put", () => {
31
+ let current: any
32
+
33
+ it("should be able to store, will go to DB", async () => {
34
+ await config.doInTenant(async () => {
35
+ const response = await writethrough.put({
36
+ _id: docId,
37
+ value: 1,
38
+ })
39
+ const output = await db.get<any>(response.id)
40
+ current = output
41
+ expect(output.value).toBe(1)
42
+ })
43
+ })
44
+
45
+ it("second put shouldn't update DB", async () => {
46
+ await config.doInTenant(async () => {
47
+ const response = await writethrough.put({ ...current, value: 2 })
48
+ const output = await db.get<any>(response.id)
49
+ expect(current._rev).toBe(output._rev)
50
+ expect(output.value).toBe(1)
51
+ })
52
+ })
53
+
54
+ it("should put it again after delay period", async () => {
55
+ await config.doInTenant(async () => {
56
+ tk.freeze(Date.now() + DELAY + 1)
57
+ const response = await writethrough.put({ ...current, value: 3 })
58
+ const output = await db.get<any>(response.id)
59
+ expect(response.rev).not.toBe(current._rev)
60
+ expect(output.value).toBe(3)
61
+
62
+ current = output
63
+ })
64
+ })
65
+
66
+ it("should handle parallel DB updates ignoring conflicts", async () => {
67
+ await config.doInTenant(async () => {
68
+ tk.freeze(Date.now() + DELAY + 1)
69
+ const responses = await Promise.all([
70
+ writethrough.put({ ...current, value: 4 }),
71
+ writethrough.put({ ...current, value: 4 }),
72
+ writethrough.put({ ...current, value: 4 }),
73
+ ])
74
+
75
+ // with a lock, this will work
76
+ const newRev = responses.map(x => x.rev).find(x => x !== current._rev)
77
+ expect(newRev).toBeDefined()
78
+ expect(responses.map(x => x.rev)).toEqual(
79
+ expect.arrayContaining([current._rev, current._rev, newRev])
80
+ )
81
+
82
+ const output = await db.get<any>(current._id)
83
+ expect(output.value).toBe(4)
84
+ expect(output._rev).toBe(newRev)
85
+
86
+ current = output
87
+ })
88
+ })
89
+
90
+ it("should handle updates with documents falling behind", async () => {
91
+ await config.doInTenant(async () => {
92
+ tk.freeze(Date.now() + DELAY + 1)
93
+
94
+ const id = structures.uuid()
95
+ await writethrough.put({ _id: id, value: 1 })
96
+ const doc = await writethrough.get(id)
97
+
98
+ // Updating document
99
+ tk.freeze(Date.now() + DELAY + 1)
100
+ await writethrough.put({ ...doc, value: 2 })
101
+
102
+ // Update with the old rev value
103
+ tk.freeze(Date.now() + DELAY + 1)
104
+ const res = await writethrough.put({
105
+ ...doc,
106
+ value: 3,
107
+ })
108
+ expect(res.ok).toBe(true)
109
+
110
+ const output = await db.get<any>(id)
111
+ expect(output.value).toBe(3)
112
+ expect(output._rev).toBe(res.rev)
113
+ })
114
+ })
115
+ })
116
+
117
+ describe("get", () => {
118
+ it("should be able to retrieve", async () => {
119
+ await config.doInTenant(async () => {
120
+ const response = await writethrough.get(docId)
121
+ expect(response.value).toBe(4)
122
+ })
123
+ })
124
+ })
125
+
126
+ describe("same doc, different databases (tenancy)", () => {
127
+ it("should be able to two different databases", async () => {
128
+ await config.doInTenant(async () => {
129
+ const resp1 = await writethrough.put({ _id: "db1", value: "first" })
130
+ const resp2 = await writethrough2.put({ _id: "db1", value: "second" })
131
+ expect(resp1.rev).toBeDefined()
132
+ expect(resp2.rev).toBeDefined()
133
+ expect((await db.get<any>("db1")).value).toBe("first")
134
+ expect((await db2.get<any>("db1")).value).toBe("second")
135
+ })
136
+ })
137
+ })
138
+ })
@@ -0,0 +1,83 @@
1
+ import * as redis from "../redis/init"
2
+ import * as tenancy from "../tenancy"
3
+ import * as context from "../context"
4
+ import * as platform from "../platform"
5
+ import env from "../environment"
6
+ import * as accounts from "../accounts"
7
+ import { UserDB } from "../users"
8
+ import { sdk } from "@budibase/shared-core"
9
+
10
+ const EXPIRY_SECONDS = 3600
11
+
12
+ /**
13
+ * The default populate user function
14
+ */
15
+ async function populateFromDB(userId: string, tenantId: string) {
16
+ const db = tenancy.getTenantDB(tenantId)
17
+ const user = await db.get<any>(userId)
18
+ user.budibaseAccess = true
19
+ if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
20
+ const account = await accounts.getAccount(user.email)
21
+ if (account) {
22
+ user.account = account
23
+ user.accountPortalAccess = true
24
+ }
25
+ }
26
+
27
+ return user
28
+ }
29
+
30
+ /**
31
+ * Get the requested user by id.
32
+ * Use redis cache to first read the user.
33
+ * If not present fallback to loading the user directly and re-caching.
34
+ * @param {*} userId the id of the user to get
35
+ * @param {*} tenantId the tenant of the user to get
36
+ * @param {*} populateUser function to provide the user for re-caching. default to couch db
37
+ * @returns
38
+ */
39
+ export async function getUser(
40
+ userId: string,
41
+ tenantId?: string,
42
+ populateUser?: any
43
+ ) {
44
+ if (!populateUser) {
45
+ populateUser = populateFromDB
46
+ }
47
+ if (!tenantId) {
48
+ try {
49
+ tenantId = context.getTenantId()
50
+ } catch (err) {
51
+ tenantId = await platform.users.lookupTenantId(userId)
52
+ }
53
+ }
54
+ const client = await redis.getUserClient()
55
+ // try cache
56
+ let user = await client.get(userId)
57
+ if (!user) {
58
+ user = await populateUser(userId, tenantId)
59
+ await client.store(userId, user, EXPIRY_SECONDS)
60
+ }
61
+ if (user && !user.tenantId && tenantId) {
62
+ // make sure the tenant ID is always correct/set
63
+ user.tenantId = tenantId
64
+ }
65
+ // if has groups, could have builder permissions granted by a group
66
+ if (user.userGroups && !sdk.users.isGlobalBuilder(user)) {
67
+ await context.doInTenant(tenantId, async () => {
68
+ const appIds = await UserDB.getGroupBuilderAppIds(user)
69
+ if (appIds.length) {
70
+ const existing = user.builder?.apps || []
71
+ user.builder = {
72
+ apps: [...new Set(existing.concat(appIds))],
73
+ }
74
+ }
75
+ })
76
+ }
77
+ return user
78
+ }
79
+
80
+ export async function invalidateUser(userId: string) {
81
+ const client = await redis.getUserClient()
82
+ await client.delete(userId)
83
+ }
@@ -0,0 +1,133 @@
1
+ import BaseCache from "./base"
2
+ import { getWritethroughClient } from "../redis/init"
3
+ import { logWarn } from "../logging"
4
+ import { Database, Document, LockName, LockType } from "@budibase/types"
5
+ import * as locks from "../redis/redlockImpl"
6
+
7
+ const DEFAULT_WRITE_RATE_MS = 10000
8
+ let CACHE: BaseCache | null = null
9
+
10
+ interface CacheItem {
11
+ doc: any
12
+ lastWrite: number
13
+ }
14
+
15
+ async function getCache() {
16
+ if (!CACHE) {
17
+ const client = await getWritethroughClient()
18
+ CACHE = new BaseCache(client)
19
+ }
20
+ return CACHE
21
+ }
22
+
23
+ function makeCacheKey(db: Database, key: string) {
24
+ return db.name + key
25
+ }
26
+
27
+ function makeCacheItem(doc: any, lastWrite: number | null = null): CacheItem {
28
+ return { doc, lastWrite: lastWrite || Date.now() }
29
+ }
30
+
31
+ async function put(
32
+ db: Database,
33
+ doc: Document,
34
+ writeRateMs: number = DEFAULT_WRITE_RATE_MS
35
+ ) {
36
+ const cache = await getCache()
37
+ const key = doc._id
38
+ let cacheItem: CacheItem | undefined
39
+ if (key) {
40
+ cacheItem = await cache.get(makeCacheKey(db, key))
41
+ }
42
+ const updateDb = !cacheItem || cacheItem.lastWrite < Date.now() - writeRateMs
43
+ let output = doc
44
+ if (updateDb) {
45
+ const lockResponse = await locks.doWithLock(
46
+ {
47
+ type: LockType.TRY_ONCE,
48
+ name: LockName.PERSIST_WRITETHROUGH,
49
+ resource: key,
50
+ ttl: 15000,
51
+ },
52
+ async () => {
53
+ const writeDb = async (toWrite: any) => {
54
+ // doc should contain the _id and _rev
55
+ const response = await db.put(toWrite, { force: true })
56
+ output = {
57
+ ...doc,
58
+ _id: response.id,
59
+ _rev: response.rev,
60
+ }
61
+ }
62
+ try {
63
+ await writeDb(doc)
64
+ } catch (err: any) {
65
+ if (err.status !== 409) {
66
+ throw err
67
+ } else {
68
+ // Swallow 409s but log them
69
+ logWarn(`Ignoring conflict in write-through cache`)
70
+ }
71
+ }
72
+ }
73
+ )
74
+
75
+ if (!lockResponse.executed) {
76
+ logWarn(`Ignoring redlock conflict in write-through cache`)
77
+ }
78
+ }
79
+ // if we are updating the DB then need to set the lastWrite to now
80
+ cacheItem = makeCacheItem(output, updateDb ? null : cacheItem?.lastWrite)
81
+ if (output._id) {
82
+ await cache.store(makeCacheKey(db, output._id), cacheItem)
83
+ }
84
+ return { ok: true, id: output._id, rev: output._rev }
85
+ }
86
+
87
+ async function get(db: Database, id: string): Promise<any> {
88
+ const cache = await getCache()
89
+ const cacheKey = makeCacheKey(db, id)
90
+ let cacheItem: CacheItem = await cache.get(cacheKey)
91
+ if (!cacheItem) {
92
+ const doc = await db.get(id)
93
+ cacheItem = makeCacheItem(doc)
94
+ await cache.store(cacheKey, cacheItem)
95
+ }
96
+ return cacheItem.doc
97
+ }
98
+
99
+ async function remove(db: Database, docOrId: any, rev?: any): Promise<void> {
100
+ const cache = await getCache()
101
+ if (!docOrId) {
102
+ throw new Error("No ID/Rev provided.")
103
+ }
104
+ const id = typeof docOrId === "string" ? docOrId : docOrId._id
105
+ rev = typeof docOrId === "string" ? rev : docOrId._rev
106
+ try {
107
+ await cache.delete(makeCacheKey(db, id))
108
+ } finally {
109
+ await db.remove(id, rev)
110
+ }
111
+ }
112
+
113
+ export class Writethrough {
114
+ db: Database
115
+ writeRateMs: number
116
+
117
+ constructor(db: Database, writeRateMs: number = DEFAULT_WRITE_RATE_MS) {
118
+ this.db = db
119
+ this.writeRateMs = writeRateMs
120
+ }
121
+
122
+ async put(doc: any) {
123
+ return put(this.db, doc, this.writeRateMs)
124
+ }
125
+
126
+ async get(id: string) {
127
+ return get(this.db, id)
128
+ }
129
+
130
+ async remove(docOrId: any, rev?: any) {
131
+ return remove(this.db, docOrId, rev)
132
+ }
133
+ }