@budibase/backend-core 3.2.4 → 3.2.6
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.map +1 -1
- package/dist/index.js.meta.json +1 -1
- package/dist/package.json +11 -4
- package/dist/plugins.js.meta.json +1 -1
- package/package.json +11 -4
- package/src/accounts/accounts.ts +0 -82
- package/src/accounts/api.ts +0 -59
- package/src/accounts/index.ts +0 -1
- package/src/auth/auth.ts +0 -210
- package/src/auth/index.ts +0 -1
- package/src/auth/tests/auth.spec.ts +0 -14
- package/src/blacklist/blacklist.ts +0 -54
- package/src/blacklist/index.ts +0 -1
- package/src/blacklist/tests/blacklist.spec.ts +0 -46
- package/src/cache/appMetadata.ts +0 -88
- package/src/cache/base/index.ts +0 -150
- package/src/cache/docWritethrough.ts +0 -105
- package/src/cache/generic.ts +0 -33
- package/src/cache/index.ts +0 -8
- package/src/cache/invite.ts +0 -86
- package/src/cache/passwordReset.ts +0 -49
- package/src/cache/tests/docWritethrough.spec.ts +0 -296
- package/src/cache/tests/user.spec.ts +0 -145
- package/src/cache/tests/writethrough.spec.ts +0 -139
- package/src/cache/user.ts +0 -154
- package/src/cache/writethrough.ts +0 -133
- package/src/configs/configs.ts +0 -263
- package/src/configs/index.ts +0 -1
- package/src/configs/tests/configs.spec.ts +0 -184
- package/src/constants/db.ts +0 -75
- package/src/constants/index.ts +0 -2
- package/src/constants/misc.ts +0 -36
- package/src/context/Context.ts +0 -14
- package/src/context/identity.ts +0 -58
- package/src/context/index.ts +0 -3
- package/src/context/mainContext.ts +0 -422
- package/src/context/tests/index.spec.ts +0 -255
- package/src/context/types.ts +0 -26
- package/src/db/Replication.ts +0 -94
- package/src/db/couch/DatabaseImpl.ts +0 -511
- package/src/db/couch/connections.ts +0 -89
- package/src/db/couch/index.ts +0 -4
- package/src/db/couch/pouchDB.ts +0 -97
- package/src/db/couch/pouchDump.ts +0 -0
- package/src/db/couch/tests/DatabaseImpl.spec.ts +0 -118
- package/src/db/couch/utils.ts +0 -55
- package/src/db/db.ts +0 -34
- package/src/db/errors.ts +0 -14
- package/src/db/index.ts +0 -12
- package/src/db/instrumentation.ts +0 -199
- package/src/db/lucene.ts +0 -721
- package/src/db/searchIndexes/index.ts +0 -1
- package/src/db/searchIndexes/searchIndexes.ts +0 -62
- package/src/db/tests/DatabaseImpl.spec.ts +0 -55
- package/src/db/tests/connections.spec.ts +0 -22
- package/src/db/tests/index.spec.ts +0 -32
- package/src/db/tests/lucene.spec.ts +0 -400
- package/src/db/tests/pouch.spec.js +0 -62
- package/src/db/tests/utils.spec.ts +0 -63
- package/src/db/utils.ts +0 -208
- package/src/db/views.ts +0 -245
- package/src/docIds/conversions.ts +0 -60
- package/src/docIds/ids.ts +0 -126
- package/src/docIds/index.ts +0 -2
- package/src/docIds/newid.ts +0 -5
- package/src/docIds/params.ts +0 -189
- package/src/docUpdates/index.ts +0 -24
- package/src/environment.ts +0 -293
- package/src/errors/errors.ts +0 -119
- package/src/errors/index.ts +0 -1
- package/src/events/analytics.ts +0 -6
- package/src/events/asyncEvents/index.ts +0 -2
- package/src/events/asyncEvents/publisher.ts +0 -12
- package/src/events/asyncEvents/queue.ts +0 -22
- package/src/events/backfill.ts +0 -183
- package/src/events/documentId.ts +0 -56
- package/src/events/events.ts +0 -47
- package/src/events/identification.ts +0 -311
- package/src/events/index.ts +0 -15
- package/src/events/processors/AnalyticsProcessor.ts +0 -64
- package/src/events/processors/AuditLogsProcessor.ts +0 -92
- package/src/events/processors/LoggingProcessor.ts +0 -36
- package/src/events/processors/Processors.ts +0 -52
- package/src/events/processors/async/DocumentUpdateProcessor.ts +0 -38
- package/src/events/processors/index.ts +0 -19
- package/src/events/processors/posthog/PosthogProcessor.ts +0 -118
- package/src/events/processors/posthog/index.ts +0 -3
- package/src/events/processors/posthog/rateLimiting.ts +0 -106
- package/src/events/processors/posthog/tests/PosthogProcessor.spec.ts +0 -164
- package/src/events/processors/types.ts +0 -1
- package/src/events/publishers/account.ts +0 -41
- package/src/events/publishers/ai.ts +0 -21
- package/src/events/publishers/app.ts +0 -168
- package/src/events/publishers/auditLog.ts +0 -26
- package/src/events/publishers/auth.ts +0 -73
- package/src/events/publishers/automation.ts +0 -110
- package/src/events/publishers/backfill.ts +0 -74
- package/src/events/publishers/backup.ts +0 -42
- package/src/events/publishers/datasource.ts +0 -48
- package/src/events/publishers/email.ts +0 -17
- package/src/events/publishers/environmentVariable.ts +0 -38
- package/src/events/publishers/group.ts +0 -99
- package/src/events/publishers/index.ts +0 -25
- package/src/events/publishers/installation.ts +0 -38
- package/src/events/publishers/layout.ts +0 -26
- package/src/events/publishers/license.ts +0 -84
- package/src/events/publishers/org.ts +0 -37
- package/src/events/publishers/plugin.ts +0 -47
- package/src/events/publishers/query.ts +0 -89
- package/src/events/publishers/role.ts +0 -62
- package/src/events/publishers/rows.ts +0 -29
- package/src/events/publishers/screen.ts +0 -36
- package/src/events/publishers/serve.ts +0 -43
- package/src/events/publishers/table.ts +0 -70
- package/src/events/publishers/user.ts +0 -202
- package/src/events/publishers/view.ts +0 -107
- package/src/features/features.ts +0 -277
- package/src/features/index.ts +0 -2
- package/src/features/tests/features.spec.ts +0 -267
- package/src/features/tests/utils.ts +0 -64
- package/src/helpers.ts +0 -9
- package/src/index.ts +0 -59
- package/src/installation.ts +0 -115
- package/src/logging/alerts.ts +0 -26
- package/src/logging/correlation/correlation.ts +0 -15
- package/src/logging/correlation/index.ts +0 -1
- package/src/logging/correlation/middleware.ts +0 -18
- package/src/logging/index.ts +0 -4
- package/src/logging/pino/logger.ts +0 -239
- package/src/logging/pino/middleware.ts +0 -48
- package/src/logging/system.ts +0 -81
- package/src/logging/tests/system.spec.ts +0 -61
- package/src/middleware/adminOnly.ts +0 -9
- package/src/middleware/auditLog.ts +0 -6
- package/src/middleware/authenticated.ts +0 -247
- package/src/middleware/builderOnly.ts +0 -21
- package/src/middleware/builderOrAdmin.ts +0 -21
- package/src/middleware/contentSecurityPolicy.ts +0 -113
- package/src/middleware/csrf.ts +0 -81
- package/src/middleware/errorHandling.ts +0 -43
- package/src/middleware/index.ts +0 -24
- package/src/middleware/internalApi.ts +0 -23
- package/src/middleware/ip.ts +0 -12
- package/src/middleware/joi-validator.ts +0 -58
- package/src/middleware/matchers.ts +0 -39
- package/src/middleware/passport/datasource/google.ts +0 -102
- package/src/middleware/passport/local.ts +0 -54
- package/src/middleware/passport/sso/google.ts +0 -77
- package/src/middleware/passport/sso/oidc.ts +0 -152
- package/src/middleware/passport/sso/sso.ts +0 -138
- package/src/middleware/passport/sso/tests/google.spec.ts +0 -68
- package/src/middleware/passport/sso/tests/oidc.spec.ts +0 -144
- package/src/middleware/passport/sso/tests/sso.spec.ts +0 -197
- package/src/middleware/passport/utils.ts +0 -38
- package/src/middleware/querystringToBody.ts +0 -28
- package/src/middleware/tenancy.ts +0 -36
- package/src/middleware/tests/builder.spec.ts +0 -181
- package/src/middleware/tests/contentSecurityPolicy.spec.ts +0 -75
- package/src/middleware/tests/matchers.spec.ts +0 -100
- package/src/migrations/definitions.ts +0 -40
- package/src/migrations/index.ts +0 -2
- package/src/migrations/migrations.ts +0 -186
- package/src/migrations/tests/__snapshots__/migrations.spec.ts.snap +0 -11
- package/src/migrations/tests/migrations.spec.ts +0 -64
- package/src/objectStore/buckets/app.ts +0 -53
- package/src/objectStore/buckets/global.ts +0 -29
- package/src/objectStore/buckets/index.ts +0 -3
- package/src/objectStore/buckets/plugins.ts +0 -71
- package/src/objectStore/buckets/tests/app.spec.ts +0 -161
- package/src/objectStore/buckets/tests/global.spec.ts +0 -74
- package/src/objectStore/buckets/tests/plugins.spec.ts +0 -111
- package/src/objectStore/cloudfront.ts +0 -41
- package/src/objectStore/index.ts +0 -3
- package/src/objectStore/objectStore.ts +0 -585
- package/src/objectStore/utils.ts +0 -113
- package/src/platform/index.ts +0 -3
- package/src/platform/platformDb.ts +0 -6
- package/src/platform/tenants.ts +0 -101
- package/src/platform/tests/tenants.spec.ts +0 -26
- package/src/platform/users.ts +0 -129
- package/src/plugin/index.ts +0 -1
- package/src/plugin/tests/validation.spec.ts +0 -209
- package/src/plugin/utils.ts +0 -175
- package/src/queue/constants.ts +0 -8
- package/src/queue/inMemoryQueue.ts +0 -189
- package/src/queue/index.ts +0 -2
- package/src/queue/listeners.ts +0 -199
- package/src/queue/queue.ts +0 -84
- package/src/redis/index.ts +0 -6
- package/src/redis/init.ts +0 -118
- package/src/redis/redis.ts +0 -358
- package/src/redis/redlockImpl.ts +0 -155
- package/src/redis/tests/redis.spec.ts +0 -207
- package/src/redis/tests/redlockImpl.spec.ts +0 -105
- package/src/redis/utils.ts +0 -128
- package/src/security/auth.ts +0 -24
- package/src/security/encryption.ts +0 -185
- package/src/security/index.ts +0 -1
- package/src/security/permissions.ts +0 -166
- package/src/security/roles.ts +0 -655
- package/src/security/secrets.ts +0 -20
- package/src/security/sessions.ts +0 -123
- package/src/security/tests/auth.spec.ts +0 -45
- package/src/security/tests/encryption.spec.ts +0 -31
- package/src/security/tests/permissions.spec.ts +0 -146
- package/src/security/tests/secrets.spec.ts +0 -35
- package/src/security/tests/sessions.spec.ts +0 -12
- package/src/sql/designDoc.ts +0 -17
- package/src/sql/index.ts +0 -5
- package/src/sql/sql.ts +0 -1854
- package/src/sql/sqlTable.ts +0 -319
- package/src/sql/utils.ts +0 -193
- package/src/tenancy/db.ts +0 -6
- package/src/tenancy/index.ts +0 -2
- package/src/tenancy/tenancy.ts +0 -148
- package/src/tenancy/tests/tenancy.spec.ts +0 -184
- package/src/timers/index.ts +0 -1
- package/src/timers/timers.ts +0 -22
- package/src/users/db.ts +0 -582
- package/src/users/events.ts +0 -176
- package/src/users/index.ts +0 -4
- package/src/users/lookup.ts +0 -99
- package/src/users/test/db.spec.ts +0 -188
- package/src/users/test/utils.spec.ts +0 -67
- package/src/users/users.ts +0 -353
- package/src/users/utils.ts +0 -81
- package/src/utils/Duration.ts +0 -56
- package/src/utils/hashing.ts +0 -15
- package/src/utils/index.ts +0 -4
- package/src/utils/stringUtils.ts +0 -8
- package/src/utils/tests/Duration.spec.ts +0 -19
- package/src/utils/tests/utils.spec.ts +0 -204
- package/src/utils/utils.ts +0 -249
- package/tests/core/logging.ts +0 -34
- package/tests/core/users/users.spec.js +0 -53
- package/tests/core/utilities/index.ts +0 -7
- package/tests/core/utilities/jestUtils.ts +0 -33
- package/tests/core/utilities/mocks/alerts.ts +0 -4
- package/tests/core/utilities/mocks/date.ts +0 -3
- package/tests/core/utilities/mocks/events.ts +0 -132
- package/tests/core/utilities/mocks/index.ts +0 -9
- package/tests/core/utilities/mocks/licenses.ts +0 -119
- package/tests/core/utilities/queue.ts +0 -9
- package/tests/core/utilities/structures/Chance.ts +0 -20
- package/tests/core/utilities/structures/accounts.ts +0 -80
- package/tests/core/utilities/structures/apps.ts +0 -21
- package/tests/core/utilities/structures/common.ts +0 -7
- package/tests/core/utilities/structures/db.ts +0 -12
- package/tests/core/utilities/structures/documents/index.ts +0 -1
- package/tests/core/utilities/structures/documents/platform/index.ts +0 -1
- package/tests/core/utilities/structures/documents/platform/installation.ts +0 -12
- package/tests/core/utilities/structures/generator.ts +0 -3
- package/tests/core/utilities/structures/index.ts +0 -15
- package/tests/core/utilities/structures/koa.ts +0 -16
- package/tests/core/utilities/structures/licenses.ts +0 -190
- package/tests/core/utilities/structures/plugins.ts +0 -19
- package/tests/core/utilities/structures/quotas.ts +0 -72
- package/tests/core/utilities/structures/scim.ts +0 -80
- package/tests/core/utilities/structures/sso.ts +0 -118
- package/tests/core/utilities/structures/tenants.ts +0 -5
- package/tests/core/utilities/structures/userGroups.ts +0 -10
- package/tests/core/utilities/structures/users.ts +0 -89
- package/tests/core/utilities/testContainerUtils.ts +0 -165
- package/tests/core/utilities/utils/index.ts +0 -2
- package/tests/core/utilities/utils/queue.ts +0 -27
- package/tests/core/utilities/utils/time.ts +0 -3
- package/tests/extra/DBTestConfiguration.ts +0 -36
- package/tests/extra/index.ts +0 -2
- package/tests/extra/testEnv.ts +0 -95
- package/tests/index.ts +0 -2
- package/tests/jestEnv.ts +0 -10
- package/tests/jestSetup.ts +0 -36
package/src/redis/redis.ts
DELETED
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import env from "../environment"
|
|
2
|
-
import Redis, { Cluster } from "ioredis"
|
|
3
|
-
// mock-redis doesn't have any typing
|
|
4
|
-
let MockRedis: any | undefined
|
|
5
|
-
if (env.MOCK_REDIS) {
|
|
6
|
-
try {
|
|
7
|
-
// ioredis mock is all in memory
|
|
8
|
-
MockRedis = require("ioredis-mock")
|
|
9
|
-
} catch (err) {
|
|
10
|
-
console.log("Mock redis unavailable")
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
import {
|
|
14
|
-
addDbPrefix,
|
|
15
|
-
removeDbPrefix,
|
|
16
|
-
getRedisOptions,
|
|
17
|
-
SEPARATOR,
|
|
18
|
-
SelectableDatabase,
|
|
19
|
-
getRedisConnectionDetails,
|
|
20
|
-
} from "./utils"
|
|
21
|
-
import { logAlert } from "../logging"
|
|
22
|
-
import * as timers from "../timers"
|
|
23
|
-
|
|
24
|
-
const RETRY_PERIOD_MS = 2000
|
|
25
|
-
const STARTUP_TIMEOUT_MS = 5000
|
|
26
|
-
const CLUSTERED = env.REDIS_CLUSTERED
|
|
27
|
-
const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
|
|
28
|
-
|
|
29
|
-
// for testing just generate the client once
|
|
30
|
-
let CLOSED = false
|
|
31
|
-
const CLIENTS: Record<number, Redis> = {}
|
|
32
|
-
let CONNECTED = false
|
|
33
|
-
|
|
34
|
-
// mock redis always connected
|
|
35
|
-
if (env.MOCK_REDIS) {
|
|
36
|
-
CONNECTED = true
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function pickClient(selectDb: number) {
|
|
40
|
-
return CLIENTS[selectDb]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function connectionError(timeout: NodeJS.Timeout, err: Error | string) {
|
|
44
|
-
// manually shut down, ignore errors
|
|
45
|
-
if (CLOSED) {
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
CLOSED = true
|
|
49
|
-
// always clear this on error
|
|
50
|
-
clearTimeout(timeout)
|
|
51
|
-
CONNECTED = false
|
|
52
|
-
logAlert("Redis connection failed", err)
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
init()
|
|
55
|
-
}, RETRY_PERIOD_MS)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Inits the system, will error if unable to connect to redis cluster (may take up to 10 seconds) otherwise
|
|
60
|
-
* will return the ioredis client which will be ready to use.
|
|
61
|
-
*/
|
|
62
|
-
function init(selectDb = DEFAULT_SELECT_DB) {
|
|
63
|
-
const RedisCore = env.MOCK_REDIS && MockRedis ? MockRedis : Redis
|
|
64
|
-
let timeout: NodeJS.Timeout
|
|
65
|
-
CLOSED = false
|
|
66
|
-
let client = pickClient(selectDb)
|
|
67
|
-
// already connected, ignore
|
|
68
|
-
if (client && CONNECTED) {
|
|
69
|
-
return
|
|
70
|
-
}
|
|
71
|
-
// testing uses a single in memory client
|
|
72
|
-
if (env.MOCK_REDIS) {
|
|
73
|
-
CLIENTS[selectDb] = new RedisCore(getRedisOptions())
|
|
74
|
-
}
|
|
75
|
-
// start the timer - only allowed 5 seconds to connect
|
|
76
|
-
timeout = setTimeout(() => {
|
|
77
|
-
if (!CONNECTED) {
|
|
78
|
-
connectionError(timeout, "Did not successfully connect in timeout")
|
|
79
|
-
}
|
|
80
|
-
}, STARTUP_TIMEOUT_MS)
|
|
81
|
-
|
|
82
|
-
// disconnect any lingering client
|
|
83
|
-
if (client) {
|
|
84
|
-
client.disconnect()
|
|
85
|
-
}
|
|
86
|
-
const { host, port } = getRedisConnectionDetails()
|
|
87
|
-
const opts = getRedisOptions()
|
|
88
|
-
|
|
89
|
-
if (CLUSTERED) {
|
|
90
|
-
client = new RedisCore.Cluster([{ host, port }], opts)
|
|
91
|
-
} else {
|
|
92
|
-
client = new RedisCore(opts)
|
|
93
|
-
}
|
|
94
|
-
// attach handlers
|
|
95
|
-
client.on("end", (err: Error) => {
|
|
96
|
-
if (env.isTest()) {
|
|
97
|
-
// don't try to re-connect in test env
|
|
98
|
-
// allow the process to exit
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
connectionError(timeout, err)
|
|
102
|
-
})
|
|
103
|
-
client.on("error", (err: Error) => {
|
|
104
|
-
connectionError(timeout, err)
|
|
105
|
-
})
|
|
106
|
-
client.on("connect", () => {
|
|
107
|
-
console.log(`Connected to Redis DB: ${selectDb}`)
|
|
108
|
-
clearTimeout(timeout)
|
|
109
|
-
CONNECTED = true
|
|
110
|
-
})
|
|
111
|
-
CLIENTS[selectDb] = client
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function closeAll() {
|
|
115
|
-
Object.values(CLIENTS).forEach(client => client.disconnect())
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) {
|
|
119
|
-
return new Promise(resolve => {
|
|
120
|
-
if (pickClient(selectDb) == null) {
|
|
121
|
-
init()
|
|
122
|
-
} else if (CONNECTED) {
|
|
123
|
-
resolve("")
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
// check if the connection is ready
|
|
127
|
-
const interval = timers.set(() => {
|
|
128
|
-
if (CONNECTED) {
|
|
129
|
-
timers.clear(interval)
|
|
130
|
-
resolve("")
|
|
131
|
-
}
|
|
132
|
-
}, 500)
|
|
133
|
-
})
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Utility function, takes a redis stream and converts it to a promisified response -
|
|
138
|
-
* this can only be done with redis streams because they will have an end.
|
|
139
|
-
* @param stream A redis stream, specifically as this type of stream will have an end.
|
|
140
|
-
* @param client The client to use for further lookups.
|
|
141
|
-
* @return The final output of the stream
|
|
142
|
-
*/
|
|
143
|
-
function promisifyStream(stream: any, client: RedisWrapper) {
|
|
144
|
-
return new Promise((resolve, reject) => {
|
|
145
|
-
const outputKeys = new Set()
|
|
146
|
-
stream.on("data", (keys: string[]) => {
|
|
147
|
-
keys.forEach(key => {
|
|
148
|
-
outputKeys.add(key)
|
|
149
|
-
})
|
|
150
|
-
})
|
|
151
|
-
stream.on("error", (err: Error) => {
|
|
152
|
-
reject(err)
|
|
153
|
-
})
|
|
154
|
-
stream.on("end", async () => {
|
|
155
|
-
const keysArray: string[] = Array.from(outputKeys) as string[]
|
|
156
|
-
try {
|
|
157
|
-
let getPromises = []
|
|
158
|
-
for (let key of keysArray) {
|
|
159
|
-
getPromises.push(client.get(key))
|
|
160
|
-
}
|
|
161
|
-
const jsonArray = await Promise.all(getPromises)
|
|
162
|
-
resolve(
|
|
163
|
-
keysArray.map(key => ({
|
|
164
|
-
key: removeDbPrefix(key),
|
|
165
|
-
value: JSON.parse(jsonArray.shift()),
|
|
166
|
-
}))
|
|
167
|
-
)
|
|
168
|
-
} catch (err) {
|
|
169
|
-
reject(err)
|
|
170
|
-
}
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
class RedisWrapper {
|
|
176
|
-
_db: string
|
|
177
|
-
_select: number
|
|
178
|
-
|
|
179
|
-
constructor(db: string, selectDb: number | null = null) {
|
|
180
|
-
this._db = db
|
|
181
|
-
this._select = selectDb || DEFAULT_SELECT_DB
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
getClient() {
|
|
185
|
-
return pickClient(this._select)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async init() {
|
|
189
|
-
CLOSED = false
|
|
190
|
-
init(this._select)
|
|
191
|
-
await waitForConnection(this._select)
|
|
192
|
-
if (this._select && !env.isTest()) {
|
|
193
|
-
this.getClient().select(this._select)
|
|
194
|
-
}
|
|
195
|
-
return this
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async finish() {
|
|
199
|
-
CLOSED = true
|
|
200
|
-
this.getClient().disconnect()
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async scan(key = ""): Promise<any> {
|
|
204
|
-
const db = this._db
|
|
205
|
-
key = `${db}${SEPARATOR}${key}`
|
|
206
|
-
let stream
|
|
207
|
-
if (CLUSTERED) {
|
|
208
|
-
let node = (this.getClient() as never as Cluster).nodes("master")
|
|
209
|
-
stream = node[0].scanStream({ match: key + "*", count: 100 })
|
|
210
|
-
} else {
|
|
211
|
-
stream = (this.getClient() as Redis).scanStream({
|
|
212
|
-
match: key + "*",
|
|
213
|
-
count: 100,
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
return promisifyStream(stream, this.getClient() as any)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async keys(pattern: string) {
|
|
220
|
-
const db = this._db
|
|
221
|
-
return this.getClient().keys(addDbPrefix(db, pattern))
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async exists(key: string) {
|
|
225
|
-
const db = this._db
|
|
226
|
-
return await this.getClient().exists(addDbPrefix(db, key))
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async get(key: string) {
|
|
230
|
-
const db = this._db
|
|
231
|
-
const response = await this.getClient().get(addDbPrefix(db, key))
|
|
232
|
-
// overwrite the prefixed key
|
|
233
|
-
// @ts-ignore
|
|
234
|
-
if (response != null && response.key) {
|
|
235
|
-
// @ts-ignore
|
|
236
|
-
response.key = key
|
|
237
|
-
}
|
|
238
|
-
// if its not an object just return the response
|
|
239
|
-
try {
|
|
240
|
-
return JSON.parse(response!)
|
|
241
|
-
} catch (err) {
|
|
242
|
-
return response
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async bulkGet<T>(keys: string[]) {
|
|
247
|
-
const db = this._db
|
|
248
|
-
if (keys.length === 0) {
|
|
249
|
-
return {}
|
|
250
|
-
}
|
|
251
|
-
const prefixedKeys = keys.map(key => addDbPrefix(db, key))
|
|
252
|
-
let response = await this.getClient().mget(prefixedKeys)
|
|
253
|
-
if (Array.isArray(response)) {
|
|
254
|
-
let final: Record<string, T> = {}
|
|
255
|
-
let count = 0
|
|
256
|
-
for (let result of response) {
|
|
257
|
-
if (result) {
|
|
258
|
-
let parsed
|
|
259
|
-
try {
|
|
260
|
-
parsed = JSON.parse(result)
|
|
261
|
-
} catch (err) {
|
|
262
|
-
parsed = result
|
|
263
|
-
}
|
|
264
|
-
final[keys[count]] = parsed
|
|
265
|
-
}
|
|
266
|
-
count++
|
|
267
|
-
}
|
|
268
|
-
return final
|
|
269
|
-
} else {
|
|
270
|
-
throw new Error(`Invalid response: ${response}`)
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async store(key: string, value: any, expirySeconds: number | null = null) {
|
|
275
|
-
const db = this._db
|
|
276
|
-
if (typeof value === "object") {
|
|
277
|
-
value = JSON.stringify(value)
|
|
278
|
-
}
|
|
279
|
-
const prefixedKey = addDbPrefix(db, key)
|
|
280
|
-
await this.getClient().set(prefixedKey, value)
|
|
281
|
-
if (expirySeconds) {
|
|
282
|
-
await this.getClient().expire(prefixedKey, expirySeconds)
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async bulkStore(
|
|
287
|
-
data: Record<string, any>,
|
|
288
|
-
expirySeconds: number | null = null
|
|
289
|
-
) {
|
|
290
|
-
const client = this.getClient()
|
|
291
|
-
|
|
292
|
-
const dataToStore = Object.entries(data).reduce((acc, [key, value]) => {
|
|
293
|
-
acc[addDbPrefix(this._db, key)] =
|
|
294
|
-
typeof value === "object" ? JSON.stringify(value) : value
|
|
295
|
-
return acc
|
|
296
|
-
}, {} as Record<string, any>)
|
|
297
|
-
|
|
298
|
-
const pipeline = client.pipeline()
|
|
299
|
-
pipeline.mset(dataToStore)
|
|
300
|
-
|
|
301
|
-
if (expirySeconds !== null) {
|
|
302
|
-
for (const key of Object.keys(dataToStore)) {
|
|
303
|
-
pipeline.expire(key, expirySeconds)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
await pipeline.exec()
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async getTTL(key: string) {
|
|
311
|
-
const db = this._db
|
|
312
|
-
const prefixedKey = addDbPrefix(db, key)
|
|
313
|
-
return this.getClient().ttl(prefixedKey)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async setExpiry(key: string, expirySeconds: number) {
|
|
317
|
-
const db = this._db
|
|
318
|
-
const prefixedKey = addDbPrefix(db, key)
|
|
319
|
-
await this.getClient().expire(prefixedKey, expirySeconds)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async delete(key: string) {
|
|
323
|
-
const db = this._db
|
|
324
|
-
await this.getClient().del(addDbPrefix(db, key))
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async bulkDelete(keys: string[]) {
|
|
328
|
-
const db = this._db
|
|
329
|
-
await this.getClient().del(keys.map(key => addDbPrefix(db, key)))
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async clear() {
|
|
333
|
-
let items = await this.scan()
|
|
334
|
-
await Promise.all(items.map((obj: any) => this.delete(obj.key)))
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async increment(key: string) {
|
|
338
|
-
const result = await this.getClient().incr(addDbPrefix(this._db, key))
|
|
339
|
-
if (isNaN(result)) {
|
|
340
|
-
throw new Error(`Redis ${key} does not contain a number`)
|
|
341
|
-
}
|
|
342
|
-
return result
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async deleteIfValue(key: string, value: any) {
|
|
346
|
-
const client = this.getClient()
|
|
347
|
-
|
|
348
|
-
const luaScript = `
|
|
349
|
-
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
350
|
-
redis.call('DEL', KEYS[1])
|
|
351
|
-
end
|
|
352
|
-
`
|
|
353
|
-
|
|
354
|
-
await client.eval(luaScript, 1, addDbPrefix(this._db, key), value)
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export default RedisWrapper
|
package/src/redis/redlockImpl.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import Redlock from "redlock"
|
|
2
|
-
import { getLockClient } from "./init"
|
|
3
|
-
import { LockOptions, LockType } from "@budibase/types"
|
|
4
|
-
import * as context from "../context"
|
|
5
|
-
import { utils } from "@budibase/shared-core"
|
|
6
|
-
import { Duration } from "../utils"
|
|
7
|
-
|
|
8
|
-
async function getClient(
|
|
9
|
-
type: LockType,
|
|
10
|
-
opts?: Redlock.Options
|
|
11
|
-
): Promise<Redlock> {
|
|
12
|
-
if (type === LockType.CUSTOM) {
|
|
13
|
-
return newRedlock(opts)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
switch (type) {
|
|
17
|
-
case LockType.TRY_ONCE: {
|
|
18
|
-
return newRedlock(OPTIONS.TRY_ONCE)
|
|
19
|
-
}
|
|
20
|
-
case LockType.TRY_TWICE: {
|
|
21
|
-
return newRedlock(OPTIONS.TRY_TWICE)
|
|
22
|
-
}
|
|
23
|
-
case LockType.DEFAULT: {
|
|
24
|
-
return newRedlock(OPTIONS.DEFAULT)
|
|
25
|
-
}
|
|
26
|
-
case LockType.DELAY_500: {
|
|
27
|
-
return newRedlock(OPTIONS.DELAY_500)
|
|
28
|
-
}
|
|
29
|
-
case LockType.AUTO_EXTEND: {
|
|
30
|
-
return newRedlock(OPTIONS.AUTO_EXTEND)
|
|
31
|
-
}
|
|
32
|
-
default: {
|
|
33
|
-
throw utils.unreachable(type)
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const OPTIONS: Record<keyof typeof LockType, Redlock.Options> = {
|
|
39
|
-
TRY_ONCE: {
|
|
40
|
-
// immediately throws an error if the lock is already held
|
|
41
|
-
retryCount: 0,
|
|
42
|
-
},
|
|
43
|
-
TRY_TWICE: {
|
|
44
|
-
retryCount: 1,
|
|
45
|
-
},
|
|
46
|
-
DEFAULT: {
|
|
47
|
-
// the expected clock drift; for more details
|
|
48
|
-
// see http://redis.io/topics/distlock
|
|
49
|
-
driftFactor: 0.01, // multiplied by lock ttl to determine drift time
|
|
50
|
-
|
|
51
|
-
// the max number of times Redlock will attempt
|
|
52
|
-
// to lock a resource before erroring
|
|
53
|
-
retryCount: 10,
|
|
54
|
-
|
|
55
|
-
// the time in ms between attempts
|
|
56
|
-
retryDelay: 200, // time in ms
|
|
57
|
-
|
|
58
|
-
// the max time in ms randomly added to retries
|
|
59
|
-
// to improve performance under high contention
|
|
60
|
-
// see https://www.awsarchitectureblog.com/2015/03/backoff.html
|
|
61
|
-
retryJitter: 100, // time in ms
|
|
62
|
-
},
|
|
63
|
-
DELAY_500: {
|
|
64
|
-
retryDelay: 500,
|
|
65
|
-
},
|
|
66
|
-
CUSTOM: {},
|
|
67
|
-
AUTO_EXTEND: {
|
|
68
|
-
retryCount: -1,
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function newRedlock(opts: Redlock.Options = {}) {
|
|
73
|
-
const options = { ...OPTIONS.DEFAULT, ...opts }
|
|
74
|
-
const redisWrapper = await getLockClient()
|
|
75
|
-
const client = redisWrapper.getClient() as any
|
|
76
|
-
return new Redlock([client], options)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type SuccessfulRedlockExecution<T> = {
|
|
80
|
-
executed: true
|
|
81
|
-
result: T
|
|
82
|
-
}
|
|
83
|
-
type UnsuccessfulRedlockExecution = {
|
|
84
|
-
executed: false
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
type RedlockExecution<T> =
|
|
88
|
-
| SuccessfulRedlockExecution<T>
|
|
89
|
-
| UnsuccessfulRedlockExecution
|
|
90
|
-
|
|
91
|
-
function getLockName(opts: LockOptions) {
|
|
92
|
-
// determine lock name
|
|
93
|
-
// by default use the tenantId for uniqueness, unless using a system lock
|
|
94
|
-
const prefix = opts.systemLock ? "system" : context.getTenantId()
|
|
95
|
-
let name: string = `lock:${prefix}_${opts.name}`
|
|
96
|
-
// add additional unique name if required
|
|
97
|
-
if (opts.resource) {
|
|
98
|
-
name = name + `_${opts.resource}`
|
|
99
|
-
}
|
|
100
|
-
return name
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export const AUTO_EXTEND_POLLING_MS = Duration.fromSeconds(10).toMs()
|
|
104
|
-
|
|
105
|
-
export async function doWithLock<T>(
|
|
106
|
-
opts: LockOptions,
|
|
107
|
-
task: () => Promise<T>
|
|
108
|
-
): Promise<RedlockExecution<T>> {
|
|
109
|
-
const redlock = await getClient(opts.type, opts.customOptions)
|
|
110
|
-
let lock: Redlock.Lock | undefined
|
|
111
|
-
let timeout
|
|
112
|
-
try {
|
|
113
|
-
const name = getLockName(opts)
|
|
114
|
-
|
|
115
|
-
const ttl =
|
|
116
|
-
opts.type === LockType.AUTO_EXTEND ? AUTO_EXTEND_POLLING_MS : opts.ttl
|
|
117
|
-
|
|
118
|
-
// create the lock
|
|
119
|
-
lock = await redlock.lock(name, ttl)
|
|
120
|
-
|
|
121
|
-
if (opts.type === LockType.AUTO_EXTEND) {
|
|
122
|
-
// We keep extending the lock while the task is running
|
|
123
|
-
const extendInIntervals = (): void => {
|
|
124
|
-
timeout = setTimeout(async () => {
|
|
125
|
-
lock = await lock!.extend(ttl, () => opts.onExtend && opts.onExtend())
|
|
126
|
-
|
|
127
|
-
extendInIntervals()
|
|
128
|
-
}, ttl / 2)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
extendInIntervals()
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// perform locked task
|
|
135
|
-
// need to await to ensure completion before unlocking
|
|
136
|
-
const result = await task()
|
|
137
|
-
return { executed: true, result }
|
|
138
|
-
} catch (e: any) {
|
|
139
|
-
// lock limit exceeded
|
|
140
|
-
if (e.name === "LockError") {
|
|
141
|
-
if (opts.type === LockType.TRY_ONCE) {
|
|
142
|
-
// don't throw for try-once locks, they will always error
|
|
143
|
-
// due to retry count (0) exceeded
|
|
144
|
-
return { executed: false }
|
|
145
|
-
} else {
|
|
146
|
-
throw e
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
throw e
|
|
150
|
-
}
|
|
151
|
-
} finally {
|
|
152
|
-
clearTimeout(timeout)
|
|
153
|
-
await lock?.unlock()
|
|
154
|
-
}
|
|
155
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { GenericContainer, StartedTestContainer } from "testcontainers"
|
|
2
|
-
import { generator, structures } from "../../../tests"
|
|
3
|
-
import RedisWrapper, { closeAll } from "../redis"
|
|
4
|
-
import env from "../../environment"
|
|
5
|
-
import { randomUUID } from "crypto"
|
|
6
|
-
|
|
7
|
-
jest.setTimeout(30000)
|
|
8
|
-
|
|
9
|
-
describe("redis", () => {
|
|
10
|
-
let redis: RedisWrapper
|
|
11
|
-
let container: StartedTestContainer
|
|
12
|
-
|
|
13
|
-
beforeAll(async () => {
|
|
14
|
-
const container = await new GenericContainer("redis")
|
|
15
|
-
.withExposedPorts(6379)
|
|
16
|
-
.start()
|
|
17
|
-
|
|
18
|
-
env._set(
|
|
19
|
-
"REDIS_URL",
|
|
20
|
-
`${container.getHost()}:${container.getMappedPort(6379)}`
|
|
21
|
-
)
|
|
22
|
-
env._set("MOCK_REDIS", 0)
|
|
23
|
-
env._set("REDIS_PASSWORD", 0)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
afterAll(() => {
|
|
27
|
-
container?.stop()
|
|
28
|
-
closeAll()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
redis = new RedisWrapper(structures.db.id())
|
|
33
|
-
await redis.init()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
describe("store", () => {
|
|
37
|
-
it("a basic value can be persisted", async () => {
|
|
38
|
-
const key = structures.uuid()
|
|
39
|
-
const value = generator.word()
|
|
40
|
-
|
|
41
|
-
await redis.store(key, value)
|
|
42
|
-
|
|
43
|
-
expect(await redis.get(key)).toEqual(value)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it("objects can be persisted", async () => {
|
|
47
|
-
const key = structures.uuid()
|
|
48
|
-
const value = { [generator.word()]: generator.word() }
|
|
49
|
-
|
|
50
|
-
await redis.store(key, value)
|
|
51
|
-
|
|
52
|
-
expect(await redis.get(key)).toEqual(value)
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe("bulkStore", () => {
|
|
57
|
-
function createRandomObject(
|
|
58
|
-
keyLength: number,
|
|
59
|
-
valueGenerator: () => any = () => randomUUID()
|
|
60
|
-
) {
|
|
61
|
-
return generator
|
|
62
|
-
.unique(() => randomUUID(), keyLength)
|
|
63
|
-
.reduce((acc, key) => {
|
|
64
|
-
acc[key] = valueGenerator()
|
|
65
|
-
return acc
|
|
66
|
-
}, {} as Record<string, string>)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
it("a basic object can be persisted", async () => {
|
|
70
|
-
const data = createRandomObject(10)
|
|
71
|
-
|
|
72
|
-
await redis.bulkStore(data)
|
|
73
|
-
|
|
74
|
-
for (const [key, value] of Object.entries(data)) {
|
|
75
|
-
expect(await redis.get(key)).toEqual(value)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
expect(await redis.keys("*")).toHaveLength(10)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it("a complex object can be persisted", async () => {
|
|
82
|
-
const data = {
|
|
83
|
-
...createRandomObject(10, () => createRandomObject(5)),
|
|
84
|
-
...createRandomObject(5),
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
await redis.bulkStore(data)
|
|
88
|
-
|
|
89
|
-
for (const [key, value] of Object.entries(data)) {
|
|
90
|
-
expect(await redis.get(key)).toEqual(value)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
expect(await redis.keys("*")).toHaveLength(15)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it("no TTL is set by default", async () => {
|
|
97
|
-
const data = createRandomObject(10)
|
|
98
|
-
|
|
99
|
-
await redis.bulkStore(data)
|
|
100
|
-
|
|
101
|
-
for (const [key, value] of Object.entries(data)) {
|
|
102
|
-
expect(await redis.get(key)).toEqual(value)
|
|
103
|
-
expect(await redis.getTTL(key)).toEqual(-1)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it("a bulk store can be persisted with TTL", async () => {
|
|
108
|
-
const ttl = 500
|
|
109
|
-
const data = createRandomObject(8)
|
|
110
|
-
|
|
111
|
-
await redis.bulkStore(data, ttl)
|
|
112
|
-
|
|
113
|
-
for (const [key, value] of Object.entries(data)) {
|
|
114
|
-
expect(await redis.get(key)).toEqual(value)
|
|
115
|
-
expect(await redis.getTTL(key)).toEqual(ttl)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
expect(await redis.keys("*")).toHaveLength(8)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it("setting a TTL of -1 will not persist the key", async () => {
|
|
122
|
-
const ttl = -1
|
|
123
|
-
const data = createRandomObject(5)
|
|
124
|
-
|
|
125
|
-
await redis.bulkStore(data, ttl)
|
|
126
|
-
|
|
127
|
-
for (const key of Object.keys(data)) {
|
|
128
|
-
expect(await redis.get(key)).toBe(null)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
expect(await redis.keys("*")).toHaveLength(0)
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
describe("increment", () => {
|
|
136
|
-
it("can increment on a new key", async () => {
|
|
137
|
-
const key = structures.uuid()
|
|
138
|
-
const result = await redis.increment(key)
|
|
139
|
-
expect(result).toBe(1)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it("can increment multiple times", async () => {
|
|
143
|
-
const key = structures.uuid()
|
|
144
|
-
const results = [
|
|
145
|
-
await redis.increment(key),
|
|
146
|
-
await redis.increment(key),
|
|
147
|
-
await redis.increment(key),
|
|
148
|
-
await redis.increment(key),
|
|
149
|
-
await redis.increment(key),
|
|
150
|
-
]
|
|
151
|
-
expect(results).toEqual([1, 2, 3, 4, 5])
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it("can increment multiple times in parallel", async () => {
|
|
155
|
-
const key = structures.uuid()
|
|
156
|
-
const results = await Promise.all(
|
|
157
|
-
Array.from({ length: 100 }).map(() => redis.increment(key))
|
|
158
|
-
)
|
|
159
|
-
expect(results).toHaveLength(100)
|
|
160
|
-
expect(results).toEqual(Array.from({ length: 100 }).map((_, i) => i + 1))
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it("can increment existing set keys", async () => {
|
|
164
|
-
const key = structures.uuid()
|
|
165
|
-
await redis.store(key, 70)
|
|
166
|
-
await redis.increment(key)
|
|
167
|
-
|
|
168
|
-
const result = await redis.increment(key)
|
|
169
|
-
expect(result).toBe(72)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it.each([
|
|
173
|
-
generator.word(),
|
|
174
|
-
generator.bool(),
|
|
175
|
-
{ [generator.word()]: generator.word() },
|
|
176
|
-
])("cannot increment if the store value is not a number", async value => {
|
|
177
|
-
const key = structures.uuid()
|
|
178
|
-
await redis.store(key, value)
|
|
179
|
-
|
|
180
|
-
await expect(redis.increment(key)).rejects.toThrow(
|
|
181
|
-
"ERR value is not an integer or out of range"
|
|
182
|
-
)
|
|
183
|
-
})
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
describe("deleteIfValue", () => {
|
|
187
|
-
it("can delete if the value matches", async () => {
|
|
188
|
-
const key = structures.uuid()
|
|
189
|
-
const value = generator.word()
|
|
190
|
-
await redis.store(key, value)
|
|
191
|
-
|
|
192
|
-
await redis.deleteIfValue(key, value)
|
|
193
|
-
|
|
194
|
-
expect(await redis.get(key)).toBeNull()
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it("will not delete if the value does not matches", async () => {
|
|
198
|
-
const key = structures.uuid()
|
|
199
|
-
const value = generator.word()
|
|
200
|
-
await redis.store(key, value)
|
|
201
|
-
|
|
202
|
-
await redis.deleteIfValue(key, generator.word())
|
|
203
|
-
|
|
204
|
-
expect(await redis.get(key)).toEqual(value)
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
})
|