Merge pull request #10755 from Budibase/fix/datasource-info-endpoint

Datasource info endpoint update
This commit is contained in:
Michael Drury 2023-06-01 12:29:46 +01:00 committed by GitHub
commit 8e4f0e531c
21 changed files with 126 additions and 99 deletions

View File

@ -33,7 +33,7 @@
"correlation-id": "4.0.0",
"dotenv": "16.0.1",
"emitter-listener": "1.1.2",
"ioredis": "4.28.0",
"ioredis": "5.3.2",
"joi": "17.6.0",
"jsonwebtoken": "9.0.0",
"koa-passport": "4.1.4",
@ -62,7 +62,6 @@
"@swc/jest": "^0.2.24",
"@trendyol/jest-testcontainers": "^2.1.1",
"@types/chance": "1.1.3",
"@types/ioredis": "4.28.0",
"@types/jest": "29.5.0",
"@types/koa": "2.13.4",
"@types/lodash": "4.14.180",
@ -74,7 +73,7 @@
"@types/tar-fs": "2.0.1",
"@types/uuid": "8.3.4",
"chance": "1.1.8",
"ioredis-mock": "5.8.0",
"ioredis-mock": "8.7.0",
"jest": "29.5.0",
"jest-environment-node": "29.5.0",
"jest-serial-runner": "^1.2.1",

View File

