Mostly working minus presence

This commit is contained in:
Atridad Lahiji 2023-11-22 12:27:30 -07:00
parent 66e20bc6ea
commit 6b93e71595
No known key found for this signature in database
16 changed files with 512 additions and 150 deletions

View file

@ -1,5 +1,5 @@
import { rootAuthLoader } from "@clerk/remix/ssr.server";
import { ClerkApp, ClerkErrorBoundary } from "@clerk/remix";
import { ClerkApp, ClerkErrorBoundary, ClerkLoaded } from "@clerk/remix";
import type {
LinksFunction,
LoaderFunction,
@ -42,11 +42,14 @@ function App() {
<Links />
</head>
<body className="h-[100%] w-[100%] fixed overflow-y-auto">
<ClerkLoaded>
<Header title={"Sprint Padawan"} />
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
<Outlet />
</div>
<Footer />
</ClerkLoaded>
<ScrollRestoration />
<Scripts />
<LiveReload />

View file

@ -1,55 +0,0 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { AblyTokenResponse } from "~/services/types";
// Get Room List
export async function loader({ context, params, request }: LoaderFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
if (!process.env.ABLY_API_KEY) {
return new Response(
`Missing ABLY_API_KEY environment variable.
If you're running locally, please ensure you have a ./.env file with a value for ABLY_API_KEY=your-key.
If you're running in Netlify, make sure you've configured env variable ABLY_API_KEY.
Please see README.md for more details on configuring your Ably API Key.`,
{
status: 500,
statusText: `Missing ABLY_API_KEY environment variable.
If you're running locally, please ensure you have a ./.env file with a value for ABLY_API_KEY=your-key.
If you're running in Netlify, make sure you've configured env variable ABLY_API_KEY.
Please see README.md for more details on configuring your Ably API Key.`,
}
);
}
const keyName = process.env.ABLY_API_KEY!.split(":")[0];
const tokenResponse = await fetch(
`https://rest.ably.io/keys/${keyName}/requestToken`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(process.env.ABLY_API_KEY!)}`,
},
body: JSON.stringify({
keyName,
clientId: userId,
timestamp: Date.now(),
}),
}
);
const tokenResponseData = (await tokenResponse.json()) as AblyTokenResponse;
return json(tokenResponseData, {
status: 200,
statusText: "SUCCESS",
});
}

View file

@ -8,6 +8,13 @@ import { rooms } from "~/services/schema";
export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
const data = await request.json();
const room = await db

View file

@ -8,6 +8,13 @@ import { rooms } from "~/services/schema";
export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
const roomId = params.roomId;
if (!roomId) {

View file

@ -19,27 +19,48 @@ export async function loader({ context, params, request }: LoaderFunctionArgs) {
});
}
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
return eventStream(request.signal, function setup(send) {
async function handler() {
const roomList = await db.query.rooms.findMany({
where: eq(rooms.userId, userId || ""),
const roomFromDb = await db.query.rooms.findFirst({
where: eq(rooms.id, roomId || ""),
with: {
logs: true,
},
});
send({
event: `room-${roomId}`,
data: JSON.stringify(roomFromDb),
});
send({ event: roomId, data: JSON.stringify(roomList) });
}
// Initial fetch
console.log("HI");
db.query.rooms
.findMany({
where: eq(rooms.userId, userId || ""),
.findFirst({
where: eq(rooms.id, roomId || ""),
with: {
logs: true,
},
})
.then((roomList) => {
send({ event: roomId, data: JSON.stringify(roomList) });
.then((roomFromDb) => {
console.log(roomId);
return send({
event: `room-${roomId}`,
data: JSON.stringify(roomFromDb),
});
});
emitter.on("roomlist", handler);
emitter.on("room", handler);
return function clear() {
emitter.off("roomlist", handler);
emitter.off("room", handler);
};
});
}

View file

@ -0,0 +1,64 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { and, eq } from "drizzle-orm";
import { eventStream } from "remix-utils/sse/server";
import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server";
import { presence, rooms } from "~/services/schema";
export async function loader({ context, params, request }: LoaderFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
const roomId = params.roomId;
if (!roomId) {
return json("RoomId Missing!", {
status: 400,
statusText: "BAD REQUEST!",
});
}
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
return eventStream(request.signal, function setup(send) {
async function handler() {
const presenceData = await db.query.presence.findMany({
where: and(
eq(presence.userId, userId || ""),
eq(presence.roomId, roomId || "")
),
});
send({
event: `${userId}-${params.roomId}`,
data: JSON.stringify(presenceData),
});
}
// Initial fetch
db.query.presence
.findMany({
where: and(
eq(presence.userId, userId || ""),
eq(presence.roomId, roomId || "")
),
})
.then((presenceData) => {
return send({
event: `${userId}-${params.roomId}`,
data: JSON.stringify(presenceData),
});
});
emitter.on("presence", handler);
return function clear() {
emitter.off("presence", handler);
};
});
}

View file

@ -0,0 +1,43 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { ActionFunctionArgs, json } from "@remix-run/node";
import { createId } from "@paralleldrive/cuid2";
import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server";
import { rooms } from "~/services/schema";
export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
const data = await request.json();
const room = await db
.insert(rooms)
.values({
id: `room_${createId()}`,
created_at: Date.now().toString(),
userId: userId || "",
roomName: data.name,
storyName: "First Story!",
scale: "0.5,1,2,3,5,8",
visible: 0,
})
.returning();
const success = room.length > 0;
if (success) {
emitter.emit("roomlist");
return json(room, {
status: 200,
statusText: "SUCCESS",
});
}
}

View file

@ -0,0 +1,42 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { ActionFunctionArgs, json } from "@remix-run/node";
import { eq } from "drizzle-orm";
import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server";
import { rooms } from "~/services/schema";
export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
const roomId = params.roomId;
if (!roomId) {
return json("RoomId Missing!", {
status: 400,
statusText: "BAD REQUEST!",
});
}
const deletedRoom = await db
.delete(rooms)
.where(eq(rooms.id, roomId))
.returning();
const success = deletedRoom.length > 0;
if (success) {
emitter.emit("roomlist");
return json(deletedRoom, {
status: 200,
statusText: "SUCCESS",
});
}
}

View file

@ -0,0 +1,87 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { ActionFunctionArgs, json } from "@remix-run/node";
import { createId } from "@paralleldrive/cuid2";
import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server";
import { logs, rooms, votes } from "~/services/schema";
import { eq } from "drizzle-orm";
export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
const data = await request.json();
const roomId = params.roomId;
if (data.log) {
const oldRoom = await db.query.rooms.findFirst({
where: eq(rooms.id, params.roomId || ""),
with: {
votes: true,
logs: true,
},
});
oldRoom &&
(await db.insert(logs).values({
id: `log_${createId()}`,
created_at: Date.now().toString(),
userId: userId || "",
roomId: roomId || "",
scale: oldRoom.scale,
votes: JSON.stringify(
oldRoom.votes.map((vote) => {
return {
name: vote.userId,
value: vote.value,
};
})
),
roomName: oldRoom.roomName,
storyName: oldRoom.storyName,
}));
}
if (data.reset) {
await db.delete(votes).where(eq(votes.roomId, params.roomId || ""));
}
const newRoom = data.reset
? await db
.update(rooms)
.set({
storyName: data.name,
visible: data.visible,
scale: [...new Set(data.scale.split(","))]
.filter((item) => item !== "")
.toString(),
})
.where(eq(rooms.id, params.roomId || ""))
.returning()
: await db
.update(rooms)
.set({
visible: data.visible,
})
.where(eq(rooms.id, params.roomId || ""))
.returning();
const success = newRoom.length > 0;
if (success) {
console.log(success);
emitter.emit("room");
emitter.emit("votes");
return json(newRoom, {
status: 200,
statusText: "SUCCESS",
});
}
}

View file

@ -0,0 +1,50 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { ActionFunctionArgs, json } from "@remix-run/node";
import { createId } from "@paralleldrive/cuid2";
import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server";
import { votes } from "~/services/schema";
export async function action({ request, params, context }: ActionFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
const data = await request.json();
const roomId = params.roomId;
const upsertResult = await db
.insert(votes)
.values({
id: `vote_${createId()}`,
created_at: Date.now().toString(),
value: data.value,
userId: userId || "",
roomId: roomId || "",
})
.onConflictDoUpdate({
target: [votes.userId, votes.roomId],
set: {
created_at: Date.now().toString(),
value: data.value,
userId: userId || "",
roomId: roomId,
},
});
const success = upsertResult.rowsAffected > 0;
if (success) {
emitter.emit("votes");
return json(upsertResult, {
status: 200,
statusText: "SUCCESS",
});
}
}

View file

@ -0,0 +1,55 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { LoaderFunctionArgs, json } from "@remix-run/node";
import { eq } from "drizzle-orm";
import { eventStream } from "remix-utils/sse/server";
import { db } from "~/services/db.server";
import { emitter } from "~/services/emitter.server";
import { votes } from "~/services/schema";
// Get Room List
export async function loader({ context, params, request }: LoaderFunctionArgs) {
const { userId } = await getAuth({ context, params, request });
const roomId = params.roomId;
if (!roomId) {
return json("RoomId Missing!", {
status: 400,
statusText: "BAD REQUEST!",
});
}
if (!userId) {
return json("Not Signed In!", {
status: 403,
statusText: "UNAUTHORIZED!",
});
}
return eventStream(request.signal, function setup(send) {
async function handler() {
const votesByRoomId = await db.query.votes.findMany({
where: eq(votes.roomId, roomId || ""),
});
send({ event: `votes-${roomId}`, data: JSON.stringify(votesByRoomId) });
}
// Initial fetch
db.query.votes
.findMany({
where: eq(votes.roomId, roomId || ""),
})
.then((votesByRoomId) => {
return send({
event: `votes-${roomId}`,
data: JSON.stringify(votesByRoomId),
});
});
emitter.on("votes", handler);
return function clear() {
emitter.off("votes", handler);
};
});
}

View file

@ -1,8 +1,6 @@
import { getAuth } from "@clerk/remix/ssr.server";
import { LoaderFunction, redirect } from "@remix-run/node";
import { Link, useParams } from "@remix-run/react";
import { AblyProvider, useChannel, usePresence } from "ably/react";
import * as Ably from "ably";
import {
CheckCircleIcon,
CopyIcon,
@ -19,13 +17,8 @@ import {
import { useEffect, useState } from "react";
import LoadingIndicator from "~/components/LoadingIndicator";
import { useEventSource } from "remix-utils/sse/react";
import {
EventTypes,
PresenceItem,
RoomResponse,
VoteResponse,
} from "~/services/types";
import { isAdmin, isVIP, jsonToCsv } from "~/services/helpers";
import { PresenceItem, RoomResponse, VoteResponse } from "~/services/types";
import { isAdmin, jsonToCsv } from "~/services/helpers";
import { useUser } from "@clerk/remix";
export const loader: LoaderFunction = async (args) => {
@ -37,21 +30,32 @@ export const loader: LoaderFunction = async (args) => {
return {};
};
function RoomContent() {
export default function Room() {
const { user } = useUser();
const params = useParams();
const roomId = params.roomId;
let roomFromDb = useEventSource("/api/room/get", { event: params.roomId });
let votesFromDb = useEventSource("/api/votes/get/all", {
event: params.roomId,
let roomFromDb = useEventSource(`/api/room/get/${roomId}`, {
event: `room-${params.roomId}`,
});
let roomFromDbParsed = JSON.parse(roomFromDb!) as RoomResponse;
let votesFromDbParsed = JSON.parse(votesFromDb!) as VoteResponse;
let votesFromDb = useEventSource(`/api/votes/get/${roomId}`, {
event: `votes-${params.roomId}`,
});
let presenceData = useEventSource(`/api/room/presence/get/${roomId}`, {
event: `${user?.id}-${params.roomId}`,
});
let roomFromDbParsed = JSON.parse(roomFromDb!) as RoomResponse | undefined;
let votesFromDbParsed = JSON.parse(votesFromDb!) as VoteResponse | undefined;
let presenceDateParsed = JSON.parse(presenceData!) as
| PresenceItem[]
| undefined;
const [storyNameText, setStoryNameText] = useState<string>("");
const [roomScale, setRoomScale] = useState<string>("");
const [copied, setCopied] = useState<boolean>(false);
// Handlers
@ -76,7 +80,7 @@ function RoomContent() {
async function setVoteHandler(value: string) {
if (roomFromDb) {
await fetch(`/api/internal/room/${roomId}/vote`, {
await fetch(`/api/vote/set/${roomId}`, {
cache: "no-cache",
method: "PUT",
body: JSON.stringify({
@ -92,7 +96,7 @@ function RoomContent() {
log: boolean | undefined;
}) {
if (roomFromDb) {
await fetch(`/api/internal/room/${roomId}`, {
await fetch(`/api/room/set/${roomId}`, {
cache: "no-cache",
method: "PUT",
body: JSON.stringify({
@ -174,7 +178,7 @@ function RoomContent() {
presenceItem: PresenceItem
) => {
const matchedVote = votes?.find(
(vote) => vote.userId === presenceItem.client_id
(vote) => vote.userId === presenceItem.userId
);
if (visible) {
@ -192,31 +196,6 @@ function RoomContent() {
// Hooks
// =================================
useChannel(
{
channelName: `${process.env.APP_ENV}-${roomId}`,
},
({ name }: { name: string }) => {
if (name === EventTypes.ROOM_UPDATE) {
void getRoomHandler();
void getVotesHandler();
} else if (name === EventTypes.VOTE_UPDATE) {
void getVotesHandler();
}
}
);
const { presenceData } = usePresence<PresenceItem>(
`${process.env.APP_ENV}-${roomId}`,
{
name: (user?.fullName ?? user?.username) || "",
image: user?.imageUrl || "",
client_id: user?.id || "unknown",
isAdmin: isAdmin(user?.publicMetadata),
isVIP: isVIP(user?.publicMetadata),
}
);
useEffect(() => {
if (roomFromDb) {
setStoryNameText(roomFromDbParsed?.storyName || "");
@ -259,33 +238,32 @@ function RoomContent() {
<ul className="p-0 flex flex-row flex-wrap justify-center items-center text-ceter gap-4">
{presenceData &&
presenceData
.filter(
presenceDateParsed
?.filter(
(value, index, self) =>
index ===
self.findIndex(
(presenceItem) =>
presenceItem.clientId === value.clientId
(presenceItem) => presenceItem.userId === value.userId
)
)
.map((presenceItem) => {
return (
<li
key={presenceItem.clientId}
key={presenceItem.userId}
className="flex flex-row items-center justify-center gap-2"
>
<div className="w-10 rounded-full avatar">
<img
src={presenceItem.data.image}
alt={`${presenceItem.data.name}'s Profile Picture`}
src={presenceItem.userImageUrl}
alt={`${presenceItem.userFullName}'s Profile Picture`}
height={32}
width={32}
/>
</div>
<p className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-md">
{presenceItem.data.name}{" "}
{presenceItem.data.isAdmin && (
{presenceItem.userFullName}{" "}
{presenceItem.isAdmin && (
<span
className="tooltip tooltip-primary"
data-tip="Admin"
@ -293,7 +271,7 @@ function RoomContent() {
<ShieldIcon className="inline-block text-primary" />
</span>
)}{" "}
{presenceItem.data.isVIP && (
{presenceItem.isVIP && (
<span
className="tooltip tooltip-secondary"
data-tip="VIP"
@ -301,7 +279,7 @@ function RoomContent() {
<StarIcon className="inline-block text-secondary" />
</span>
)}{" "}
{presenceItem.clientId ===
{presenceItem.userId ===
roomFromDbParsed?.userId && (
<span
className="tooltip tooltip-warning"
@ -318,7 +296,7 @@ function RoomContent() {
voteString(
roomFromDbParsed?.visible!,
votesFromDbParsed,
presenceItem.data
presenceItem
)}
</li>
);
@ -476,15 +454,3 @@ function RoomContent() {
);
}
}
export default function Room() {
const client = new Ably.Realtime.Promise({
authUrl: "/api/ably",
});
return (
<AblyProvider client={client}>
<RoomContent />
</AblyProvider>
);
}

View file

@ -73,3 +73,23 @@ export const logsRelations = relations(logs, ({ one }) => ({
references: [rooms.id],
}),
}));
export const presence = sqliteTable(
"Presence",
{
id: text("id", { length: 255 }).notNull().primaryKey(),
userId: text("userId", { length: 255 }).notNull(),
userFullName: text("userFullName", { length: 255 }).notNull(),
userImageUrl: text("userImageUrl", { length: 255 }).notNull(),
isVIP: integer("isVIP").default(0).notNull(),
isAdmin: integer("isAdmin").default(0).notNull(),
roomId: text("roomId", { length: 255 })
.notNull()
.references(() => rooms.id, { onDelete: "cascade" }),
},
(table) => {
return {
unq: unique().on(table.userId, table.roomId),
};
}
);

View file

@ -8,9 +8,12 @@ export const EventTypes = {
export type EventType = BetterEnum<typeof EventTypes>;
export interface PresenceItem {
name: string;
image: string;
client_id: string;
id: string;
userId: string;
userFullName: string;
userImageUrl: string;
roomId: string;
value: string;
isAdmin: boolean;
isVIP: boolean;
}
@ -66,11 +69,3 @@ export type VoteResponse =
}[]
| null
| undefined;
export type AblyTokenResponse = {
token: string;
issued: number;
expires: number;
capability: string;
clientId: string;
};

View file

@ -21,6 +21,7 @@
"ably": "1.2.47",
"csv42": "^5.0.0",
"drizzle-orm": "^0.29.0",
"ioredis": "^5.3.2",
"isbot": "^3.7.1",
"lucide-react": "^0.292.0",
"react": "^18.2.0",

60
pnpm-lock.yaml generated
View file

@ -35,6 +35,9 @@ dependencies:
drizzle-orm:
specifier: ^0.29.0
version: 0.29.0(@libsql/client@0.4.0-pre.2)(better-sqlite3@9.1.1)
ioredis:
specifier: ^5.3.2
version: 5.3.2
isbot:
specifier: ^3.7.1
version: 3.7.1
@ -1333,6 +1336,10 @@ packages:
resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==}
dev: true
/@ioredis/commands@1.2.0:
resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==}
dev: false
/@isaacs/cliui@8.0.2:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@ -2828,6 +2835,11 @@ packages:
engines: {node: '>=0.8'}
dev: true
/cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
@ -3034,7 +3046,6 @@ packages:
optional: true
dependencies:
ms: 2.1.2
dev: true
/decode-named-character-reference@1.0.2:
resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==}
@ -3127,6 +3138,11 @@ packages:
engines: {node: '>=0.4.0'}
dev: false
/denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
dev: false
/depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@ -4721,6 +4737,23 @@ packages:
side-channel: 1.0.4
dev: true
/ioredis@5.3.2:
resolution: {integrity: sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==}
engines: {node: '>=12.22.0'}
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.3.4
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
dev: false
/ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -5180,6 +5213,14 @@ packages:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
dev: true
/lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
dev: false
/lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
dev: false
/lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
@ -5907,7 +5948,6 @@ packages:
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -6739,6 +6779,18 @@ packages:
dependencies:
picomatch: 2.3.1
/redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
dev: false
/redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
dependencies:
redis-errors: 1.2.0
dev: false
/reflect.getprototypeof@1.0.4:
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
engines: {node: '>= 0.4'}
@ -7211,6 +7263,10 @@ packages:
get-source: 2.0.12
dev: true
/standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
dev: false
/statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}