diff --git a/package.json b/package.json index 04d0134bd81..938c349b68e 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "husky": "^7.0.4", "jscodeshift": "^0.14.0", "kysely": "^0.27.4", - "kysely-codegen": "^0.15.0", + "kysely-codegen": "^0.17.0", "kysely-ctl": "^0.9.0", "lerna": "^6.4.1", "mini-css-extract-plugin": "^2.7.2", diff --git a/packages/server/dataloader/integrationAuthLoaders.ts b/packages/server/dataloader/integrationAuthLoaders.ts index 9099b920886..a6a3b79227f 100644 --- a/packages/server/dataloader/integrationAuthLoaders.ts +++ b/packages/server/dataloader/integrationAuthLoaders.ts @@ -1,4 +1,5 @@ import DataLoader from 'dataloader' +import {Selectable, sql} from 'kysely' import errorFilter from '../graphql/errorFilter' import isValid from '../graphql/isValid' import getKysely from '../postgres/getKysely' @@ -8,7 +9,7 @@ import getIntegrationProvidersByIds, { } from '../postgres/queries/getIntegrationProvidersByIds' import {selectSlackNotifications, selectTeamMemberIntegrationAuth} from '../postgres/select' import {SlackAuth, SlackNotification, TeamMemberIntegrationAuth} from '../postgres/types' -import {NotificationSettings} from '../postgres/types/pg' +import {TeamNotificationSettings} from '../postgres/types/pg' import NullableDataLoader from './NullableDataLoader' import RootDataLoader from './RootDataLoader' @@ -164,9 +165,9 @@ export const slackNotificationsByTeamIdAndEvent = (parent: RootDataLoader) => { }) } -export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoader) => { +export const teamMemberIntegrationAuthsByTeamIdAndService = (parent: RootDataLoader) => { return new DataLoader< - {teamId: string; service: IntegrationProviderServiceEnum; event: SlackNotification['event']}, + {teamId: string; service: IntegrationProviderServiceEnum}, TeamMemberIntegrationAuth[], string >( @@ -174,13 +175,12 @@ export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoade const pg = getKysely() const res = (await pg .selectFrom('TeamMemberIntegrationAuth') - .innerJoin('NotificationSettings', 'authId', 'TeamMemberIntegrationAuth.id') .selectAll() .where(({eb, refTuple, tuple}) => eb( - refTuple('teamId', 'service', 'event'), + refTuple('teamId', 'service'), 'in', - keys.map(({teamId, service, event}) => tuple(teamId, service, event)) + keys.map(({teamId, service}) => tuple(teamId, service)) ) ) .execute()) as unknown as TeamMemberIntegrationAuth[] @@ -196,17 +196,33 @@ export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoade ) } -export const notificationSettingsByAuthId = (parent: RootDataLoader) => { - return new DataLoader( +export const notificationSettingsByProviderIdAndTeamId = (parent: RootDataLoader) => { + return new DataLoader< + {providerId: number; teamId: string}, + Selectable['events'], + string + >( async (keys) => { const pg = getKysely() const res = await pg - .selectFrom('NotificationSettings') + .selectFrom('TeamNotificationSettings') .selectAll() - .where(({eb}) => eb('authId', 'in', keys)) + // convert to text[] as kysely would otherwise not parse the array + .select(sql`events::text[]`.as('events')) + .where(({eb, refTuple, tuple}) => + eb( + refTuple('providerId', 'teamId'), + 'in', + keys.map(({providerId, teamId}) => tuple(providerId, teamId)) + ) + ) .execute() - return keys.map((key) => res.filter(({authId}) => authId === key).map(({event}) => event)) + return keys.map( + (key) => + res.find(({providerId, teamId}) => providerId === key.providerId && teamId === key.teamId) + ?.events || [] + ) }, { ...parent.dataLoaderOptions diff --git a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts index 3172da054f8..854bb61a852 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts @@ -7,11 +7,12 @@ import {IntegrationProviderMSTeams as IIntegrationProviderMSTeams} from '../../. import {SlackNotification, Team} from '../../../../postgres/types' import IUser from '../../../../postgres/types/IUser' import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' -import {NotificationSettings} from '../../../../postgres/types/pg' import MSTeamsServerManager from '../../../../utils/MSTeamsServerManager' import {analytics} from '../../../../utils/analytics/analytics' import sendToSentry from '../../../../utils/sendToSentry' import {DataLoaderWorker} from '../../../graphql' +import isValid from '../../../isValid' +import {SlackNotificationEventEnum} from '../../../public/resolverTypes' import {NotificationIntegrationHelper} from './NotificationIntegrationHelper' import {createNotifier} from './Notifier' import getSummaryText from './getSummaryText' @@ -338,22 +339,38 @@ async function getMSTeams( dataLoader: DataLoaderWorker, teamId: string, userId: string, - event: NotificationSettings['event'] + event: SlackNotificationEventEnum ) { const [auths, user] = await Promise.all([ dataLoader - .get('teamMemberIntegrationAuthsByTeamIdAndEvent') - .load({service: 'msTeams', teamId, event}), + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({service: 'msTeams', teamId}), dataLoader.get('users').loadNonNull(userId) ]) - return Promise.all( - auths.map(async (auth) => { - const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) - return MSTeamsNotificationHelper({ - ...(provider as IntegrationProviderMSTeams), - userId, - email: user.email + + const providers = ( + await Promise.all( + auths.map(async (auth) => { + const {providerId} = auth + const [provider, events] = await Promise.all([ + dataLoader + .get('integrationProviders') + .loadNonNull(providerId) as Promise, + dataLoader.get('notificationSettingsByProviderIdAndTeamId').load({providerId, teamId}) + ]) + if (events.includes(event)) { + return provider + } + return null }) + ) + ).filter(isValid) + + return providers.map((provider) => + MSTeamsNotificationHelper({ + ...(provider as IntegrationProviderMSTeams), + userId, + email: user.email }) ) } diff --git a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts index 7bbe29a2965..f523bcbbd21 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts @@ -8,12 +8,13 @@ import {IntegrationProviderMattermost} from '../../../../postgres/queries/getInt import {SlackNotification, Team} from '../../../../postgres/types' import IUser from '../../../../postgres/types/IUser' import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' -import {NotificationSettings} from '../../../../postgres/types/pg' import MattermostServerManager from '../../../../utils/MattermostServerManager' import {analytics} from '../../../../utils/analytics/analytics' import {toEpochSeconds} from '../../../../utils/epochTime' import sendToSentry from '../../../../utils/sendToSentry' import {DataLoaderWorker} from '../../../graphql' +import isValid from '../../../isValid' +import {SlackNotificationEventEnum} from '../../../public/resolverTypes' import {NotificationIntegrationHelper} from './NotificationIntegrationHelper' import {createNotifier} from './Notifier' import getSummaryText from './getSummaryText' @@ -359,7 +360,7 @@ async function getMattermost( dataLoader: DataLoaderWorker, teamId: string, userId: string, - event: NotificationSettings['event'] + event: SlackNotificationEventEnum ) { if (MATTERMOST_SECRET && MATTERMOST_URL) { return [ @@ -374,17 +375,28 @@ async function getMattermost( } const auths = await dataLoader - .get('teamMemberIntegrationAuthsByTeamIdAndEvent') - .load({service: 'mattermost', teamId, event}) + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({service: 'mattermost', teamId}) - return Promise.all( - auths.map(async (auth) => { - const provider = (await dataLoader - .get('integrationProviders') - .loadNonNull(auth.providerId)) as IntegrationProviderMattermost - return MattermostNotificationHelper({...provider, teamId, userId}) - }) - ) + const providers = ( + await Promise.all( + auths.map(async (auth) => { + const {providerId} = auth + const [provider, events] = await Promise.all([ + dataLoader + .get('integrationProviders') + .loadNonNull(providerId) as Promise, + dataLoader.get('notificationSettingsByProviderIdAndTeamId').load({providerId, teamId}) + ]) + if (events.includes(event)) { + return provider + } + return null + }) + ) + ).filter(isValid) + + return providers.map((provider) => MattermostNotificationHelper({...provider, teamId, userId})) } export const MattermostNotifier = createNotifier(getMattermost) diff --git a/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts b/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts index bf5fa254250..b4578c2a85e 100644 --- a/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts +++ b/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts @@ -149,15 +149,18 @@ const addTeamMemberIntegrationAuth: MutationResolvers['addTeamMemberIntegrationA }) } - await pg - .insertInto('NotificationSettings') - .columns(['authId', 'event']) - .values(() => ({ - authId, - event: sql`unnest(enum_range(NULL::"SlackNotificationEventEnum"))` - })) - .onConflict((oc) => oc.doNothing()) - .execute() + if (service === 'msTeams' || service === 'mattermost') { + await pg + .insertInto('TeamNotificationSettings') + .columns(['providerId', 'teamId', 'events']) + .values(() => ({ + providerId: providerDbId, + teamId, + events: sql`enum_range(NULL::"SlackNotificationEventEnum")` + })) + .onConflict((oc) => oc.doNothing()) + .execute() + } updateRepoIntegrationsCacheByPerms(dataLoader, viewerId, teamId, true) diff --git a/packages/server/graphql/public/mutations/setNotificationSetting.ts b/packages/server/graphql/public/mutations/setNotificationSetting.ts index cd79ed2a4b4..8a99a2d41f2 100644 --- a/packages/server/graphql/public/mutations/setNotificationSetting.ts +++ b/packages/server/graphql/public/mutations/setNotificationSetting.ts @@ -22,7 +22,7 @@ const setNotificationSetting: MutationResolvers['setNotificationSetting'] = asyn if (!auth) { return standardError(new Error('Integration auth not found'), {userId: viewerId}) } - const {teamId, service} = auth + const {providerId, teamId, service} = auth if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) } @@ -35,18 +35,24 @@ const setNotificationSetting: MutationResolvers['setNotificationSetting'] = asyn // RESOLUTION if (isEnabled) { await pg - .insertInto('NotificationSettings') - .values({authId, event}) - .onConflict((oc) => oc.doNothing()) + .updateTable('TeamNotificationSettings') + .set(({fn, val}) => ({ + events: fn('arr_append_uniq', ['events', val(event)]) + })) + .where('providerId', '=', providerId) + .where('teamId', '=', teamId) .execute() } else { await pg - .deleteFrom('NotificationSettings') - .where('authId', '=', authId) - .where('event', '=', event) + .updateTable('TeamNotificationSettings') + .set(({fn, val}) => ({ + events: fn('array_remove', ['events', val(event)]) + })) + .where('providerId', '=', providerId) + .where('teamId', '=', teamId) .execute() } - const data = {authId} + const data = {authId, providerId, teamId} publish(SubscriptionChannel.TEAM, teamId, 'SetNotificationSettingSuccess', data, subOptions) return data } diff --git a/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts b/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts index c799bac1cc0..f45d53ccb2f 100644 --- a/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts +++ b/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts @@ -2,14 +2,16 @@ import {SetNotificationSettingSuccessResolvers} from '../resolverTypes' export type SetNotificationSettingSuccessSource = { authId: number + providerId: number + teamId: string } const SetNotificationSettingSuccess: SetNotificationSettingSuccessResolvers = { auth: async ({authId}, _args, {dataLoader}) => { return dataLoader.get('teamMemberIntegrationAuths').loadNonNull(authId) }, - events: async ({authId}, _args, {dataLoader}) => { - return dataLoader.get('notificationSettingsByAuthId').load(authId) + events: async ({providerId, teamId}, _args, {dataLoader}) => { + return dataLoader.get('notificationSettingsByProviderIdAndTeamId').load({providerId, teamId}) } } diff --git a/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts b/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts index fe7b1e72ab5..d740ad4409b 100644 --- a/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts +++ b/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts @@ -9,8 +9,8 @@ const TeamMemberIntegrationAuthWebhook: TeamMemberIntegrationAuthWebhookResolver provider: async ({providerId}, _args, {dataLoader}) => { return dataLoader.get('integrationProviders').loadNonNull(providerId) }, - events: async ({id}, _args, {dataLoader}) => { - return dataLoader.get('notificationSettingsByAuthId').load(id) + events: async ({teamId, providerId}, _args, {dataLoader}) => { + return dataLoader.get('notificationSettingsByProviderIdAndTeamId').load({providerId, teamId}) } } diff --git a/packages/server/postgres/migrations/2025-02-18T17:28:11.536Z_notificationSettingsPerTeamAndChannel.ts b/packages/server/postgres/migrations/2025-02-18T17:28:11.536Z_notificationSettingsPerTeamAndChannel.ts new file mode 100644 index 00000000000..92d9b742cc4 --- /dev/null +++ b/packages/server/postgres/migrations/2025-02-18T17:28:11.536Z_notificationSettingsPerTeamAndChannel.ts @@ -0,0 +1,80 @@ +import {sql, type Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + // Schema changes significantly, easier to create a new table + await db.schema + .createTable('TeamNotificationSettings') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('providerId', 'integer', (col) => + col.references('IntegrationProvider.id').onDelete('cascade').notNull() + ) + .addColumn('teamId', 'varchar(100)', (col) => + col.references('Team.id').onDelete('cascade').notNull() + ) + .addColumn('channelId', 'varchar(255)') + .addColumn('events', sql`"SlackNotificationEventEnum"[]`, (col) => + col.defaultTo(sql`enum_range(NULL::"SlackNotificationEventEnum")`).notNull() + ) + .addUniqueConstraint( + 'TeamNotificationSettings_providerId_teamId_channelId_key', + ['providerId', 'teamId', 'channelId'], + (uc) => uc.nullsNotDistinct() + ) + .execute() + + await db + .insertInto('TeamNotificationSettings') + .columns(['providerId', 'teamId', 'events']) + .expression((eb) => + eb + .selectFrom('NotificationSettings') + .innerJoin('TeamMemberIntegrationAuth as auth', 'authId', 'auth.id') + .select(['providerId', 'auth.teamId', sql`array_agg(event)`]) + // There was a bug which might have added settings for other providers like gcal + .where((eb) => eb.or([eb('service', '=', 'mattermost'), eb('service', '=', 'msTeams')])) + .groupBy(['providerId', 'auth.teamId']) + ) + .execute() + + /* dropping the old table will be done in a later change + await db.schema + .dropTable('NotificationSettings') + .execute() + */ +} + +export async function down(db: Kysely): Promise { + /* + await db.schema + .createTable('NotificationSettings') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('authId', 'integer', (col) => + col.references('TeamMemberIntegrationAuth.id').onDelete('cascade').notNull() + ) + .addColumn('event', sql`"SlackNotificationEventEnum"`, (col) => col.notNull()) + .addUniqueConstraint('NotificationSettings_authId_event_key', ['authId', 'event']) + .execute() + + await db.schema + .createIndex('NotificationSettings_authId_idx') + .on('NotificationSettings') + .column('authId') + .execute() + + await db + .insertInto('NotificationSettings') + .columns(['authId', 'event']) + .expression((eb) => + eb + .selectFrom('TeamMemberIntegrationAuth as auth') + .innerJoin('TeamNotificationSettings as settings', (join) => join + .onRef('auth.teamId', '=', 'settings.teamId') + .onRef('auth.providerId', '=', 'settings.providerId') + ) + .select(['auth.id', sql`unnest(events)`]) + ) + .execute() + */ + + await db.schema.dropTable('TeamNotificationSettings').execute() +} diff --git a/yarn.lock b/yarn.lock index 4bf311ce1c5..bee56872f7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16374,17 +16374,18 @@ koalas@^1.0.2: resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" integrity sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA== -kysely-codegen@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/kysely-codegen/-/kysely-codegen-0.15.0.tgz#771c0256c24897ea64d5713dc10e40e8a359b96b" - integrity sha512-LPta2nQOyoEPDQ3w/Gsplc+2iyZPAsGvtWoS21VzOB0NDQ0B38Xy1gS8WlbGef542Zdw2eLJHxekud9DzVdNRw== +kysely-codegen@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/kysely-codegen/-/kysely-codegen-0.17.0.tgz#07bb2182ce2f315953c2407a52c99ee1ee942f91" + integrity sha512-C36g6epial8cIOSBEWGI9sRfkKSsEzTcivhjPivtYFQnhMdXnrVFaUe7UMZHeSdXaHiWDqDOkReJgWLD8nPKdg== dependencies: chalk "4.1.2" dotenv "^16.4.5" dotenv-expand "^11.0.6" git-diff "^2.0.6" - micromatch "^4.0.5" + micromatch "^4.0.8" minimist "^1.2.8" + pluralize "^8.0.0" kysely-ctl@^0.9.0: version "0.9.0" @@ -19356,6 +19357,11 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + pm2-axon-rpc@~0.7.0, pm2-axon-rpc@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz#2daec5383a63135b3f18babb70266dacdcbc429a" @@ -22153,7 +22159,7 @@ string-similarity@^3.0.0: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-3.0.0.tgz#07b0bc69fae200ad88ceef4983878d03793847c7" integrity sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22171,6 +22177,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -22257,7 +22272,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22271,6 +22286,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -24285,7 +24307,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -24303,6 +24325,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"