Teams refactor: removed team websites.

This commit is contained in:
Mike Cao 2024-01-25 23:20:53 -08:00
parent 0d442b751d
commit f85393f8df
23 changed files with 190 additions and 351 deletions

View File

@ -67,7 +67,7 @@
"@prisma/extension-read-replicas": "^0.3.0",
"@react-spring/web": "^9.7.3",
"@tanstack/react-query": "^5.12.2",
"@umami/prisma-client": "^0.8.0",
"@umami/prisma-client": "^0.13.0",
"@umami/redis-client": "^0.18.0",
"chalk": "^4.1.1",
"chart.js": "^4.2.1",

View File

@ -17,6 +17,12 @@ export function TeamsTable({ data = [] }: { data: any[] }) {
<GridColumn name="owner" label={formatMessage(labels.owner)}>
{row => row.teamUser.find(({ role }) => role === ROLES.teamOwner)?.user?.username}
</GridColumn>
<GridColumn name="websites" label={formatMessage(labels.websites)}>
{row => row._count.website}
</GridColumn>
<GridColumn name="members" label={formatMessage(labels.members)}>
{row => row._count.teamUser}
</GridColumn>
<GridColumn name="action" label=" " alignment="end">
{row => {
const { id, name, teamUser } = row;

View File

@ -1,8 +1,8 @@
import useApi from 'components/hooks/useApi';
import TeamMembersTable from './TeamMembersTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
import useCache from 'store/cache';
import TeamMembersTable from './TeamMembersTable';
export function TeamMembers({ teamId, readOnly }: { teamId: string; readOnly: boolean }) {
const { get } = useApi();
@ -18,11 +18,9 @@ export function TeamMembers({ teamId, readOnly }: { teamId: string; readOnly: bo
});
return (
<>
<DataTable queryResult={queryResult}>
{({ data }) => <TeamMembersTable data={data} teamId={teamId} readOnly={readOnly} />}
</DataTable>
</>
<DataTable queryResult={queryResult}>
{({ data }) => <TeamMembersTable data={data} teamId={teamId} readOnly={readOnly} />}
</DataTable>
);
}

View File

@ -25,7 +25,6 @@ export function TeamSettings({ teamId }: { teamId: string }) {
return get(`/teams/${teamId}`);
}
},
gcTime: 0,
});
const canEdit = data?.teamUser?.find(
({ userId, role }) => role === ROLES.teamOwner && userId === user.id,

View File

@ -1,79 +0,0 @@
import useApi from 'components/hooks/useApi';
import { useState } from 'react';
import { Button, Form, FormButtons, GridColumn, Loading, SubmitButton, Toggle } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import WebsitesDataTable from 'app/(main)/settings/websites/WebsitesDataTable';
import Empty from 'components/common/Empty';
import { setValue } from 'store/cache';
import { useUser } from 'components/hooks';
export function TeamWebsiteAddForm({
teamId,
onSave,
onClose,
}: {
teamId: string;
onSave: () => void;
onClose: () => void;
}) {
const { user } = useUser();
const { formatMessage, labels } = useMessages();
const { get, post, useQuery, useMutation } = useApi();
const { mutate, error } = useMutation({
mutationFn: (data: any) => post(`/teams/${teamId}/websites`, data),
});
const { data: websites, isLoading } = useQuery({
queryKey: ['websites'],
queryFn: () => get('/websites'),
});
const [selected, setSelected] = useState([]);
const hasData = websites && websites.data.length > 0;
const handleSubmit = () => {
mutate(
{ websiteIds: selected },
{
onSuccess: async () => {
setValue('team:websites', Date.now());
onSave?.();
onClose?.();
},
},
);
};
const handleSelect = id => {
setSelected(state => (state.includes(id) ? state.filter(n => n !== id) : state.concat(id)));
};
return (
<>
{isLoading && !hasData && <Loading icon="dots" position="center" />}
{!isLoading && !hasData && <Empty />}
{hasData && (
<Form onSubmit={handleSubmit} error={error}>
<WebsitesDataTable userId={user.id} showActions={false}>
<GridColumn name="select" label={formatMessage(labels.selectWebsite)} alignment="end">
{row => (
<Toggle
key={row.id}
value={row.id}
checked={selected?.includes(row.id)}
onChange={handleSelect.bind(null, row.id)}
/>
)}
</GridColumn>
</WebsitesDataTable>
<FormButtons flex>
<SubmitButton variant="primary" disabled={selected?.length === 0}>
{formatMessage(labels.addWebsite)}
</SubmitButton>
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
</FormButtons>
</Form>
)}
</>
);
}
export default TeamWebsiteAddForm;

View File

@ -1,13 +1,13 @@
import useFilterQuery from 'components/hooks/useFilterQuery';
import WebsitesTable from 'app/(main)/settings/websites/WebsitesTable';
import DataTable from 'components/common/DataTable';
import useFilterQuery from 'components/hooks/useFilterQuery';
import useApi from 'components/hooks/useApi';
import useUser from 'components/hooks/useUser';
import useCache from 'store/cache';
import WebsitesTable from '../../websites/WebsitesTable';
export function TeamWebsites({ teamId }: { teamId: string; readOnly: boolean }) {
const { user } = useUser();
const { get } = useApi();
const { user } = useUser();
const modified = useCache(state => state?.['team:websites']);
const queryResult = useFilterQuery({
queryKey: ['team:websites', { teamId, modified }],

View File

@ -1,6 +1,6 @@
import { Metadata } from 'next';
import TeamsDataTable from './TeamsDataTable';
import TeamsHeader from './TeamsHeader';
import { Metadata } from 'next';
export default function () {
if (process.env.cloudMode) {
@ -16,5 +16,5 @@ export default function () {
}
export const metadata: Metadata = {
title: 'Teams Settings | umami',
title: 'Teams Settings - Umami',
};

View File

@ -3,10 +3,9 @@ import useApi from 'components/hooks/useApi';
import useFilterQuery from 'components/hooks/useFilterQuery';
import DataTable from 'components/common/DataTable';
import UsersTable from './UsersTable';
import UsersHeader from './UsersHeader';
import useCache from 'store/cache';
export function UsersDataTable() {
export function UsersDataTable({ showActions }: { showActions: boolean }) {
const { get } = useApi();
const modified = useCache((state: any) => state?.users);
const queryResult = useFilterQuery({
@ -15,10 +14,9 @@ export function UsersDataTable() {
});
return (
<>
<UsersHeader />
<DataTable queryResult={queryResult}>{({ data }) => <UsersTable data={data} />}</DataTable>
</>
<DataTable queryResult={queryResult}>
{({ data }) => <UsersTable data={data} showActions={showActions} />}
</DataTable>
);
}

View File

@ -6,7 +6,13 @@ import useMessages from 'components/hooks/useMessages';
import useLocale from 'components/hooks/useLocale';
import UserDeleteButton from './UserDeleteButton';
export function UsersTable({ data = [] }: { data: any[] }) {
export function UsersTable({
data = [],
showActions = true,
}: {
data: any[];
showActions?: boolean;
}) {
const { formatMessage, labels } = useMessages();
const { dateLocale } = useLocale();
const breakpoint = useBreakpoint();
@ -29,24 +35,26 @@ export function UsersTable({ data = [] }: { data: any[] }) {
})
}
</GridColumn>
<GridColumn name="action" label=" " alignment="end">
{row => {
const { id, username } = row;
return (
<>
<Link href={`/settings/users/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
<UserDeleteButton userId={id} username={username} />
</>
);
}}
</GridColumn>
{showActions && (
<GridColumn name="action" label=" " alignment="end">
{row => {
const { id, username } = row;
return (
<>
<Link href={`/settings/users/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
<UserDeleteButton userId={id} username={username} />
</>
);
}}
</GridColumn>
)}
</GridTable>
);
}

View File

@ -1,8 +1,14 @@
import UsersDataTable from './UsersDataTable';
import { Metadata } from 'next';
import UsersDataTable from './UsersDataTable';
import UsersHeader from './UsersHeader';
export default function () {
return <UsersDataTable />;
return (
<>
<UsersHeader />
<UsersDataTable />
</>
);
}
export const metadata: Metadata = {
title: 'Users | umami',

View File

@ -52,6 +52,7 @@ export const labels = defineMessages({
deleteWebsite: { id: 'label.delete-website', defaultMessage: 'Delete website' },
reset: { id: 'label.reset', defaultMessage: 'Reset' },
addWebsite: { id: 'label.add-website', defaultMessage: 'Add website' },
addMember: { id: 'label.add-member', defaultMessage: 'Add member' },
addDescription: { id: 'label.add-description', defaultMessage: 'Add description' },
changePassword: { id: 'label.change-password', defaultMessage: 'Change password' },
currentPassword: { id: 'label.current-password', defaultMessage: 'Current password' },

View File

@ -18,7 +18,6 @@ export * from 'components/hooks/useTimezone';
export * from 'components/hooks/useUser';
export * from 'components/hooks/useWebsite';
export * from 'app/(main)/settings/teams/[id]/TeamWebsiteAddForm';
export * from 'app/(main)/settings/teams/[id]/TeamEditForm';
export * from 'app/(main)/settings/teams/[id]/TeamMemberRemoveButton';
export * from 'app/(main)/settings/teams/[id]/TeamMembers';

View File

@ -1,10 +1,10 @@
import { Report } from '@prisma/client';
import debug from 'debug';
import redis from '@umami/redis-client';
import { PERMISSIONS, ROLE_PERMISSIONS, SHARE_TOKEN_HEADER } from 'lib/constants';
import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from 'lib/constants';
import { secret } from 'lib/crypto';
import { createSecureToken, ensureArray, getRandomChars, parseToken } from 'next-basics';
import { findTeamWebsiteByUserId, getTeamUser, getTeamWebsite } from 'queries';
import { getTeamUser, getWebsiteById } from 'queries';
import { loadWebsite } from './load';
import { Auth } from './types';
import { NextApiRequest } from 'next';
@ -55,8 +55,6 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
if (user.id === website?.userId) {
return true;
}
return !!(await findTeamWebsiteByUserId(websiteId, user.id));
}
export async function canViewAllWebsites({ user }: Auth) {
@ -178,16 +176,12 @@ export async function canDeleteTeamWebsite({ user }: Auth, teamId: string, websi
return true;
}
const teamWebsite = await getTeamWebsite(teamId, websiteId);
const teamWebsite = await getWebsiteById(websiteId);
if (teamWebsite?.website?.userId === user.id) {
return true;
}
if (teamWebsite && teamWebsite.teamId === teamId) {
const teamUser = await getTeamUser(teamId, user.id);
if (teamWebsite) {
const teamUser = await getTeamUser(teamWebsite.teamId, user.id);
return hasPermission(teamUser.role, PERMISSIONS.teamUpdate);
return teamUser.role === ROLES.teamOwner || teamUser.role === ROLES.teamMember;
}
return false;

View File

@ -125,6 +125,7 @@ export const ROLES = {
viewOnly: 'view-only',
teamOwner: 'team-owner',
teamMember: 'team-member',
teamGuest: 'team-guest',
} as const;
export const PERMISSIONS = {

View File

@ -68,7 +68,12 @@ export default async (
});
}
log(`Login from ip ${getIpAddress(req)} with username "${username.replace(/["\r\n]/g, '')}" failed.`);
log(
`Login from ip ${getIpAddress(req)} with username "${username.replace(
/["\r\n]/g,
'',
)}" failed.`,
);
return unauthorized(res, 'message.incorrect-username-password');
}

View File

@ -6,7 +6,6 @@ import { pageInfo } from 'lib/schema';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { getWebsitesByTeamId } from 'queries';
import { createTeamWebsites } from 'queries/admin/teamWebsite';
export interface TeamWebsiteRequestQuery extends SearchFilter {
id: string;
@ -52,17 +51,5 @@ export default async (
return ok(res, websites);
}
if (req.method === 'POST') {
if (!(await canViewTeam(req.auth, teamId))) {
return unauthorized(res);
}
const { websiteIds } = req.body;
const websites = await createTeamWebsites(teamId, websiteIds);
return ok(res, websites);
}
return methodNotAllowed(res);
};

View File

@ -10,8 +10,9 @@ export interface GetTeamOptions {
async function getTeam(where: Prisma.TeamWhereInput, options: GetTeamOptions = {}): Promise<Team> {
const { includeTeamUser = false } = options;
const { client } = prisma;
return prisma.client.team.findFirst({
return client.team.findFirst({
where,
include: {
teamUser: includeTeamUser,
@ -27,14 +28,15 @@ export function getTeamByAccessCode(accessCode: string, options: GetTeamOptions
return getTeam({ accessCode }, options);
}
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<Team> {
export async function createTeam(data: Prisma.TeamCreateInput, userId: string): Promise<any> {
const { id } = data;
const { client, transaction } = prisma;
return prisma.transaction([
prisma.client.team.create({
return transaction([
client.team.create({
data,
}),
prisma.client.teamUser.create({
client.teamUser.create({
data: {
id: uuid(),
teamId: id,
@ -46,7 +48,9 @@ export async function createTeam(data: Prisma.TeamCreateInput, userId: string):
}
export async function updateTeam(teamId: string, data: Prisma.TeamUpdateInput): Promise<Team> {
return prisma.client.team.update({
const { client } = prisma;
return client.team.update({
where: {
id: teamId,
},
@ -61,13 +65,22 @@ export async function deleteTeam(
teamId: string,
): Promise<Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Team]>> {
const { client, transaction } = prisma;
const cloudMode = process.env.CLOUD_MODE;
if (cloudMode) {
return transaction([
client.team.update({
data: {
deletedAt: new Date(),
},
where: {
id: teamId,
},
}),
]);
}
return transaction([
client.teamWebsite.deleteMany({
where: {
teamId,
},
}),
client.teamUser.deleteMany({
where: {
teamId,
@ -87,6 +100,7 @@ export async function getTeams(
): Promise<FilterResult<Team[]>> {
const { userId, query } = filters;
const mode = prisma.getQueryMode();
const { client } = prisma;
const where: Prisma.TeamWhereInput = {
...(userId && {
@ -123,7 +137,7 @@ export async function getTeams(
...filters,
});
const teams = await prisma.client.team.findMany({
const teams = await client.team.findMany({
where: {
...where,
},
@ -131,7 +145,7 @@ export async function getTeams(
...(options?.include && { include: options?.include }),
});
const count = await prisma.client.team.count({ where });
const count = await client.team.count({ where });
return { data: teams, count, ...getParameters };
}
@ -154,6 +168,9 @@ export async function getTeamsByUserId(
},
},
},
_count: {
select: { website: true, teamUser: true },
},
},
},
);

View File

@ -68,14 +68,6 @@ export async function deleteTeamUser(teamId: string, userId: string): Promise<Te
const { client, transaction } = prisma;
return transaction([
client.teamWebsite.deleteMany({
where: {
teamId: teamId,
website: {
userId: userId,
},
},
}),
client.teamUser.deleteMany({
where: {
teamId,

View File

@ -1,127 +0,0 @@
import { Prisma, Team, TeamUser, TeamWebsite, Website } from '@prisma/client';
import { ROLES } from 'lib/constants';
import { uuid } from 'lib/crypto';
import prisma from 'lib/prisma';
export async function getTeamWebsite(
teamId: string,
websiteId: string,
): Promise<
TeamWebsite & {
website: Website;
}
> {
return prisma.client.teamWebsite.findFirst({
where: {
teamId,
websiteId,
},
include: {
website: true,
},
});
}
export async function findTeamWebsiteByUserId(
websiteId: string,
userId: string,
): Promise<TeamWebsite> {
return prisma.client.teamWebsite.findFirst({
where: {
websiteId,
team: {
teamUser: {
some: {
userId,
},
},
},
},
});
}
export async function getTeamWebsites(teamId: string): Promise<
(TeamWebsite & {
team: Team & { teamUser: TeamUser[] };
website: Website & {
user: { id: string; username: string };
};
})[]
> {
return prisma.client.teamWebsite.findMany({
where: {
teamId,
},
include: {
team: {
include: {
teamUser: {
where: {
role: ROLES.teamOwner,
},
},
},
},
website: {
include: {
user: {
select: {
id: true,
username: true,
},
},
},
},
},
orderBy: [
{
team: {
name: 'asc',
},
},
],
});
}
export async function createTeamWebsite(teamId: string, websiteId: string): Promise<TeamWebsite> {
return prisma.client.teamWebsite.create({
data: {
id: uuid(),
teamId,
websiteId,
},
});
}
export async function createTeamWebsites(teamId: string, websiteIds: string[]) {
const currentTeamWebsites = await getTeamWebsites(teamId);
// filter out websites that already exists on the team
const addWebsites = websiteIds.filter(
websiteId => !currentTeamWebsites.some(a => a.websiteId === websiteId),
);
const teamWebsites: Prisma.TeamWebsiteCreateManyInput[] = addWebsites.map(a => {
return {
id: uuid(),
teamId,
websiteId: a,
};
});
return prisma.client.teamWebsite.createMany({
data: teamWebsites,
});
}
export async function deleteTeamWebsite(
teamId: string,
websiteId: string,
): Promise<Prisma.BatchPayload> {
return prisma.client.teamWebsite.deleteMany({
where: {
teamId,
websiteId,
},
});
}

View File

@ -105,6 +105,7 @@ export async function getUsersByTeamId(teamId: string, filter?: UserSearchFilter
include: {
teamUser: {
select: {
teamId: true,
role: true,
},
},
@ -188,7 +189,27 @@ export async function deleteUser(
const teamIds = teams.map(a => a.id);
return prisma
if (cloudMode) {
return client.transaction([
client.website.updateMany({
data: {
deletedAt: new Date(),
},
where: { id: { in: websiteIds } },
}),
client.user.update({
data: {
username: getRandomChars(32),
deletedAt: new Date(),
},
where: {
id: userId,
},
}),
]);
}
return client
.transaction([
client.eventData.deleteMany({
where: { websiteId: { in: websiteIds } },
@ -199,29 +220,6 @@ export async function deleteUser(
client.session.deleteMany({
where: { websiteId: { in: websiteIds } },
}),
client.teamWebsite.deleteMany({
where: {
OR: [
{
websiteId: {
in: websiteIds,
},
},
{
teamId: {
in: teamIds,
},
},
],
},
}),
client.teamWebsite.deleteMany({
where: {
teamId: {
in: teamIds,
},
},
}),
client.teamUser.deleteMany({
where: {
OR: [
@ -257,33 +255,16 @@ export async function deleteUser(
],
},
}),
cloudMode
? client.website.updateMany({
data: {
deletedAt: new Date(),
},
where: { id: { in: websiteIds } },
})
: client.website.deleteMany({
where: { id: { in: websiteIds } },
}),
cloudMode
? client.user.update({
data: {
username: getRandomChars(32),
deletedAt: new Date(),
},
where: {
id: userId,
},
})
: client.user.delete({
where: {
id: userId,
},
}),
client.website.deleteMany({
where: { id: { in: websiteIds } },
}),
client.user.delete({
where: {
id: userId,
},
}),
])
.then(async data => {
.then(async (data: any) => {
if (cache.enabled) {
const ids = websites.map(a => a.id);

View File

@ -260,7 +260,7 @@ export async function deleteWebsite(
},
}),
cloudMode
? prisma.client.website.update({
? client.website.update({
data: {
deletedAt: new Date(),
},

View File

@ -1,7 +1,6 @@
export * from './admin/report';
export * from './admin/team';
export * from './admin/teamUser';
export * from './admin/teamWebsite';
export * from './admin/user';
export * from './admin/website';
export * from './analytics/events/getEventMetrics';

View File

@ -2597,13 +2597,15 @@
"@typescript-eslint/types" "6.14.0"
eslint-visitor-keys "^3.4.1"
"@umami/prisma-client@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@umami/prisma-client/-/prisma-client-0.8.0.tgz#9f866c813b15b7ab0e7632506316bf1e5d2e74cc"
integrity sha512-ix3/75CO3eVlf1Rg0cUIjoHDFJV7nxx5sSh1NnvbjyGn8EsTpZ7fVYF874w8+ENQsaKFIMftUSGGiwvgxaZN3g==
"@umami/prisma-client@^0.13.0":
version "0.13.0"
resolved "https://registry.yarnpkg.com/@umami/prisma-client/-/prisma-client-0.13.0.tgz#00ed90adf36e8e8cb7301ab557ad617dd946188f"
integrity sha512-HkhwTH7cxkbqlxw6LK8QGbEaIl0aYZeVf5bUwncv68C/RaQrQ0/g9x6TzHo6uKlSgk2LtavnGwxYmjxD1fdGQw==
dependencies:
"@prisma/extension-read-replicas" "^0.3.0"
chalk "^4.1.2"
debug "^4.3.4"
sql-formatter "^15.1.2"
"@umami/redis-client@^0.18.0":
version "0.18.0"
@ -3377,7 +3379,7 @@ commander@11.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67"
integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==
commander@2, commander@^2.20.0, commander@^2.20.3:
commander@2, commander@^2.19.0, commander@^2.20.0, commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -3933,6 +3935,11 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
discontinuous-range@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@ -4793,6 +4800,11 @@ get-port-please@^3.1.1:
resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.1.1.tgz#2556623cddb4801d823c0a6a15eec038abb483be"
integrity sha512-3UBAyM3u4ZBVYDsxOQfJDxEa6XTbpBDrOjp4mf7ExFRt5BKs/QywQQiJsh2B+hxcZLSapWqCRvElUe8DnKcFHA==
get-stdin@=8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53"
integrity sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==
get-stream@^6.0.0, get-stream@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
@ -6308,6 +6320,11 @@ moment-timezone@^0.5.35:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
moo@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==
mri@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@ -6353,6 +6370,16 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
nearley@^2.20.1:
version "2.20.1"
resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==
dependencies:
commander "^2.19.0"
moo "^0.5.0"
railroad-diagrams "^1.0.0"
randexp "0.4.6"
next-basics@^0.39.0:
version "0.39.0"
resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.39.0.tgz#1ec448a1c12966a82067445bfb9319b7e883dd6a"
@ -7507,6 +7534,19 @@ raf-schd@^4.0.2:
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
railroad-diagrams@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
dependencies:
discontinuous-range "1.0.0"
ret "~0.1.10"
randombytes@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -7883,6 +7923,11 @@ restore-cursor@^4.0.0:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@~0.1.10:
version "0.1.15"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@ -8280,6 +8325,15 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
sql-formatter@^15.1.2:
version "15.1.2"
resolved "https://registry.yarnpkg.com/sql-formatter/-/sql-formatter-15.1.2.tgz#86df2592eedf6d422244e10e00a74380c22791b7"
integrity sha512-zBrLBclCNurCsQaO6yMvkXzHvv7eJPjiF8LIEQ5HdBV/x6UuWIZwqss3mlZ/6HLj+VYhFKeHpQnyLuZWG2agKQ==
dependencies:
argparse "^2.0.1"
get-stdin "=8.0.0"
nearley "^2.20.1"
stable@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"