Merge pull request #2942 from omnivore-app/fix/rules

improvement on subscriptions and delete user api
This commit is contained in:
Hongbo Wu
2023-10-17 17:14:49 +08:00
committed by GitHub
32 changed files with 289 additions and 137 deletions

View File

@ -22,6 +22,7 @@ export enum RegistrationType {
export enum StatusType {
Active = 'ACTIVE',
Pending = 'PENDING',
Deleted = 'DELETED',
}
@Entity()

View File

@ -1,6 +1,6 @@
import { In } from 'typeorm'
import { appDataSource } from '../data_source'
import { User } from './../entity/user'
import { StatusType, User } from './../entity/user'
const TOP_USERS = [
'jacksonh',
@ -16,7 +16,7 @@ export const MAX_RECORDS_LIMIT = 1000
export const userRepository = appDataSource.getRepository(User).extend({
findById(id: string) {
return this.findOneBy({ id })
return this.findOneBy({ id, status: StatusType.Active })
},
findByEmail(email: string) {

View File

@ -62,11 +62,11 @@ import {
} from '../../services/labels'
import {
createLibraryItem,
findLibraryItemById,
findLibraryItemByUrl,
findLibraryItemsByPrefix,
searchLibraryItems,
updateLibraryItem,
updateLibraryItemReadingProgress,
updateLibraryItems,
} from '../../services/library_item'
import { parsedContentToLibraryItem } from '../../services/save_page'
@ -572,14 +572,8 @@ export const saveArticleReadingProgressResolver = authorized<
readingProgressTopPercent,
},
},
{ uid, pubsub }
{ log, pubsub, uid }
) => {
const libraryItem = await findLibraryItemById(id, uid)
if (!libraryItem) {
return { errorCodes: [SaveArticleReadingProgressErrorCode.NotFound] }
}
if (
readingProgressPercent < 0 ||
readingProgressPercent > 100 ||
@ -590,40 +584,26 @@ export const saveArticleReadingProgressResolver = authorized<
) {
return { errorCodes: [SaveArticleReadingProgressErrorCode.BadData] }
}
// If we have a top percent, we only save it if it's greater than the current top percent
// or set to zero if the top percent is zero.
const readingProgressTopPercentToSave = readingProgressTopPercent
? Math.max(
readingProgressTopPercent,
libraryItem.readingProgressTopPercent || 0
)
: readingProgressTopPercent === 0
? 0
: undefined
// If setting to zero we accept the update, otherwise we require it
// be greater than the current reading progress.
const updatedPart: QueryDeepPartialEntity<LibraryItem> = {
readingProgressBottomPercent:
readingProgressPercent === 0
? 0
: Math.max(
readingProgressPercent,
libraryItem.readingProgressBottomPercent
),
readingProgressHighestReadAnchor:
readingProgressAnchorIndex === 0
? 0
: Math.max(
readingProgressAnchorIndex || 0,
libraryItem.readingProgressHighestReadAnchor
),
readingProgressTopPercent: readingProgressTopPercentToSave,
readAt: new Date(),
}
const updatedItem = await updateLibraryItem(id, updatedPart, uid, pubsub)
try {
const updatedItem = await updateLibraryItemReadingProgress(
id,
uid,
readingProgressPercent,
readingProgressTopPercent,
readingProgressAnchorIndex,
pubsub
)
if (!updatedItem) {
return { errorCodes: [SaveArticleReadingProgressErrorCode.BadData] }
}
return {
updatedArticle: libraryItemToArticle(updatedItem),
return {
updatedArticle: libraryItemToArticle(updatedItem),
}
} catch (error) {
log.error('saveArticleReadingProgressResolver error', error)
return { errorCodes: [SaveArticleReadingProgressErrorCode.Unauthorized] }
}
}
)

View File

@ -33,14 +33,14 @@ export const uploadImportFileResolver = authorized<
UploadImportFileSuccess,
UploadImportFileError,
MutationUploadImportFileArgs
>(async (_, { type, contentType }, { claims: { uid }, log }) => {
>(async (_, { type, contentType }, { uid }) => {
if (!VALID_CONTENT_TYPES.includes(contentType)) {
return {
errorCodes: [UploadImportFileErrorCode.BadRequest],
}
}
const user = await userRepository.findOneBy({ id: uid })
const user = await userRepository.findById(uid)
if (!user) {
return {
errorCodes: [UploadImportFileErrorCode.Unauthorized],

View File

@ -48,9 +48,7 @@ export const createGroupResolver = authorized<
MutationCreateGroupArgs
>(async (_, { input }, { uid, log }) => {
try {
const userData = await userRepository.findOneBy({
id: uid,
})
const userData = await userRepository.findById(uid)
if (!userData) {
return {
errorCodes: [CreateGroupErrorCode.Unauthorized],
@ -107,9 +105,7 @@ export const createGroupResolver = authorized<
export const groupsResolver = authorized<GroupsSuccess, GroupsError>(
async (_, __, { uid, log }) => {
try {
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return {
errorCodes: [GroupsErrorCode.Unauthorized],

View File

@ -18,9 +18,9 @@ export const savePageResolver = authorized<
SaveSuccess,
SaveError,
MutationSavePageArgs
>(async (_, { input }, ctx) => {
>(async (_, { input }, { uid }) => {
analytics.track({
userId: ctx.uid,
userId: uid,
event: 'link_saved',
properties: {
url: input.url,
@ -30,9 +30,7 @@ export const savePageResolver = authorized<
},
})
const user = await userRepository.findOneBy({
id: ctx.uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
@ -44,11 +42,7 @@ export const saveUrlResolver = authorized<
SaveSuccess,
SaveError,
MutationSaveUrlArgs
>(async (_, { input }, ctx) => {
const {
claims: { uid },
} = ctx
>(async (_, { input }, { uid }) => {
analytics.track({
userId: uid,
event: 'link_saved',
@ -60,9 +54,7 @@ export const saveUrlResolver = authorized<
},
})
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}
@ -74,9 +66,9 @@ export const saveFileResolver = authorized<
SaveSuccess,
SaveError,
MutationSaveFileArgs
>(async (_, { input }, ctx) => {
>(async (_, { input }, { uid }) => {
analytics.track({
userId: ctx.uid,
userId: uid,
event: 'link_saved',
properties: {
url: input.url,
@ -86,9 +78,7 @@ export const saveFileResolver = authorized<
},
})
const user = await userRepository.findOneBy({
id: ctx.uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SaveErrorCode.Unauthorized] }
}

View File

@ -14,11 +14,9 @@ const INSTALL_INSTRUCTIONS_EMAIL_TEMPLATE_ID =
export const sendInstallInstructionsResolver = authorized<
SendInstallInstructionsSuccess,
SendInstallInstructionsError
>(async (_parent, _args, { claims, log }) => {
>(async (_parent, _args, { uid, log }) => {
try {
const user = await userRepository.findOneBy({
id: claims.uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [SendInstallInstructionsErrorCode.Unauthorized] }

View File

@ -89,7 +89,8 @@ export const subscriptionsResolver = authorized<
}
const subscriptions = await queryBuilder
.orderBy(`subscription.${sortBy}`, sortOrder, 'NULLS LAST')
.orderBy('subscription.status', 'ASC')
.addOrderBy(`subscription.${sortBy}`, sortOrder, 'NULLS LAST')
.getMany()
return {
@ -184,7 +185,6 @@ export const subscribeResolver = authorized<
url: input.url || undefined,
name: input.name || undefined,
user: { id: uid },
status: SubscriptionStatus.Active,
type: input.subscriptionType || SubscriptionType.Rss, // default to rss
})
)

View File

@ -1,5 +1,9 @@
import * as jwt from 'jsonwebtoken'
import { RegistrationType, User as UserEntity } from '../../entity/user'
import {
RegistrationType,
StatusType,
User as UserEntity,
} from '../../entity/user'
import { env } from '../../env'
import {
DeleteAccountError,
@ -38,6 +42,7 @@ import {
import { userRepository } from '../../repository/user'
import { createUser } from '../../services/create_user'
import { sendVerificationEmail } from '../../services/send_emails'
import { updateUser } from '../../services/user'
import { authorized, userDataToUser } from '../../utils/helpers'
import { validateUsername } from '../../utils/usernamePolicy'
import { WithDataSourcesContext } from '../types'
@ -47,9 +52,7 @@ export const updateUserResolver = authorized<
UpdateUserError,
MutationUpdateUserArgs
>(async (_, { input: { name, bio } }, { uid, authTrx }) => {
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return { errorCodes: [UpdateUserErrorCode.UserNotFound] }
}
@ -87,9 +90,7 @@ export const updateUserProfileResolver = authorized<
UpdateUserProfileError,
MutationUpdateUserProfileArgs
>(async (_, { input: { userId, username, pictureUrl } }, { uid, authTrx }) => {
const user = await userRepository.findOneBy({
id: userId,
})
const user = await userRepository.findById(userId)
if (!user) {
return { errorCodes: [UpdateUserProfileErrorCode.Unauthorized] }
}
@ -112,6 +113,7 @@ export const updateUserProfileResolver = authorized<
profile: {
username: lowerCasedUsername,
},
status: StatusType.Active,
})
if (existingUser?.id) {
return {
@ -156,6 +158,7 @@ export const googleLoginResolver: ResolverFn<
const user = await userRepository.findOneBy({
email,
status: StatusType.Active,
})
if (!user?.id) {
return { errorCodes: [LoginErrorCode.UserNotFound] }
@ -251,9 +254,7 @@ export const getMeUserResolver: ResolverFn<
return undefined
}
const user = await userRepository.findOneBy({
id: claims.uid,
})
const user = await userRepository.findById(claims.uid)
if (!user) {
return undefined
}
@ -277,12 +278,17 @@ export const getUserResolver: ResolverFn<
const userId =
id ||
(username &&
(await userRepository.findOneBy({ profile: { username } }))?.id)
(
await userRepository.findOneBy({
profile: { username },
status: StatusType.Active,
})
)?.id)
if (!userId) {
return { errorCodes: [UserErrorCode.UserNotFound] }
}
const userRecord = await userRepository.findOneBy({ id: userId })
const userRecord = await userRepository.findById(userId)
if (!userRecord) {
return { errorCodes: [UserErrorCode.UserNotFound] }
}
@ -313,9 +319,10 @@ export const deleteAccountResolver = authorized<
DeleteAccountSuccess,
DeleteAccountError,
MutationDeleteAccountArgs
>(async (_, { userID }, { authTrx, log }) => {
const result = await authTrx(async (t) => {
return t.withRepository(userRepository).delete(userID)
>(async (_, { userID }, { log }) => {
// soft delete user
const result = await updateUser(userID, {
status: StatusType.Deleted,
})
if (!result.affected) {
log.error('Error deleting user account')
@ -334,9 +341,7 @@ export const updateEmailResolver = authorized<
MutationUpdateEmailArgs
>(async (_, { input: { email } }, { authTrx, uid, log }) => {
try {
const user = await userRepository.findOneBy({
id: uid,
})
const user = await userRepository.findById(uid)
if (!user) {
return {

View File

@ -15,6 +15,7 @@ import {
suggestedUsername,
} from './jwt_helpers'
import { analytics } from '../../utils/analytics'
import { StatusType } from '../../entity/user'
const appleBaseURL = 'https://appleid.apple.com'
const audienceName = 'app.omnivore.app'
@ -122,6 +123,7 @@ export async function handleAppleWebAuth(
const user = await userRepository.findOneBy({
sourceUserId: decodedTokenResult.sourceUserId,
source: 'APPLE',
status: StatusType.Active,
})
const userId = user?.id

View File

@ -421,7 +421,7 @@ export function authRouter() {
const { email, password } = req.body
try {
const user = await userRepository.findByEmail(email.trim())
if (!user?.id) {
if (!user || user.status === StatusType.Deleted) {
return res.redirect(
`${env.client.url}/auth/email-login?errorCodes=${LoginErrorCode.UserNotFound}`
)
@ -610,7 +610,7 @@ export function authRouter() {
try {
const user = await userRepository.findByEmail(email)
if (!user) {
if (!user || user.status === StatusType.Deleted) {
return res.redirect(`${env.client.url}/auth/reset-sent`)
}

View File

@ -1,6 +1,7 @@
import { google, oauth2_v2 as oauthV2 } from 'googleapis'
import { OAuth2Client } from 'googleapis-common'
import url from 'url'
import { StatusType } from '../../entity/user'
import { env, homePageURL } from '../../env'
import { LoginErrorCode } from '../../generated/graphql'
import { userRepository } from '../../repository/user'
@ -130,6 +131,7 @@ export async function handleGoogleWebAuth(
const user = await userRepository.findOneBy({
email,
source: 'GOOGLE',
status: StatusType.Active,
})
const userId = user?.id

View File

@ -46,7 +46,7 @@ export async function createMobileEmailSignInResponse(
}
const user = await userRepository.findByEmail(email.trim())
if (!user?.id || !user?.password) {
if (!user || !user.password || user.status === StatusType.Deleted) {
throw new Error('user not found')
}

View File

@ -39,7 +39,7 @@ export function userRouter() {
return
}
try {
const user = await userRepository.findOneBy({ id: claims.uid })
const user = await userRepository.findById(claims.uid)
if (!user) {
res.status(400).send('Bad Request')
return

View File

@ -438,6 +438,75 @@ export const updateLibraryItem = async (
return updatedLibraryItem
}
export const updateLibraryItemReadingProgress = async (
id: string,
userId: string,
bottomPercent: number,
topPercent: number | null = null,
anchorIndex: number | null = null,
pubsub = createPubSubClient()
): Promise<LibraryItem | null> => {
// If we have a top percent, we only save it if it's greater than the current top percent
// or set to zero if the top percent is zero.
const result = (await authTrx(
async (tx) =>
tx.getRepository(LibraryItem).query(
`
UPDATE omnivore.library_item
SET reading_progress_top_percent = CASE
WHEN reading_progress_top_percent < $2 THEN $2
WHEN $2 = 0 THEN 0
ELSE reading_progress_top_percent
END,
reading_progress_bottom_percent = CASE
WHEN reading_progress_bottom_percent < $3 THEN $3
WHEN $3 = 0 THEN 0
ELSE reading_progress_bottom_percent
END,
reading_progress_highest_read_anchor = CASE
WHEN reading_progress_top_percent < $4 THEN $4
WHEN $4 = 0 THEN 0
ELSE reading_progress_highest_read_anchor
END,
read_at = now()
WHERE id = $1 AND (
(reading_progress_top_percent < $2 OR $2 = 0) OR
(reading_progress_bottom_percent < $3 OR $3 = 0) OR
(reading_progress_highest_read_anchor < $4 OR $4 = 0)
)
RETURNING
id,
reading_progress_top_percent as "readingProgressTopPercent",
reading_progress_bottom_percent as "readingProgressBottomPercent",
reading_progress_highest_read_anchor as "readingProgressHighestReadAnchor",
read_at as "readAt"
`,
[id, topPercent, bottomPercent, anchorIndex]
),
undefined,
userId
)) as [LibraryItem[], number]
if (result[1] === 0) {
return null
}
const updatedItem = result[0][0]
await pubsub.entityUpdated<QueryDeepPartialEntity<LibraryItem>>(
EntityType.PAGE,
{
id,
readingProgressBottomPercent: updatedItem.readingProgressBottomPercent,
readingProgressTopPercent: updatedItem.readingProgressTopPercent,
readingProgressHighestReadAnchor:
updatedItem.readingProgressHighestReadAnchor,
readAt: updatedItem.readAt,
},
userId
)
return updatedItem
}
export const createLibraryItems = async (
libraryItems: DeepPartial<LibraryItem>[],
userId: string

View File

@ -21,10 +21,7 @@ export const createNewsletterEmail = async (
userId: string,
confirmationCode?: string
): Promise<NewsletterEmail> => {
const user = await userRepository.findOne({
where: { id: userId },
relations: ['profile'],
})
const user = await userRepository.findById(userId)
if (!user) {
return Promise.reject({
errorCode: CreateNewsletterEmailErrorCode.Unauthorized,

View File

@ -42,9 +42,7 @@ export const saveUrlFromEmail = async (
clientRequestId: string,
userId: string
): Promise<boolean> => {
const user = await userRepository.findOneBy({
id: userId,
})
const user = await userRepository.findById(userId)
if (!user) {
return false
}

View File

@ -1,4 +1,4 @@
import { User } from '../entity/user'
import { StatusType, User } from '../entity/user'
import { authTrx } from '../repository'
import { userRepository } from '../repository/user'
@ -13,7 +13,7 @@ export const deleteUser = async (userId: string) => {
}
export const updateUser = async (userId: string, update: Partial<User>) => {
await authTrx(
return authTrx(
async (t) => t.getRepository(User).update(userId, update),
undefined,
userId
@ -21,5 +21,5 @@ export const updateUser = async (userId: string, update: Partial<User>) => {
}
export const findUser = async (id: string): Promise<User | null> => {
return userRepository.findOneBy({ id })
return userRepository.findOneBy({ id, status: StatusType.Active })
}

View File

@ -16,6 +16,7 @@ import { ILike } from 'typeorm'
import { promisify } from 'util'
import { v4 as uuid } from 'uuid'
import { Highlight } from '../entity/highlight'
import { StatusType } from '../entity/user'
import { env } from '../env'
import { PageType, PreparedDocumentInput } from '../generated/graphql'
import { userRepository } from '../repository/user'
@ -465,6 +466,7 @@ export const isProbablyArticle = async (
): Promise<boolean> => {
const user = await userRepository.findOneBy({
email: ILike(email),
status: StatusType.Active,
})
return !!user || subject.includes(ARTICLE_PREFIX)
}

View File

@ -706,7 +706,6 @@ describe('Article API', () => {
).to.eq(75)
// Now try to set to a lower value (50), value should not be updated
// refresh index to ensure the reading progress is updated
const secondQuery = saveArticleReadingProgressQuery(itemId, 50)
const secondRes = await graphqlRequest(secondQuery, authToken).expect(200)
expect(

View File

@ -146,7 +146,7 @@ describe('Subscriptions API', () => {
undefined,
SubscriptionType.Newsletter
)
const allSubscriptions = [sub5, ...subscriptions]
const allSubscriptions = [...subscriptions, sub5]
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.subscriptions.subscriptions).to.eql(

View File

@ -9,7 +9,7 @@ import { findProfile } from '../../src/services/profile'
import { deleteUser, findUser } from '../../src/services/user'
import { hashPassword } from '../../src/utils/auth'
import { createTestUser } from '../db'
import { graphqlRequest, request } from '../util'
import { generateFakeUuid, graphqlRequest, request } from '../util'
describe('User API', () => {
const correctPassword = 'fakePassword'
@ -238,4 +238,58 @@ describe('User API', () => {
return graphqlRequest(query, invalidAuthToken).expect(500)
})
})
describe('Delete account', () => {
const query = (userId: string) => `
mutation {
deleteAccount(
userID: "${userId}"
) {
... on DeleteAccountSuccess {
userID
}
... on DeleteAccountError {
errorCodes
}
}
}
`
let userId: string
let authToken: string
before(async () => {
const user = await createTestUser('to_delete_user')
const res = await request
.post('/local/debug/fake-user-login')
.send({ fakeEmail: user.email })
userId = user.id
authToken = res.body.authToken
})
after(async () => {
await deleteUser(userId)
})
context('when user id is valid', () => {
it('deletes user and responds with 200', async () => {
const response = await graphqlRequest(query(userId), authToken).expect(
200
)
expect(response.body.data.deleteAccount.userID).to.eql(userId)
})
})
context('when user not found', () => {
it('responds with error code UserNotFound', async () => {
const response = await graphqlRequest(
query(generateFakeUuid()),
authToken
).expect(200)
expect(response.body.data.deleteAccount.errorCodes).to.eql([
'USER_NOT_FOUND',
])
})
})
})
})

View File

@ -0,0 +1,9 @@
-- Type: DO
-- Name: alter_user_status_type
-- Description: Add DELETED to the user_status_type enum
BEGIN;
ALTER TYPE user_status_type ADD VALUE 'DELETED';
COMMIT;

View File

@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: alter_user_status_type
-- Description: Add DELETED to the user_status_type enum
BEGIN;
ALTER TYPE user_status_type DROP VALUE IF EXISTS 'DELETED';
COMMIT;

View File

@ -0,0 +1,18 @@
-- Type: DO
-- Name: add_unique_to_subscription_url
-- Description: Add unique constraint to the url field on the omnivore.subscription table
BEGIN;
-- Deleting duplicates first to avoid unique constraint violation
WITH DuplicateCTE AS (
SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id, url ORDER BY status, last_fetched_at DESC NULLS LAST) AS row_number
FROM omnivore.subscriptions
WHERE type = 'RSS'
)
DELETE FROM omnivore.subscriptions
WHERE id IN (SELECT id FROM DuplicateCTE WHERE row_number > 1);
ALTER TABLE omnivore.subscriptions ADD CONSTRAINT subscriptions_user_id_url_key UNIQUE (user_id, url);
COMMIT;

View File

@ -0,0 +1,9 @@
-- Type: UNDO
-- Name: add_unique_to_subscription_url
-- Description: Add unique constraint to the url field on the omnivore.subscription table
BEGIN;
ALTER TABLE omnivore.subscriptions DROP CONSTRAINT IF EXISTS subscriptions_user_id_url_key;
COMMIT;

View File

@ -0,0 +1,15 @@
-- Type: DO
-- Name: alter_library_item_tsv_update_trigger
-- Description: Alter library_item_tsv_update trigger on omnivore.library_item table to add conditions
BEGIN;
DROP TRIGGER IF EXISTS library_item_tsv_update ON omnivore.library_item;
CREATE TRIGGER library_item_tsv_update
BEFORE INSERT OR UPDATE OF readable_content, site_name, title, author, description, note, highlight_annotations
ON omnivore.library_item
FOR EACH ROW
EXECUTE PROCEDURE update_library_item_tsv();
COMMIT;

View File

@ -0,0 +1,15 @@
-- Type: UNDO
-- Name: alter_library_item_tsv_update_trigger
-- Description: Alter library_item_tsv_update trigger on omnivore.library_item table to add conditions
BEGIN;
DROP TRIGGER IF EXISTS library_item_tsv_update ON omnivore.library_item;
CREATE TRIGGER library_item_tsv_update
BEFORE INSERT OR UPDATE
ON omnivore.library_item
FOR EACH ROW
EXECUTE PROCEDURE update_library_item_tsv();
COMMIT;

View File

@ -9,7 +9,6 @@
"author": "",
"license": "ISC",
"dependencies": {
"@elastic/elasticsearch": "~7.12.0",
"dotenv": "^8.2.0",
"pg": "^8.3.0",
"postgrator": "^4.1.1",

View File

@ -6,6 +6,9 @@ interface SearchResponse {
edges: Edge[]
}
}
errors?: {
message: string
}[]
}
interface Edge {
@ -28,7 +31,6 @@ interface Label {
}
export const search = async (
userId: string,
apiEndpoint: string,
auth: string,
query: string
@ -75,6 +77,12 @@ export const search = async (
}
)
if (response.data.errors) {
console.error(response.data.errors)
return []
}
const edges = response.data.data.search.edges
if (edges.length === 0) {
return []
@ -89,14 +97,13 @@ export const search = async (
}
export const filterPage = async (
userId: string,
apiEndpoint: string,
auth: string,
filter: string,
pageId: string
): Promise<Page | null> => {
filter += ` includes:${pageId}`
const pages = await search(userId, apiEndpoint, auth, filter)
const pages = await search(apiEndpoint, auth, filter)
return pages.length > 0 ? pages[0] : null
}

View File

@ -93,7 +93,6 @@ export const triggerActions = async (
}
const filteredPage = await filterPage(
userId,
apiEndpoint,
authToken,
rule.filter,

View File

@ -2228,17 +2228,6 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@elastic/elasticsearch@~7.12.0":
version "7.12.0"
resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.12.0.tgz#dbb51a2841f644b670a56d8c15899e860928856f"
integrity sha512-GquUEytCijFRPEk3DKkkDdyhspB3qbucVQOwih9uNyz3iz804I+nGBUsFo2LwVvLQmQfEM0IY2+yoYfEz5wMug==
dependencies:
debug "^4.3.1"
hpagent "^0.1.1"
ms "^2.1.3"
pump "^3.0.0"
secure-json-parse "^2.3.1"
"@emotion/cache@^10.0.27":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
@ -15835,11 +15824,6 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1"
wbuf "^1.1.0"
hpagent@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9"
integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==
html-encoding-sniffer@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
@ -20177,7 +20161,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:
ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@ -24468,11 +24452,6 @@ search-query-parser@^1.6.0:
resolved "https://registry.yarnpkg.com/search-query-parser/-/search-query-parser-1.6.0.tgz#d69ade33f3685cae25613a70189b7b18970b46f1"
integrity sha512-bhf+phLlKF38nuniwLcVHWPArHGdzenlPhPi955CR3vm1QQifXIuPHwAffhjapojdVVzmv4hgIJ6NOX1d/w+Uw==
secure-json-parse@^2.3.1:
version "2.4.0"
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.4.0.tgz#5aaeaaef85c7a417f76271a4f5b0cc3315ddca85"
integrity sha512-Q5Z/97nbON5t/L/sH6mY2EacfjVGwrCcSi5D3btRO2GZ8pf1K1UN7Z9H5J57hjVU2Qzxr1xO+FmBhOvEkzCMmg==
selderee@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.6.0.tgz#f3bee66cfebcb6f33df98e4a1df77388b42a96f7"