@ -97,7 +97,6 @@ const environment = {
REDIS_URL: process.env.REDIS_URL || "localhost:6379",
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
MOCK_REDIS: process.env.MOCK_REDIS,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
AWS_REGION: process.env.AWS_REGION,
@ -129,6 +128,7 @@ const environment = {
PLUGIN_BUCKET_NAME:
process.env.PLUGIN_BUCKET_NAME || DefaultBucketName.PLUGINS,
USE_COUCH: process.env.USE_COUCH || true,
MOCK_REDIS: process.env.MOCK_REDIS,
DEFAULT_LICENSE: process.env.DEFAULT_LICENSE,
SERVICE: process.env.SERVICE || "budibase",
LOG_LEVEL: process.env.LOG_LEVEL || "info",

View File

@ -21,6 +21,7 @@ export * as context from "./context"
export * as cache from "./cache"
export * as objectStore from "./objectStore"
export * as redis from "./redis"
export { Client as RedisClient } from "./redis"
export * as locks from "./redis/redlockImpl"
export * as utils from "./utils"
export * as errors from "./errors"

View File

@ -1,6 +1,7 @@
import env from "../environment"
import Redis from "ioredis"
// ioredis mock is all in memory
const Redis = env.MOCK_REDIS ? require("ioredis-mock") : require("ioredis")
const MockRedis = require("ioredis-mock")
import {
addDbPrefix,
removeDbPrefix,
@ -18,7 +19,7 @@ const DEFAULT_SELECT_DB = SelectableDatabase.DEFAULT
// for testing just generate the client once
let CLOSED = false
let CLIENTS: { [key: number]: any } = {}
0
let CONNECTED = false
// mock redis always connected
@ -55,6 +56,7 @@ function connectionError(
* will return the ioredis client which will be ready to use.
*/
function init(selectDb = DEFAULT_SELECT_DB) {
const RedisCore = env.MOCK_REDIS ? MockRedis : Redis
let timeout: NodeJS.Timeout
CLOSED = false
let client = pickClient(selectDb)
@ -64,7 +66,7 @@ function init(selectDb = DEFAULT_SELECT_DB) {
}
// testing uses a single in memory client
if (env.MOCK_REDIS) {
CLIENTS[selectDb] = new Redis(getRedisOptions())
CLIENTS[selectDb] = new RedisCore(getRedisOptions())
}
// start the timer - only allowed 5 seconds to connect
timeout = setTimeout(() => {
@ -84,11 +86,11 @@ function init(selectDb = DEFAULT_SELECT_DB) {
const { redisProtocolUrl, opts, host, port } = getRedisOptions()
if (CLUSTERED) {
client = new Redis.Cluster([{ host, port }], opts)
client = new RedisCore.Cluster([{ host, port }], opts)
} else if (redisProtocolUrl) {
client = new Redis(redisProtocolUrl)
client = new RedisCore(redisProtocolUrl)
} else {
client = new Redis(opts)
client = new RedisCore(opts)
}
// attach handlers
client.on("end", (err: Error) => {

View File

@ -95,7 +95,7 @@ export function getRedisOptions() {
opts.port = port
opts.password = password
}
return { opts, host, port, redisProtocolUrl }
return { opts, host, port: parseInt(port), redisProtocolUrl }
}
export function addDbPrefix(db: string, key: string) {

View File

@ -80,6 +80,7 @@
"global-agent": "3.0.0",
"google-auth-library": "7.12.0",
"google-spreadsheet": "3.2.0",
"ioredis": "5.3.2",
"jimp": "0.16.1",
"joi": "17.6.0",
"js-yaml": "4.1.0",
@ -122,8 +123,8 @@
"validate.js": "0.13.1",
"vm2": "3.9.17",
"worker-farm": "1.7.0",
"yargs": "13.2.4",
"xml2js": "0.5.0"
"xml2js": "0.5.0",
"yargs": "13.2.4"
},
"devDependencies": {
"@babel/core": "7.17.4",
@ -137,7 +138,6 @@
"@types/bson": "4.2.0",
"@types/global-agent": "2.1.1",
"@types/google-spreadsheet": "3.1.5",
"@types/ioredis": "4.28.10",
"@types/jest": "29.5.0",
"@types/koa": "2.13.4",
"@types/koa__router": "8.0.8",
@ -157,7 +157,6 @@
"copyfiles": "2.4.1",
"docker-compose": "0.23.17",
"eslint": "6.8.0",
"ioredis-mock": "7.2.0",
"is-wsl": "2.2.0",
"jest": "29.5.0",
"jest-openapi": "0.14.2",

View File

@ -21,6 +21,7 @@ import {
CreateDatasourceRequest,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse,
IntegrationBase,
DatasourcePlus,
@ -57,6 +58,21 @@ async function getConnector(
return new Connector(datasource.config)
}
async function getAndMergeDatasource(datasource: Datasource) {
let existingDatasource: undefined | Datasource
if (datasource._id) {
existingDatasource = await sdk.datasources.get(datasource._id)
}
let enrichedDatasource = datasource
if (existingDatasource) {
enrichedDatasource = sdk.datasources.mergeConfigs(
datasource,
existingDatasource
)
}
return await sdk.datasources.enrich(enrichedDatasource)
}
async function buildSchemaHelper(datasource: Datasource) {
const connector = (await getConnector(datasource)) as DatasourcePlus
await connector.buildSchema(datasource._id!, datasource.entities!)
@ -132,17 +148,7 @@ export async function verify(
ctx: UserCtx<VerifyDatasourceRequest, VerifyDatasourceResponse>
) {
const { datasource } = ctx.request.body
let existingDatasource: undefined | Datasource
if (datasource._id) {
existingDatasource = await sdk.datasources.get(datasource._id)
}
let enrichedDatasource = datasource
if (existingDatasource) {
enrichedDatasource = sdk.datasources.mergeConfigs(
datasource,
existingDatasource
)
}
const enrichedDatasource = await getAndMergeDatasource(datasource)
const connector = await getConnector(enrichedDatasource)
if (!connector.testConnection) {
ctx.throw(400, "Connection information verification not supported")
@ -156,11 +162,11 @@ export async function verify(
}
export async function information(
ctx: UserCtx<void, FetchDatasourceInfoResponse>
ctx: UserCtx<FetchDatasourceInfoRequest, FetchDatasourceInfoResponse>
) {
const datasourceId = ctx.params.datasourceId
const datasource = await sdk.datasources.get(datasourceId, { enriched: true })
const connector = (await getConnector(datasource)) as DatasourcePlus
const { datasource } = ctx.request.body
const enrichedDatasource = await getAndMergeDatasource(datasource)
const connector = (await getConnector(enrichedDatasource)) as DatasourcePlus
if (!connector.getTableNames) {
ctx.throw(400, "Table name fetching not supported by datasource")
}
@ -297,7 +303,7 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
ctx.body = {
datasource: await sdk.datasources.removeSecretSingle(datasource),
}
builderSocket.emitDatasourceUpdate(ctx, datasource)
builderSocket?.emitDatasourceUpdate(ctx, datasource)
}
export async function save(
@ -340,7 +346,7 @@ export async function save(
response.error = schemaError
}
ctx.body = response
builderSocket.emitDatasourceUpdate(ctx, datasource)
builderSocket?.emitDatasourceUpdate(ctx, datasource)
}
async function destroyInternalTablesBySourceId(datasourceId: string) {
@ -400,7 +406,7 @@ export async function destroy(ctx: UserCtx) {
ctx.message = `Datasource deleted.`
ctx.status = 200
builderSocket.emitDatasourceDeletion(ctx, datasourceId)
builderSocket?.emitDatasourceDeletion(ctx, datasourceId)
}
export async function find(ctx: UserCtx) {

View File

@ -71,7 +71,7 @@ export async function create(ctx: any) {
const doc = await pro.plugins.storePlugin(metadata, directory, source)
clientAppSocket.emit("plugins-update", { name, hash: doc.hash })
clientAppSocket?.emit("plugins-update", { name, hash: doc.hash })
ctx.body = {
message: "Plugin uploaded successfully",
plugins: [doc],

View File

@ -78,7 +78,7 @@ export async function save(ctx: UserCtx) {
ctx.eventEmitter &&
ctx.eventEmitter.emitTable(`table:save`, appId, savedTable)
ctx.body = savedTable
builderSocket.emitTableUpdate(ctx, savedTable)
builderSocket?.emitTableUpdate(ctx, savedTable)
}
export async function destroy(ctx: UserCtx) {
@ -91,7 +91,7 @@ export async function destroy(ctx: UserCtx) {
ctx.status = 200
ctx.table = deletedTable
ctx.body = { message: `Table ${tableId} deleted.` }
builderSocket.emitTableDeletion(ctx, tableId)
builderSocket?.emitTableDeletion(ctx, tableId)
}
export async function bulkImport(ctx: UserCtx) {

View File

@ -58,7 +58,7 @@ export async function save(ctx: Ctx) {
await handleViewEvents(existingTable.views[viewName], table.views[viewName])
ctx.body = table.views[viewName]
builderSocket.emitTableUpdate(ctx, table)
builderSocket?.emitTableUpdate(ctx, table)
}
export async function calculationEvents(existingView: View, newView: View) {
@ -127,7 +127,7 @@ export async function destroy(ctx: Ctx) {
await events.view.deleted(view)
ctx.body = view
builderSocket.emitTableUpdate(ctx, table)
builderSocket?.emitTableUpdate(ctx, table)
}
export async function exportView(ctx: Ctx) {

View File

@ -20,8 +20,8 @@ router
authorized(permissions.BUILDER),
datasourceController.verify
)
.get(
"/api/datasources/:datasourceId/info",
.post(
"/api/datasources/info",
authorized(permissions.BUILDER),
datasourceController.information
)

View File

@ -114,8 +114,8 @@ describe("/views", () => {
expect(res.body.tableId).toBe(table._id)
const updatedTable = await config.getTable(table._id)
expect(updatedTable.views).toEqual({
TestView: {
const expectedObj = expect.objectContaining({
TestView: expect.objectContaining({
field: "Price",
calculation: "stats",
tableId: table._id,
@ -143,8 +143,9 @@ describe("/views", () => {
type: "string",
},
},
},
}),
})
expect(updatedTable.views).toEqual(expectedObj)
})
})

View File

@ -25,6 +25,7 @@ const config = setup.getConfig()!
jest.setTimeout(30000)
jest.unmock("pg")
jest.mock("../websockets")
describe("postgres integrations", () => {
let makeRequest: MakeRequestResponse,
@ -1055,13 +1056,12 @@ describe("postgres integrations", () => {
})
})
describe("GET /api/datasources/:datasourceId/info", () => {
describe("POST /api/datasources/info", () => {
it("should fetch information about postgres datasource", async () => {
const primaryName = primaryPostgresTable.name
const response = await makeRequest(
"get",
`/api/datasources/${postgresDatasource._id}/info`
)
const response = await makeRequest("post", "/api/datasources/info", {
datasource: postgresDatasource,
})
expect(response.status).toBe(200)
expect(response.body.tableNames).toBeDefined()
expect(response.body.tableNames.indexOf(primaryName)).not.toBe(-1)

View File

@ -177,7 +177,13 @@ class RedisIntegration {
const pipeline = this.client.pipeline(pipelineCommands)
const result = await pipeline.exec()
return result.map((output: string | string[]) => output[1])
return result?.map((output: any) => {
if (typeof output === "string") {
return output
} else if (Array.isArray(output)) {
return output[1]
}
})
})
}
}

View File

@ -36,6 +36,6 @@ export async function processUploaded(plugin: FileType, source?: PluginSource) {
}
const doc = await pro.plugins.storePlugin(metadata, directory, source)
clientAppSocket.emit("plugin-update", { name: doc.name, hash: doc.hash })
clientAppSocket?.emit("plugin-update", { name: doc.name, hash: doc.hash })
return doc
}

View File

@ -44,10 +44,18 @@ function getLoopIterations(loopStep: LoopStep) {
if (!binding) {
return 0
}
const isString = typeof binding === "string"
try {
if (isString) {
binding = JSON.parse(binding)
}
} catch (err) {
// ignore error - wasn't able to parse
}
if (Array.isArray(binding)) {
return binding.length
}
if (typeof binding === "string") {
if (isString) {
return automationUtils.stringSplit(binding).length
}
return 0

View File

@ -1,13 +1,16 @@
import { redis } from "@budibase/backend-core"
import { redis, RedisClient } from "@budibase/backend-core"
import { getGlobalIDFromUserMetadataID } from "../db/utils"
import { ContextUser } from "@budibase/types"
import env from "../environment"
const APP_DEV_LOCK_SECONDS = 600
const AUTOMATION_TEST_FLAG_SECONDS = 60
let devAppClient: any, debounceClient: any, flagClient: any
let devAppClient: RedisClient,
debounceClient: RedisClient,
flagClient: RedisClient
// We need to maintain a duplicate client for socket.io pub/sub
let socketClient: any
let socketClient: RedisClient
let socketSubClient: any
// We init this as we want to keep the connection open all the time
@ -22,8 +25,10 @@ export async function init() {
// Duplicate the socket client for pub/sub
socketClient = await redis.clients.getSocketClient()
if (!env.isTest()) {
socketSubClient = socketClient.getClient().duplicate()
}
}
export async function shutdown() {
console.log("REDIS SHUTDOWN")

View File

@ -14,9 +14,9 @@ export default class BuilderSocket extends BaseSocket {
super(app, server, "/socket/builder", [authorized(permissions.BUILDER)])
}
async onConnect(socket: Socket) {
async onConnect(socket?: Socket) {
// Initial identification of selected app
socket.on(BuilderSocketEvent.SelectApp, async (appId, callback) => {
socket?.on(BuilderSocketEvent.SelectApp, async (appId, callback) => {
await this.joinRoom(socket, appId)
// Reply with all users in current room
@ -49,14 +49,14 @@ export default class BuilderSocket extends BaseSocket {
this.io
.in(ctx.appId)
.emit(BuilderSocketEvent.TableChange, { id: table._id, table })
gridSocket.emitTableUpdate(table)
gridSocket?.emitTableUpdate(table)
}
emitTableDeletion(ctx: any, id: string) {
this.io
.in(ctx.appId)
.emit(BuilderSocketEvent.TableChange, { id, table: null })
gridSocket.emitTableDeletion(id)
gridSocket?.emitTableDeletion(id)
}
emitDatasourceUpdate(ctx: any, datasource: Datasource) {

View File

@ -3,15 +3,19 @@ import Koa from "koa"
import ClientAppSocket from "./client"
import GridSocket from "./grid"
import BuilderSocket from "./builder"
import env from "../environment"
let clientAppSocket: ClientAppSocket
let gridSocket: GridSocket
let builderSocket: BuilderSocket
let clientAppSocket: ClientAppSocket | undefined
let gridSocket: GridSocket | undefined
let builderSocket: BuilderSocket | undefined
export const initialise = (app: Koa, server: http.Server) => {
// have to remove these for testing until ioredis-mock can be fully removed
if (!env.isTest()) {
clientAppSocket = new ClientAppSocket(app, server)
gridSocket = new GridSocket(app, server)
builderSocket = new BuilderSocket(app, server)
}
}
export { clientAppSocket, gridSocket, builderSocket }

View File

@ -23,6 +23,10 @@ export interface VerifyDatasourceResponse {
error?: string
}
export interface FetchDatasourceInfoRequest {
datasource: Datasource
}
export interface FetchDatasourceInfoResponse {
tableNames: string[]
}

View File

@ -2591,6 +2591,16 @@
resolved "https://registry.yarnpkg.com/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz#98c23c950a3d9b6c8f0daed06da6c3af06981340"
integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==
"@ioredis/as-callback@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@ioredis/as-callback/-/as-callback-3.0.0.tgz#b96c9b05e6701e85ec6a5e62fa254071b0aec97f"
integrity sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg==
"@ioredis/commands@^1.1.1", "@ioredis/commands@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11"
integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==
"@isaacs/string-locale-compare@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz#291c227e93fd407a96ecd59879a35809120e432b"
@ -5649,13 +5659,6 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65"
integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==
"@types/ioredis@4.28.0":
version "4.28.0"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.0.tgz#609b2ea0d91231df2dd7f67dd77436bc72584911"
integrity sha512-HSA/JQivJgV0e+353gvgu6WVoWvGRe0HyHOnAN2AvbVIhUlJBhNnnkP8gEEokrDWrxywrBkwo8NuDZ6TVPL9XA==
dependencies:
"@types/node" "*"
"@types/ioredis@4.28.10":
version "4.28.10"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
@ -11956,7 +11959,7 @@ fecha@^4.2.0:
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
fengari-interop@^0.1.2, fengari-interop@^0.1.3:
fengari-interop@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.3.tgz#3ad37a90e7430b69b365441e9fc0ba168942a146"
integrity sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw==
@ -13993,39 +13996,28 @@ invert-kv@^2.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
ioredis-mock@5.8.0:
version "5.8.0"
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.8.0.tgz#99d3f0fe58419c2ff15c81cb4e13709aff8f2a44"
integrity sha512-PokVyNC/d3qaAyHAdf+Ex4HHBe7GdaSGrcn8pP7ran+pVmSrJ6Aofwm7Od5bQ5JtjDSiylEtIoZzEPYIjUKMHA==
dependencies:
fengari "^0.1.4"
fengari-interop "^0.1.2"
lodash "^4.17.21"
standard-as-callback "^2.1.0"
ioredis-mock@7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-7.2.0.tgz#48f006c07ef7f1f93f75e60d8f9035fa46c4ef0a"
integrity sha512-xzABBG3NhfDBGxH1KX9n6vs7WGNn9lhcxMT3b+vjynVImxlUV+vOXU+tjGzSUnGmx4IYllA8RqbXN8z6ROMPVA==
ioredis-mock@8.7.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.7.0.tgz#9877a85e0d233e1b49123d1c6e320df01e9a1d36"
integrity sha512-BJcSjkR3sIMKbH93fpFzwlWi/jl1kd5I3vLvGQxnJ/W/6bD2ksrxnyQN186ljAp3Foz4p1ivViDE3rZeKEAluA==
dependencies:
"@ioredis/as-callback" "^3.0.0"
"@ioredis/commands" "^1.2.0"
fengari "^0.1.4"
fengari-interop "^0.1.3"
redis-commands "^1.7.0"
standard-as-callback "^2.1.0"
semver "^7.3.8"
ioredis@4.28.0:
version "4.28.0"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.0.tgz#5a2be3f37ff2075e2332f280eaeb02ab4d9ff0d3"
integrity sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==
ioredis@5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7"
integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==
dependencies:
"@ioredis/commands" "^1.1.1"
cluster-key-slot "^1.1.0"
debug "^4.3.1"
denque "^1.1.0"
debug "^4.3.4"
denque "^2.1.0"
lodash.defaults "^4.2.0"
lodash.flatten "^4.4.0"
lodash.isarguments "^3.1.0"
p-map "^2.1.0"
redis-commands "1.7.0"
redis-errors "^1.2.0"
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"
@ -21759,7 +21751,7 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
redis-commands@1.7.0, redis-commands@^1.7.0:
redis-commands@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89"
integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==