@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.
- package/dist/index.js +5 -4
- package/dist/index.js.map +2 -2
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +6 -6
- package/dist/src/cache/appMetadata.js +1 -1
- package/dist/src/cache/appMetadata.js.map +1 -1
- package/dist/src/constants/misc.d.ts +0 -2
- package/dist/src/constants/misc.js +0 -2
- package/dist/src/constants/misc.js.map +1 -1
- package/dist/src/environment.js +5 -4
- package/dist/src/environment.js.map +1 -1
- package/dist/src/logging/system.d.ts +1 -1
- package/dist/src/timers/timers.d.ts +1 -1
- package/package.json +6 -6
- package/src/accounts/accounts.ts +82 -0
- package/src/accounts/api.ts +59 -0
- package/src/accounts/index.ts +1 -0
- package/src/auth/auth.ts +208 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/tests/auth.spec.ts +14 -0
- package/src/blacklist/blacklist.ts +54 -0
- package/src/blacklist/index.ts +1 -0
- package/src/blacklist/tests/blacklist.spec.ts +46 -0
- package/src/cache/appMetadata.ts +88 -0
- package/src/cache/base/index.ts +92 -0
- package/src/cache/generic.ts +30 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/tests/writethrough.spec.ts +138 -0
- package/src/cache/user.ts +83 -0
- package/src/cache/writethrough.ts +133 -0
- package/src/configs/configs.ts +257 -0
- package/src/configs/index.ts +1 -0
- package/src/configs/tests/configs.spec.ts +184 -0
- package/src/constants/db.ts +63 -0
- package/src/constants/index.ts +2 -0
- package/src/constants/misc.ts +50 -0
- package/src/context/Context.ts +14 -0
- package/src/context/identity.ts +58 -0
- package/src/context/index.ts +3 -0
- package/src/context/mainContext.ts +310 -0
- package/src/context/tests/index.spec.ts +147 -0
- package/src/context/types.ts +11 -0
- package/src/db/Replication.ts +84 -0
- package/src/db/constants.ts +10 -0
- package/src/db/couch/DatabaseImpl.ts +238 -0
- package/src/db/couch/connections.ts +77 -0
- package/src/db/couch/index.ts +5 -0
- package/src/db/couch/pouchDB.ts +97 -0
- package/src/db/couch/pouchDump.ts +0 -0
- package/src/db/couch/utils.ts +50 -0
- package/src/db/db.ts +43 -0
- package/src/db/errors.ts +14 -0
- package/src/db/index.ts +12 -0
- package/src/db/lucene.ts +750 -0
- package/src/db/searchIndexes/index.ts +1 -0
- package/src/db/searchIndexes/searchIndexes.ts +62 -0
- package/src/db/tests/index.spec.js +25 -0
- package/src/db/tests/lucene.spec.ts +368 -0
- package/src/db/tests/pouch.spec.js +62 -0
- package/src/db/tests/utils.spec.ts +63 -0
- package/src/db/utils.ts +207 -0
- package/src/db/views.ts +241 -0
- package/src/docIds/conversions.ts +59 -0
- package/src/docIds/ids.ts +113 -0
- package/src/docIds/index.ts +2 -0
- package/src/docIds/newid.ts +5 -0
- package/src/docIds/params.ts +174 -0
- package/src/docUpdates/index.ts +29 -0
- package/src/environment.ts +201 -0
- package/src/errors/errors.ts +119 -0
- package/src/errors/index.ts +1 -0
- package/src/events/analytics.ts +6 -0
- package/src/events/asyncEvents/index.ts +2 -0
- package/src/events/asyncEvents/publisher.ts +12 -0
- package/src/events/asyncEvents/queue.ts +22 -0
- package/src/events/backfill.ts +183 -0
- package/src/events/documentId.ts +56 -0
- package/src/events/events.ts +40 -0
- package/src/events/identification.ts +310 -0
- package/src/events/index.ts +14 -0
- package/src/events/processors/AnalyticsProcessor.ts +64 -0
- package/src/events/processors/AuditLogsProcessor.ts +93 -0
- package/src/events/processors/LoggingProcessor.ts +37 -0
- package/src/events/processors/Processors.ts +52 -0
- package/src/events/processors/async/DocumentUpdateProcessor.ts +43 -0
- package/src/events/processors/index.ts +19 -0
- package/src/events/processors/posthog/PosthogProcessor.ts +118 -0
- package/src/events/processors/posthog/index.ts +2 -0
- package/src/events/processors/posthog/rateLimiting.ts +106 -0
- package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +168 -0
- package/src/events/processors/types.ts +1 -0
- package/src/events/publishers/account.ts +35 -0
- package/src/events/publishers/app.ts +155 -0
- package/src/events/publishers/auditLog.ts +26 -0
- package/src/events/publishers/auth.ts +73 -0
- package/src/events/publishers/automation.ts +110 -0
- package/src/events/publishers/backfill.ts +74 -0
- package/src/events/publishers/backup.ts +42 -0
- package/src/events/publishers/datasource.ts +48 -0
- package/src/events/publishers/email.ts +17 -0
- package/src/events/publishers/environmentVariable.ts +38 -0
- package/src/events/publishers/group.ts +99 -0
- package/src/events/publishers/index.ts +24 -0
- package/src/events/publishers/installation.ts +38 -0
- package/src/events/publishers/layout.ts +26 -0
- package/src/events/publishers/license.ts +84 -0
- package/src/events/publishers/org.ts +37 -0
- package/src/events/publishers/plugin.ts +47 -0
- package/src/events/publishers/query.ts +88 -0
- package/src/events/publishers/role.ts +62 -0
- package/src/events/publishers/rows.ts +29 -0
- package/src/events/publishers/screen.ts +36 -0
- package/src/events/publishers/serve.ts +43 -0
- package/src/events/publishers/table.ts +70 -0
- package/src/events/publishers/user.ts +202 -0
- package/src/events/publishers/view.ts +107 -0
- package/src/features/index.ts +78 -0
- package/src/features/installation.ts +17 -0
- package/src/features/tests/featureFlags.spec.ts +85 -0
- package/src/helpers.ts +9 -0
- package/src/index.ts +54 -0
- package/src/installation.ts +107 -0
- package/src/logging/alerts.ts +26 -0
- package/src/logging/correlation/correlation.ts +13 -0
- package/src/logging/correlation/index.ts +1 -0
- package/src/logging/correlation/middleware.ts +17 -0
- package/src/logging/index.ts +4 -0
- package/src/logging/pino/logger.ts +232 -0
- package/src/logging/pino/middleware.ts +45 -0
- package/src/logging/system.ts +81 -0
- package/src/logging/tests/system.spec.ts +61 -0
- package/src/middleware/adminOnly.ts +9 -0
- package/src/middleware/auditLog.ts +6 -0
- package/src/middleware/authenticated.ts +193 -0
- package/src/middleware/builderOnly.ts +21 -0
- package/src/middleware/builderOrAdmin.ts +21 -0
- package/src/middleware/csrf.ts +81 -0
- package/src/middleware/errorHandling.ts +29 -0
- package/src/middleware/index.ts +21 -0
- package/src/middleware/internalApi.ts +23 -0
- package/src/middleware/joi-validator.ts +45 -0
- package/src/middleware/matchers.ts +47 -0
- package/src/middleware/passport/datasource/google.ts +95 -0
- package/src/middleware/passport/local.ts +54 -0
- package/src/middleware/passport/sso/google.ts +77 -0
- package/src/middleware/passport/sso/oidc.ts +154 -0
- package/src/middleware/passport/sso/sso.ts +165 -0
- package/src/middleware/passport/sso/tests/google.spec.ts +67 -0
- package/src/middleware/passport/sso/tests/oidc.spec.ts +152 -0
- package/src/middleware/passport/sso/tests/sso.spec.ts +197 -0
- package/src/middleware/passport/utils.ts +38 -0
- package/src/middleware/querystringToBody.ts +28 -0
- package/src/middleware/tenancy.ts +36 -0
- package/src/middleware/tests/builder.spec.ts +180 -0
- package/src/middleware/tests/matchers.spec.ts +134 -0
- package/src/migrations/definitions.ts +40 -0
- package/src/migrations/index.ts +2 -0
- package/src/migrations/migrations.ts +191 -0
- package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +11 -0
- package/src/migrations/tests/migrations.spec.ts +64 -0
- package/src/objectStore/buckets/app.ts +40 -0
- package/src/objectStore/buckets/global.ts +29 -0
- package/src/objectStore/buckets/index.ts +3 -0
- package/src/objectStore/buckets/plugins.ts +71 -0
- package/src/objectStore/buckets/tests/app.spec.ts +171 -0
- package/src/objectStore/buckets/tests/global.spec.ts +74 -0
- package/src/objectStore/buckets/tests/plugins.spec.ts +111 -0
- package/src/objectStore/cloudfront.ts +41 -0
- package/src/objectStore/index.ts +3 -0
- package/src/objectStore/objectStore.ts +440 -0
- package/src/objectStore/utils.ts +27 -0
- package/src/platform/index.ts +3 -0
- package/src/platform/platformDb.ts +6 -0
- package/src/platform/tenants.ts +101 -0
- package/src/platform/tests/tenants.spec.ts +26 -0
- package/src/platform/users.ts +90 -0
- package/src/plugin/index.ts +1 -0
- package/src/plugin/tests/validation.spec.ts +83 -0
- package/src/plugin/utils.ts +156 -0
- package/src/queue/constants.ts +6 -0
- package/src/queue/inMemoryQueue.ts +141 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/listeners.ts +195 -0
- package/src/queue/queue.ts +54 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/init.ts +86 -0
- package/src/redis/redis.ts +308 -0
- package/src/redis/redlockImpl.ts +139 -0
- package/src/redis/utils.ts +117 -0
- package/src/security/encryption.ts +179 -0
- package/src/security/permissions.ts +158 -0
- package/src/security/roles.ts +389 -0
- package/src/security/sessions.ts +120 -0
- package/src/security/tests/encryption.spec.ts +31 -0
- package/src/security/tests/permissions.spec.ts +145 -0
- package/src/security/tests/sessions.spec.ts +12 -0
- package/src/tenancy/db.ts +6 -0
- package/src/tenancy/index.ts +2 -0
- package/src/tenancy/tenancy.ts +140 -0
- package/src/tenancy/tests/tenancy.spec.ts +184 -0
- package/src/timers/index.ts +1 -0
- package/src/timers/timers.ts +22 -0
- package/src/users/db.ts +484 -0
- package/src/users/events.ts +176 -0
- package/src/users/index.ts +4 -0
- package/src/users/lookup.ts +102 -0
- package/src/users/users.ts +276 -0
- package/src/users/utils.ts +55 -0
- package/src/utils/hashing.ts +14 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/stringUtils.ts +8 -0
- package/src/utils/tests/utils.spec.ts +191 -0
- package/src/utils/utils.ts +239 -0
- package/tests/core/logging.ts +34 -0
- package/tests/core/utilities/index.ts +6 -0
- package/tests/core/utilities/jestUtils.ts +30 -0
- package/tests/core/utilities/mocks/alerts.ts +3 -0
- package/tests/core/utilities/mocks/date.ts +2 -0
- package/tests/core/utilities/mocks/events.ts +131 -0
- package/tests/core/utilities/mocks/fetch.ts +17 -0
- package/tests/core/utilities/mocks/index.ts +10 -0
- package/tests/core/utilities/mocks/licenses.ts +115 -0
- package/tests/core/utilities/mocks/posthog.ts +7 -0
- package/tests/core/utilities/structures/Chance.ts +20 -0
- package/tests/core/utilities/structures/accounts.ts +115 -0
- package/tests/core/utilities/structures/apps.ts +21 -0
- package/tests/core/utilities/structures/common.ts +7 -0
- package/tests/core/utilities/structures/db.ts +12 -0
- package/tests/core/utilities/structures/documents/index.ts +1 -0
- package/tests/core/utilities/structures/documents/platform/index.ts +1 -0
- package/tests/core/utilities/structures/documents/platform/installation.ts +12 -0
- package/tests/core/utilities/structures/generator.ts +2 -0
- package/tests/core/utilities/structures/index.ts +15 -0
- package/tests/core/utilities/structures/koa.ts +16 -0
- package/tests/core/utilities/structures/licenses.ts +167 -0
- package/tests/core/utilities/structures/plugins.ts +19 -0
- package/tests/core/utilities/structures/quotas.ts +67 -0
- package/tests/core/utilities/structures/scim.ts +80 -0
- package/tests/core/utilities/structures/shared.ts +19 -0
- package/tests/core/utilities/structures/sso.ts +119 -0
- package/tests/core/utilities/structures/tenants.ts +5 -0
- package/tests/core/utilities/structures/userGroups.ts +10 -0
- package/tests/core/utilities/structures/users.ts +73 -0
- package/tests/core/utilities/testContainerUtils.ts +85 -0
- package/tests/core/utilities/utils/index.ts +1 -0
- package/tests/core/utilities/utils/time.ts +3 -0
- package/tests/extra/DBTestConfiguration.ts +36 -0
- package/tests/extra/index.ts +2 -0
- package/tests/extra/testEnv.ts +95 -0
- package/tests/index.ts +1 -0
- package/tests/jestEnv.ts +6 -0
- 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,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
|
+
}
|