diff --git a/cron/monthly/user-report.js b/cron/monthly/user-report.js index 7dc96435313..1e0d4eec5d1 100755 --- a/cron/monthly/user-report.js +++ b/cron/monthly/user-report.js @@ -32,7 +32,7 @@ const debug = debugLib('monthlyreport'); * @param {*} backerCollective */ const fetchUserSubscribers = async (notificationType, backerCollective) => { - const unsubscriptions = await models.Notification.findAll({ + const unsubscriptions = await models.ActivitySubscription.findAll({ attributes: ['UserId'], where: { CollectiveId: backerCollective.id, diff --git a/cron/yearly/user-report.js b/cron/yearly/user-report.js index 4af9dc78b51..db3228b852f 100644 --- a/cron/yearly/user-report.js +++ b/cron/yearly/user-report.js @@ -207,7 +207,7 @@ const getCollectives = () => { }; const getUsers = collective => { - return models.Notification.findAll({ + return models.ActivitySubscription.findAll({ where: { channel: 'email', CollectiveId: collective.id, diff --git a/migrations/20260331120000-rename-notifications-to-activity-subscriptions.ts b/migrations/20260331120000-rename-notifications-to-activity-subscriptions.ts new file mode 100644 index 00000000000..6eedbfb0382 --- /dev/null +++ b/migrations/20260331120000-rename-notifications-to-activity-subscriptions.ts @@ -0,0 +1,17 @@ +'use strict'; + +/** + * Renames the legacy `Notifications` table to `ActivitySubscriptions` to match + * the ActivitySubscription Sequelize model (`server/models/ActivitySubscription.ts`). + * + * @type {import('sequelize-cli').Migration} + */ +module.exports = { + async up(queryInterface) { + await queryInterface.renameTable('Notifications', 'ActivitySubscriptions'); + }, + + async down(queryInterface) { + await queryInterface.renameTable('ActivitySubscriptions', 'Notifications'); + }, +}; diff --git a/scripts/sanitize-db.ts b/scripts/sanitize-db.ts index d06fa0c654c..c0961f3a563 100644 --- a/scripts/sanitize-db.ts +++ b/scripts/sanitize-db.ts @@ -284,7 +284,7 @@ const removeConnectedAccountsTokens = () => { // Remove all webhooks to ensure we won't ping user apps const deleteWebhooks = () => { - return models.Notification.destroy({ + return models.ActivitySubscription.destroy({ where: { channel: [channels.WEBHOOK, channels.SLACK] }, }).catch(e => console.error('There was an error removing the webhooks. Please do it manually', e)); }; diff --git a/server/controllers/services/email.js b/server/controllers/services/email.js index 3eaa66b2f3c..812f188cc3e 100644 --- a/server/controllers/services/email.js +++ b/server/controllers/services/email.js @@ -28,7 +28,7 @@ export const unsubscribe = async (req, res, next) => { type = 'order.processed'; } - await models.Notification.unsubscribe(type, 'email', user.id, collective?.id); + await models.ActivitySubscription.unsubscribe(type, 'email', user.id, collective?.id); res.send({ response: 'ok' }); } catch (e) { next(e); diff --git a/server/graphql/v1/CollectiveInterface.js b/server/graphql/v1/CollectiveInterface.js index 04b126f2f3f..6c30a3b6bc4 100644 --- a/server/graphql/v1/CollectiveInterface.js +++ b/server/graphql/v1/CollectiveInterface.js @@ -497,6 +497,7 @@ export const CollectiveInterfaceType = new GraphQLInterfaceType({ notifications: { type: new GraphQLList(NotificationType), description: 'List of all notifications for this collective', + deprecationReason: '2026-03-31: Please move to GraphQL v2', args: { limit: { type: GraphQLInt }, offset: { type: GraphQLInt }, @@ -1292,6 +1293,7 @@ const CollectiveFields = () => { }, notifications: { type: new GraphQLList(NotificationType), + deprecationReason: '2026-03-31: Please move to GraphQL v2', args: { limit: { type: GraphQLInt }, offset: { type: GraphQLInt }, @@ -1318,7 +1320,7 @@ const CollectiveFields = () => { where.active = args.active; } - return models.Notification.findAll({ + return models.ActivitySubscription.findAll({ where: where, limit: args.limit, offset: args.offset, diff --git a/server/graphql/v1/mutations.js b/server/graphql/v1/mutations.js index dd3bf8a31e4..214f86afb2d 100644 --- a/server/graphql/v1/mutations.js +++ b/server/graphql/v1/mutations.js @@ -339,6 +339,7 @@ const mutations = { editWebhooks: { type: new GraphQLList(NotificationType), description: 'Edits (by replacing) the admin-level webhooks for a collective.', + deprecationReason: '2026-03-31: Please move to GraphQL v2', args: { collectiveId: { type: new GraphQLNonNull(GraphQLInt), diff --git a/server/graphql/v1/mutations/notifications.js b/server/graphql/v1/mutations/notifications.js index 88b18bc7d73..2a259dd4b64 100644 --- a/server/graphql/v1/mutations/notifications.js +++ b/server/graphql/v1/mutations/notifications.js @@ -32,7 +32,7 @@ export async function editWebhooks(args, req) { } const getAllWebhooks = async () => { - return await models.Notification.findAll({ + return await models.ActivitySubscription.findAll({ where: { CollectiveId: args.collectiveId, channel: channels.WEBHOOK }, order: [['createdAt', 'ASC']], }); @@ -46,7 +46,7 @@ export async function editWebhooks(args, req) { // Delete old if (toRemove.length > 0) { promises.push( - models.Notification.destroy({ + models.ActivitySubscription.destroy({ where: { id: { [Op.in]: toRemove.map(n => n.id) } }, }), ); @@ -57,7 +57,7 @@ export async function editWebhooks(args, req) { promises.push( Promise.all( toCreate.map(notification => - models.Notification.create({ + models.ActivitySubscription.create({ ...pick(notification, allowedFields), CollectiveId: args.collectiveId, UserId: req.remoteUser.id, @@ -72,7 +72,7 @@ export async function editWebhooks(args, req) { if (toUpdate.length > 0) { promises.push( ...toUpdate.map(notification => { - return models.Notification.update(pick(notification, allowedFields), { + return models.ActivitySubscription.update(pick(notification, allowedFields), { where: { id: notification.id, CollectiveId: args.collectiveId }, }); }), diff --git a/server/graphql/v1/types.js b/server/graphql/v1/types.js index 98386273a3f..d7f797491b3 100644 --- a/server/graphql/v1/types.js +++ b/server/graphql/v1/types.js @@ -887,6 +887,7 @@ export const ExpenseType = new GraphQLObjectType({ export const NotificationType = new GraphQLObjectType({ name: 'NotificationType', description: 'This represents a Notification', + deprecationReason: '2026-03-31: Please move to GraphQL v2', fields: () => { return { id: { diff --git a/server/graphql/v2/collection/WebhookCollection.js b/server/graphql/v2/collection/WebhookCollection.js index c0c115a93fe..1bdc7fc093f 100644 --- a/server/graphql/v2/collection/WebhookCollection.js +++ b/server/graphql/v2/collection/WebhookCollection.js @@ -41,7 +41,7 @@ export const WebhookCollectionResolver = async (args, req) => { const { offset, limit } = args; - const result = await models.Notification.findAndCountAll({ + const result = await models.ActivitySubscription.findAndCountAll({ where, limit, offset, diff --git a/server/graphql/v2/identifiers.ts b/server/graphql/v2/identifiers.ts index 69b2bdbf32c..8a43e1fddaa 100644 --- a/server/graphql/v2/identifiers.ts +++ b/server/graphql/v2/identifiers.ts @@ -20,6 +20,7 @@ export const IDENTIFIER_TYPES = { ACCOUNT: 'account', ACCOUNTING_CATEGORY: 'accounting-category', ACTIVITY: 'activity', + ACTIVITY_SUBSCRIPTION: 'activity-subscription', AGREEMENT: 'agreement', COMMENT: 'comment', COMMENT_REACTION: 'comment-reaction', @@ -42,7 +43,6 @@ export const IDENTIFIER_TYPES = { UPDATE: 'update', APPLICATION: 'application', USER_TOKEN: 'user-token', - NOTIFICATION: 'notification', PERSONAL_TOKEN: 'personal-token', UPLOADED_FILE: 'uploaded-file', USER: 'user', diff --git a/server/graphql/v2/input/WebhookReferenceInput.js b/server/graphql/v2/input/WebhookReferenceInput.js index 48773f1657a..f8839927881 100644 --- a/server/graphql/v2/input/WebhookReferenceInput.js +++ b/server/graphql/v2/input/WebhookReferenceInput.js @@ -8,7 +8,7 @@ import { idDecode, IDENTIFIER_TYPES } from '../identifiers'; export const WebhookReferenceFields = { id: { type: GraphQLString, - description: `The public id identifying the webhook (ie: dgm9bnk8-0437xqry-ejpvzeol-jdayw5re, ${EntityShortIdPrefix.Notification}_xxxxxxxx)`, + description: `The public id identifying the webhook (ie: dgm9bnk8-0437xqry-ejpvzeol-jdayw5re, ${EntityShortIdPrefix.ActivitySubscription}_xxxxxxxx)`, }, legacyId: { type: GraphQLInt, @@ -29,13 +29,13 @@ export const GraphQLWebhookReferenceInput = new GraphQLInputObjectType({ */ export const fetchWebhookWithReference = async input => { let notification; - if (isEntityPublicId(input.id, EntityShortIdPrefix.Notification)) { - notification = await models.Notification.findOne({ where: { publicId: input.id } }); + if (isEntityPublicId(input.id, EntityShortIdPrefix.ActivitySubscription)) { + notification = await models.ActivitySubscription.findOne({ where: { publicId: input.id } }); } else if (input.id) { - const id = idDecode(input.id, IDENTIFIER_TYPES.NOTIFICATION); - notification = await models.Notification.findByPk(id); + const id = idDecode(input.id, IDENTIFIER_TYPES.ACTIVITY_SUBSCRIPTION); + notification = await models.ActivitySubscription.findByPk(id); } else if (input.legacyId) { - notification = await models.Notification.findByPk(input.legacyId); + notification = await models.ActivitySubscription.findByPk(input.legacyId); } else { throw new Error('Please provide an id'); } diff --git a/server/graphql/v2/interface/Account.ts b/server/graphql/v2/interface/Account.ts index feb601aba00..ddedb007e5e 100644 --- a/server/graphql/v2/interface/Account.ts +++ b/server/graphql/v2/interface/Account.ts @@ -890,7 +890,7 @@ const accountFieldsDefinition = () => ({ where['channel'] = args.channel; } - return models.Notification.findAll({ where }); + return models.ActivitySubscription.findAll({ where }); }, }, permissions: { diff --git a/server/graphql/v2/mutation/ActivitySubscriptionsMutations.ts b/server/graphql/v2/mutation/ActivitySubscriptionsMutations.ts index 4862d8c0b24..c72f203172b 100644 --- a/server/graphql/v2/mutation/ActivitySubscriptionsMutations.ts +++ b/server/graphql/v2/mutation/ActivitySubscriptionsMutations.ts @@ -27,9 +27,19 @@ const notificationMutations = { args.account && (await fetchAccountWithReference(args.account, { loaders: req.loaders, throwIfMissing: true })); if (args.active) { - return models.Notification.subscribe(args.type, Channels.EMAIL, req.remoteUser.id, collective?.id || null); + return models.ActivitySubscription.subscribe( + args.type, + Channels.EMAIL, + req.remoteUser.id, + collective?.id || null, + ); } else { - return models.Notification.unsubscribe(args.type, Channels.EMAIL, req.remoteUser.id, collective?.id || null); + return models.ActivitySubscription.unsubscribe( + args.type, + Channels.EMAIL, + req.remoteUser.id, + collective?.id || null, + ); } }, }, diff --git a/server/graphql/v2/mutation/WebhookMutations.js b/server/graphql/v2/mutation/WebhookMutations.js index 18800d3f5e8..abc34af4816 100644 --- a/server/graphql/v2/mutation/WebhookMutations.js +++ b/server/graphql/v2/mutation/WebhookMutations.js @@ -41,7 +41,7 @@ const createWebhook = { CollectiveId: account.id, }; - return models.Notification.create(createParams); + return models.ActivitySubscription.create(createParams); }, }; diff --git a/server/graphql/v2/object/ActivitySubscription.ts b/server/graphql/v2/object/ActivitySubscription.ts index dcfd280f590..f5f5bdb9f6f 100644 --- a/server/graphql/v2/object/ActivitySubscription.ts +++ b/server/graphql/v2/object/ActivitySubscription.ts @@ -17,7 +17,7 @@ export const GraphQLActivitySubscription = new GraphQLObjectType({ type: new GraphQLNonNull(GraphQLString), description: 'Unique identifier for this notification setting', resolve(notification) { - if (isEntityMigratedToPublicId(EntityShortIdPrefix.Notification, notification.createdAt)) { + if (isEntityMigratedToPublicId(EntityShortIdPrefix.ActivitySubscription, notification.createdAt)) { return notification.publicId; } else { return idEncode(notification.id, IDENTIFIER_TYPES.NOTIFICATION); @@ -26,7 +26,7 @@ export const GraphQLActivitySubscription = new GraphQLObjectType({ }, publicId: { type: new GraphQLNonNull(GraphQLString), - description: `The resource public id (ie: ${EntityShortIdPrefix.Notification}_xxxxxxxx)`, + description: `The resource public id (ie: ${EntityShortIdPrefix.ActivitySubscription}_xxxxxxxx)`, }, channel: { type: new GraphQLNonNull(GraphQLActivityChannel), diff --git a/server/graphql/v2/object/Webhook.js b/server/graphql/v2/object/Webhook.js index 78a17a42a7a..f4e638d0768 100644 --- a/server/graphql/v2/object/Webhook.js +++ b/server/graphql/v2/object/Webhook.js @@ -12,42 +12,42 @@ export const GraphQLWebhook = new GraphQLObjectType({ fields: () => ({ id: { type: new GraphQLNonNull(GraphQLString), - resolve(notification) { - if (isEntityMigratedToPublicId(EntityShortIdPrefix.Notification, notification.createdAt)) { - return notification.publicId; + resolve(activitySubscription) { + if (isEntityMigratedToPublicId(EntityShortIdPrefix.ActivitySubscription, activitySubscription.createdAt)) { + return activitySubscription.publicId; } else { - return idEncode(notification.id, 'notification'); + return idEncode(activitySubscription.id, 'activitySubscription'); } }, }, publicId: { type: new GraphQLNonNull(GraphQLString), - description: `The resource public id (ie: ${EntityShortIdPrefix.Notification}_xxxxxxxx)`, + description: `The resource public id (ie: ${EntityShortIdPrefix.ActivitySubscription}_xxxxxxxx)`, }, legacyId: { type: new GraphQLNonNull(GraphQLInt), deprecationReason: '2026-02-25: use publicId', - resolve(notification) { - return notification.id; + resolve(activitySubscription) { + return activitySubscription.id; }, }, activityType: { type: GraphQLActivityType, - resolve(notification) { - return notification.type; + resolve(activitySubscription) { + return activitySubscription.type; }, }, webhookUrl: { type: URL, - resolve(notification) { - return notification.webhookUrl; + resolve(activitySubscription) { + return activitySubscription.webhookUrl; }, }, account: { type: new GraphQLNonNull(GraphQLAccount), - resolve(notification, args, req) { - if (notification.CollectiveId) { - return req.loaders.Collective.byId.load(notification.CollectiveId); + resolve(activitySubscription, args, req) { + if (activitySubscription.CollectiveId) { + return req.loaders.Collective.byId.load(activitySubscription.CollectiveId); } }, }, diff --git a/server/lib/email.ts b/server/lib/email.ts index 4db161438b5..f746f361c78 100644 --- a/server/lib/email.ts +++ b/server/lib/email.ts @@ -335,7 +335,7 @@ const generateEmailFromTemplate = ( const isNotificationActive = async (template: string, data: SendMessageData) => { if (data.user && data.user.id) { - return models.Notification.isActive(template, data.user, data.collective); + return models.ActivitySubscription.isActive(template, data.user, data.collective); } else { return true; } diff --git a/server/lib/import-export/import.ts b/server/lib/import-export/import.ts index abda93cf3bb..1e6af8630b3 100644 --- a/server/lib/import-export/import.ts +++ b/server/lib/import-export/import.ts @@ -86,6 +86,7 @@ const getNextPK = async model => { const modelsDeduplicationSchema: Record = { AccountingCategory: {}, Activity: {}, + ActivitySubscription: {}, Agreement: {}, Application: {}, Collective: { unique: ['slug'] }, @@ -106,7 +107,6 @@ const modelsDeduplicationSchema: Record = { Member: {}, MemberInvitation: {}, MigrationLog: {}, - Notification: {}, OAuthAuthorizationCode: {}, Order: { unique: ['totalAmount', 'currency', 'createdAt', 'quantity', 'status', 'interval', 'description'] }, PaymentMethod: { unique: ['uuid'] }, diff --git a/server/lib/import-export/sanitize.ts b/server/lib/import-export/sanitize.ts index 964311400f2..c8a69b2ae20 100644 --- a/server/lib/import-export/sanitize.ts +++ b/server/lib/import-export/sanitize.ts @@ -113,6 +113,11 @@ const PROD_SANITIZERS: { [k in ModelNames]: Sanitizer } = { Activity: async (activity, req) => ({ data: await sanitizeActivityData(req, activity), }), + ActivitySubscription: async notification => { + if (notification.channel !== Channels.EMAIL) { + return null; + } + }, Agreement: (agreement, req) => { if (!Agreement.canSeeAgreementsForHostCollectiveId(req.remoteUser, agreement.HostCollectiveId)) { return null; @@ -199,11 +204,6 @@ const PROD_SANITIZERS: { [k in ModelNames]: Sanitizer } = { } } }, - Notification: async notification => { - if (notification.channel !== Channels.EMAIL) { - return null; - } - }, Order: async (order, req) => { req.loaders.Order.byId.prime(order.id, order); // Store the order in the cache for later row resolvers const publicDataFields = [ diff --git a/server/lib/merge-accounts.ts b/server/lib/merge-accounts.ts index ead191be8f8..096850fa179 100644 --- a/server/lib/merge-accounts.ts +++ b/server/lib/merge-accounts.ts @@ -179,6 +179,7 @@ const collectiveFieldsConfig: CollectiveFieldsConfig = { accountingCategories: { model: models.AccountingCategory, field: 'CollectiveId' }, agreements: { model: models.Agreement, field: 'CollectiveId' }, activities: { model: models.Activity, field: 'CollectiveId' }, + activitySubscriptions: { model: models.ActivitySubscription, field: 'CollectiveId' }, applications: { model: models.Application, field: 'CollectiveId' }, childrenCollectives: { model: Collective, field: 'ParentCollectiveId' }, comments: { model: models.Comment, field: 'CollectiveId' }, @@ -204,7 +205,6 @@ const collectiveFieldsConfig: CollectiveFieldsConfig = { members: { model: models.Member, field: 'MemberCollectiveId', getIdsToIgnore: getMemberIdsToIgnore }, membershipInvitations: { model: models.MemberInvitation, field: 'CollectiveId' }, memberships: { model: models.Member, field: 'CollectiveId', getIdsToIgnore: getMembershipIdsToIgnore }, - notifications: { model: models.Notification, field: 'CollectiveId' }, ordersCreated: { model: models.Order, field: 'FromCollectiveId' }, ordersReceived: { model: models.Order, field: 'CollectiveId' }, paymentMethods: { model: models.PaymentMethod, field: 'CollectiveId' }, @@ -232,6 +232,7 @@ type UserFieldsConfig = Record; const userFieldsConfig = { agreements: { model: models.Agreement, field: 'UserId' }, activities: { model: models.Activity, field: 'UserId' }, + activitySubscriptions: { model: models.ActivitySubscription, field: 'UserId' }, applications: { model: models.Application, field: 'CreatedByUserId' }, collectives: { model: models.Collective, field: 'CreatedByUserId' }, comments: { model: models.Comment, field: 'CreatedByUserId' }, @@ -244,7 +245,6 @@ const userFieldsConfig = { memberInvitations: { model: models.MemberInvitation, field: 'CreatedByUserId' }, members: { model: models.Member, field: 'CreatedByUserId' }, migrationLogs: { model: models.MigrationLog, field: 'CreatedByUserId' }, - notifications: { model: models.Notification, field: 'UserId' }, oAuthAuthorizationCodes: { model: models.OAuthAuthorizationCode, field: 'UserId' }, orders: { model: models.Order, field: 'CreatedByUserId' }, paymentMethods: { model: models.PaymentMethod, field: 'CreatedByUserId' }, diff --git a/server/lib/notifications/email.ts b/server/lib/notifications/email.ts index b9c1937a768..fa0573bcc43 100644 --- a/server/lib/notifications/email.ts +++ b/server/lib/notifications/email.ts @@ -68,7 +68,7 @@ export const notify = { } if (!options?.skipUnsubscribedCheck) { - const unsubscribed = await models.Notification.getUnsubscribers({ + const unsubscribed = await models.ActivitySubscription.getUnsubscribers({ type: activity.type, UserId: user.id, CollectiveId: options?.collective?.id || activity.CollectiveId, @@ -110,7 +110,7 @@ export const notify = { return; } - const unsubscribed = await models.Notification.getUnsubscribers({ + const unsubscribed = await models.ActivitySubscription.getUnsubscribers({ type: activity.type, CollectiveId: options?.collective?.id || activity.CollectiveId, channel: Channels.EMAIL, diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 12eef9f08aa..cce7d7e2812 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -3,7 +3,7 @@ import config from 'config'; import { activities, channels } from '../../constants'; import ActivityTypes from '../../constants/activities'; -import { Activity, Notification } from '../../models'; +import { Activity, ActivitySubscription } from '../../models'; import logger from '../logger'; import { reportErrorToSentry } from '../sentry'; import slackLib from '../slack'; @@ -24,7 +24,7 @@ const shouldSkipActivity = (activity: Activity) => { return false; }; -const publishToWebhook = async (notification: Notification, activity: Activity): Promise => { +const publishToWebhook = async (notification: ActivitySubscription, activity: Activity): Promise => { if (slackLib.isSlackWebhookUrl(notification.webhookUrl)) { await slackLib.postActivityOnPublicChannel(activity, notification.webhookUrl); return true; @@ -76,7 +76,7 @@ const dispatch = async ( active: true, }; - const notificationChannels = await Notification.findAll({ where }); + const notificationChannels = await ActivitySubscription.findAll({ where }); return Promise.all( notificationChannels.map(async notifConfig => { if (!shouldNotifyChannel(notifConfig.channel)) { diff --git a/server/lib/onboarding.ts b/server/lib/onboarding.ts index 0439fa9fed2..89def144bb3 100644 --- a/server/lib/onboarding.ts +++ b/server/lib/onboarding.ts @@ -25,7 +25,7 @@ async function processCollective(collective: Collective, template: string) { return; } const users = await collective.getAdminUsers(); - const unsubscribers = await models.Notification.getUnsubscribersUserIds('onboarding', collective.id); + const unsubscribers = await models.ActivitySubscription.getUnsubscribersUserIds('onboarding', collective.id); const recipients = users.filter(u => u && unsubscribers.indexOf(u.id) === -1).map(u => u.email); if (!recipients || recipients.length === 0) { return; diff --git a/server/lib/permalink/entity-map.ts b/server/lib/permalink/entity-map.ts index c112de988d0..f730520dbb6 100644 --- a/server/lib/permalink/entity-map.ts +++ b/server/lib/permalink/entity-map.ts @@ -3,6 +3,7 @@ import moment from 'moment'; export enum EntityShortIdPrefix { AccountingCategory = 'acat', Activity = 'act', + ActivitySubscription = 'asub', Agreement = 'agr', Application = 'app', Comment = 'cmt', @@ -19,7 +20,6 @@ export enum EntityShortIdPrefix { ManualPaymentProvider = 'mpp', Member = 'mem', MemberInvitation = 'mi', - Notification = 'not', OAuthAuthorizationCode = 'oac', Order = 'or', PayoutMethod = 'po', diff --git a/server/lib/permalink/handler.ts b/server/lib/permalink/handler.ts index b99cfbbdf09..cf4f9448ef1 100644 --- a/server/lib/permalink/handler.ts +++ b/server/lib/permalink/handler.ts @@ -50,7 +50,7 @@ const handlerMap: Record = { [EntityShortIdPrefix.ManualPaymentProvider]: handleNotFound, [EntityShortIdPrefix.Member]: handleMember, [EntityShortIdPrefix.MemberInvitation]: handleMemberInvitation, - [EntityShortIdPrefix.Notification]: handleNotFound, + [EntityShortIdPrefix.ActivitySubscription]: handleNotFound, [EntityShortIdPrefix.OAuthAuthorizationCode]: handleNotFound, [EntityShortIdPrefix.Order]: handleOrder, [EntityShortIdPrefix.PayoutMethod]: handlePayoutMethod, diff --git a/server/lib/queries.js b/server/lib/queries.js index 2a0c4eb0697..0da3b57a6fe 100644 --- a/server/lib/queries.js +++ b/server/lib/queries.js @@ -317,7 +317,7 @@ export const usersToNotifyForUpdateSQLQuery = ` ON u."CollectiveId" = admins_of_members.id LEFT JOIN member_collectives ON (member_collectives."type" = 'USER' AND u."CollectiveId" = member_collectives.id) - LEFT JOIN "Notifications" n + LEFT JOIN "ActivitySubscriptions" n ON n.channel = 'email' AND n.type = 'collective.update.published' AND n."UserId" = u.id AND n."CollectiveId" = :collectiveId WHERE (admins_of_members.id IS NOT NULL OR member_collectives.id IS NOT NULL) AND n.active IS NOT FALSE diff --git a/server/models/Notification.ts b/server/models/ActivitySubscription.ts similarity index 83% rename from server/models/Notification.ts rename to server/models/ActivitySubscription.ts index b6445413cbb..b954466a1a3 100644 --- a/server/models/Notification.ts +++ b/server/models/ActivitySubscription.ts @@ -16,7 +16,7 @@ import Member from './Member'; import { ModelWithPublicId } from './ModelWithPublicId'; import User from './User'; -const debug = debugLib('models:Notification'); +const debug = debugLib('models:ActivitySubscription'); const DEFAULT_ACTIVE_STATE_BY_CHANNEL = { [channels.EMAIL]: true, @@ -24,13 +24,13 @@ const DEFAULT_ACTIVE_STATE_BY_CHANNEL = { [channels.WEBHOOK]: false, }; -class Notification extends ModelWithPublicId< - EntityShortIdPrefix.Notification, - InferAttributes, - InferCreationAttributes +class ActivitySubscription extends ModelWithPublicId< + EntityShortIdPrefix.ActivitySubscription, + InferAttributes, + InferCreationAttributes > { - public static readonly nanoIdPrefix = EntityShortIdPrefix.Notification; - public static readonly tableName = 'Notifications' as const; + public static readonly nanoIdPrefix = EntityShortIdPrefix.ActivitySubscription; + public static readonly tableName = 'ActivitySubscriptions' as const; declare public readonly id: CreationOptional; declare public channel: channels; @@ -55,10 +55,10 @@ class Notification extends ModelWithPublicId< } static async createMany( - notifications: InferCreationAttributes[], - defaultValues?: InferCreationAttributes, - ): Promise { - return Promise.all(notifications.map(u => Notification.create(defaults({}, u, defaultValues)))); + notifications: InferCreationAttributes[], + defaultValues?: InferCreationAttributes, + ): Promise { + return Promise.all(notifications.map(u => ActivitySubscription.create(defaults({}, u, defaultValues)))); } static async unsubscribe( @@ -76,14 +76,14 @@ class Notification extends ModelWithPublicId< } return sequelize.transaction(async transaction => { - let notification = await Notification.findOne({ + let notification = await ActivitySubscription.findOne({ where: { UserId, CollectiveId, type, channel, webhookUrl }, transaction, }); if (DEFAULT_ACTIVE_STATE_BY_CHANNEL[channel] === true) { if (!notification) { - notification = await Notification.create( + notification = await ActivitySubscription.create( { UserId, CollectiveId, type, channel, active: false, webhookUrl }, { transaction }, ); @@ -91,9 +91,9 @@ class Notification extends ModelWithPublicId< await notification.update({ active: false }, { transaction }); } - // If user is unsubscribing from ActivityClass, remove existing Notifications for any included ActivityType + // If user is unsubscribing from ActivityClass, remove existing subscriptions for any included ActivityType if ((isClass || type === ActivityTypes.ACTIVITY_ALL) && UserId) { - await Notification.destroy({ + await ActivitySubscription.destroy({ where: { type: isClass ? { [Op.in]: ActivitiesPerClass[type] } : { [Op.ne]: ActivityTypes.ACTIVITY_ALL }, UserId, @@ -104,7 +104,7 @@ class Notification extends ModelWithPublicId< }); } } else { - await Notification.destroy({ + await ActivitySubscription.destroy({ where: { type, channel, UserId, CollectiveId, active: true, webhookUrl }, transaction, }); @@ -128,11 +128,14 @@ class Notification extends ModelWithPublicId< const isClass = Object.values(ActivityClasses).includes(type as ActivityClasses); return sequelize.transaction(async transaction => { if (DEFAULT_ACTIVE_STATE_BY_CHANNEL[channel] === true) { - await Notification.destroy({ where: { type, channel, UserId, CollectiveId, active: false }, transaction }); + await ActivitySubscription.destroy({ + where: { type, channel, UserId, CollectiveId, active: false }, + transaction, + }); // If subscribing from ActivityClass, remove existing unsubscription for its ActivityTypes if ((isClass || type === ActivityTypes.ACTIVITY_ALL) && UserId) { - await Notification.destroy({ + await ActivitySubscription.destroy({ where: { type: isClass ? { [Op.in]: ActivitiesPerClass[type] } : { [Op.ne]: ActivityTypes.ACTIVITY_ALL }, channel, @@ -144,12 +147,15 @@ class Notification extends ModelWithPublicId< }); } } else { - const notification = await Notification.findOne({ + const notification = await ActivitySubscription.findOne({ where: { UserId, CollectiveId, type, channel, webhookUrl }, transaction, }); if (!notification) { - await Notification.create({ UserId, CollectiveId, type, channel, active: true, webhookUrl }, { transaction }); + await ActivitySubscription.create( + { UserId, CollectiveId, type, channel, active: true, webhookUrl }, + { transaction }, + ); } else if (notification.active === false) { await notification.update({ active: true }, { transaction }); } @@ -160,7 +166,7 @@ class Notification extends ModelWithPublicId< /** * Get the list of subscribers to a mailing list * (e.g. backers@:collectiveSlug.opencollective.com, :eventSlug@:collectiveSlug.opencollective.com) - * We exclude users that have unsubscribed (by looking for rows in the Notifications table that are active: false) + * We exclude users that have unsubscribed (by looking for rows in ActivitySubscriptions that are active: false) */ static async getSubscribers(collectiveSlug: string | number, mailinglist: string): Promise { const findByAttribute = typeof collectiveSlug === 'string' ? 'findBySlug' : 'findById'; @@ -184,10 +190,12 @@ class Notification extends ModelWithPublicId< return []; } - return Notification.getUnsubscribersUserIds(`mailinglist.${mailinglist}`, collective.id).then(excludeIds => { - debug('excluding', excludeIds.length, 'members'); - return members.filter(m => excludeIds.indexOf(m.CreatedByUserId) === -1); - }); + return ActivitySubscription.getUnsubscribersUserIds(`mailinglist.${mailinglist}`, collective.id).then( + excludeIds => { + debug('excluding', excludeIds.length, 'members'); + return members.filter(m => excludeIds.indexOf(m.CreatedByUserId) === -1); + }, + ); }; const getMembersForMailingList = () => { @@ -206,7 +214,7 @@ class Notification extends ModelWithPublicId< static async getSubscribersUsers(collectiveSlug: string, mailinglist: string) { debug('getSubscribersUsers', collectiveSlug, mailinglist); - const memberships = await Notification.getSubscribers(collectiveSlug, mailinglist); + const memberships = await ActivitySubscription.getSubscribers(collectiveSlug, mailinglist); if (!memberships || memberships.length === 0) { return []; } @@ -219,7 +227,7 @@ class Notification extends ModelWithPublicId< static async getSubscribersCollectives(collectiveSlug: string, mailinglist: string) { debug('getSubscribersCollectives', collectiveSlug, mailinglist); - const memberships = await Notification.getSubscribers(collectiveSlug, mailinglist); + const memberships = await ActivitySubscription.getSubscribers(collectiveSlug, mailinglist); if (!memberships || memberships.length === 0) { return []; } @@ -236,7 +244,7 @@ class Notification extends ModelWithPublicId< */ static async getUnsubscribersUserIds(notificationType: string, CollectiveId?: number) { debug('getUnsubscribersUserIds', notificationType, CollectiveId); - const notifications = await Notification.findAll({ + const notifications = await ActivitySubscription.findAll({ attributes: ['UserId'], where: { CollectiveId, @@ -273,7 +281,7 @@ class Notification extends ModelWithPublicId< const collective = _where.CollectiveId && (await Collective.findByPk(_where.CollectiveId)); if (collective) { - // When looking for Notifications about specific Collective, we're also including the Collective parent and + // When looking for ActivitySubscriptions about specific Collective, we're also including the Collective parent and // it's host because: // 1. A user who unsubscribes from a Collective activity should not receive activities from its events or // projects either; @@ -281,8 +289,8 @@ class Notification extends ModelWithPublicId< // by unsubscribing straight to the Host; where['CollectiveId'] = compact([collective.id, collective.ParentCollectiveId, collective.HostCollectiveId]); - // If a User provided, we also want to include "global Notifications settings" in the search. A user can - // set their global setting by creating a Notification that has no specific Collective attached. + // If a User provided, we also want to include global activity subscription settings in the search. A user can + // set their global setting by creating a ActivitySubscription that has no specific Collective attached. if (where['UserId']) { where['CollectiveId'].push(null); } @@ -292,7 +300,7 @@ class Notification extends ModelWithPublicId< } debug('getUnsubscribers', where); - const unsubs = await Notification.findAll({ + const unsubs = await ActivitySubscription.findAll({ where, include, }).then(getUsers); @@ -301,7 +309,7 @@ class Notification extends ModelWithPublicId< // unsubscribed to this activity through a global setting or a host wide rule but explicitly created // a notification rule for this collective or parent collective. const subs = collective - ? await Notification.findAll({ + ? await ActivitySubscription.findAll({ where: { ...where, active: true, CollectiveId: [collective.id, collective.ParentCollectiveId] }, include, }).then(getUsers) @@ -324,7 +332,7 @@ class Notification extends ModelWithPublicId< where['CollectiveId'] = collective.id; } - return Notification.findOne({ where }).then(notification => { + return ActivitySubscription.findOne({ where }).then(notification => { if (notification) { return notification.active; } else { @@ -337,11 +345,11 @@ class Notification extends ModelWithPublicId< * Counts registered webhooks for a user, for a collective. */ static countRegisteredWebhooks(CollectiveId: number) { - return Notification.count({ where: { CollectiveId, channel: channels.WEBHOOK } }); + return ActivitySubscription.count({ where: { CollectiveId, channel: channels.WEBHOOK } }); } } -Notification.init( +ActivitySubscription.init( { id: { type: DataTypes.INTEGER, @@ -452,4 +460,4 @@ Notification.init( }, ); -export default Notification; +export default ActivitySubscription; diff --git a/server/models/index.ts b/server/models/index.ts index b56e388ccba..5d4fbd52a91 100644 --- a/server/models/index.ts +++ b/server/models/index.ts @@ -2,6 +2,7 @@ import sequelize, { Op } from '../lib/sequelize'; import AccountingCategory from './AccountingCategory'; import Activity from './Activity'; +import ActivitySubscription from './ActivitySubscription'; import Agreement from './Agreement'; import Application from './Application'; import Collective from './Collective'; @@ -23,7 +24,6 @@ import ManualPaymentProvider from './ManualPaymentProvider'; import Member from './Member'; import MemberInvitation from './MemberInvitation'; import MigrationLog from './MigrationLog'; -import Notification from './Notification'; import OAuthAuthorizationCode from './OAuthAuthorizationCode'; import Order from './Order'; import PaymentMethod from './PaymentMethod'; @@ -76,7 +76,7 @@ const models = { Member, MemberInvitation, MigrationLog, - Notification, + ActivitySubscription, OAuthAuthorizationCode, Order, PaymentMethod, @@ -156,7 +156,7 @@ Collective.hasMany(LegalDocument, { foreignKey: 'CollectiveId', as: 'legalDocume Collective.hasMany(Member, { foreignKey: 'CollectiveId', as: 'members' }); Collective.hasMany(Member, { foreignKey: 'CollectiveId', as: 'adminMembers', scope: { role: 'ADMIN' } }); Collective.hasMany(Member, { foreignKey: 'MemberCollectiveId', as: 'memberships' }); -Collective.hasMany(Notification); +Collective.hasMany(ActivitySubscription); Collective.hasMany(Order, { foreignKey: 'CollectiveId', as: 'orders' }); Collective.hasMany(PayoutMethod); Collective.hasMany(RequiredLegalDocument, { foreignKey: 'HostCollectiveId' }); @@ -249,9 +249,9 @@ MemberInvitation.belongsTo(Collective, { foreignKey: 'MemberCollectiveId', as: ' MemberInvitation.belongsTo(Tier); MemberInvitation.belongsTo(User, { foreignKey: 'CreatedByUserId', as: 'createdByUser' }); -// Notification. -Notification.belongsTo(Collective); -Notification.belongsTo(User); +// ActivitySubscription. +ActivitySubscription.belongsTo(Collective); +ActivitySubscription.belongsTo(User); // OAuthAuthorizationCode OAuthAuthorizationCode.belongsTo(Application, { foreignKey: 'ApplicationId', as: 'application' }); @@ -342,7 +342,7 @@ User.hasMany(Activity); User.hasMany(ConnectedAccount, { foreignKey: 'CreatedByUserId' }); User.hasMany(ExportRequest, { foreignKey: 'CreatedByUserId', as: 'exportRequests' }); User.hasMany(Member, { foreignKey: 'CreatedByUserId' }); -User.hasMany(Notification); +User.hasMany(ActivitySubscription); User.hasMany(Order, { foreignKey: 'CreatedByUserId', as: 'orders' }); User.hasMany(PaymentMethod, { foreignKey: 'CreatedByUserId' }); User.hasMany(Transaction, { foreignKey: 'CreatedByUserId', as: 'transactions' }); @@ -409,7 +409,7 @@ export { Member, MemberInvitation, MigrationLog, - Notification, + ActivitySubscription, OAuthAuthorizationCode, Order, PaymentMethod, diff --git a/sql/ban-collectives.sql b/sql/ban-collectives.sql index e646c3f7413..2c42a554641 100644 --- a/sql/ban-collectives.sql +++ b/sql/ban-collectives.sql @@ -4,7 +4,7 @@ -- • collectiveSlugs: The list of collective slugs to ban -- -- Not covered yet: --- - models with no soft-delete mechanism: AccountingCategories, Activity, ConversationFollowers, Notifications, EmojiReactions, ExpenseAttachedFiles, SocialLinks. +-- - models with no soft-delete mechanism: AccountingCategories, Activity, ConversationFollowers, ActivitySubscriptions, EmojiReactions, ExpenseAttachedFiles, SocialLinks. -- - incognito profiles -- -- --------------------------------------------------------------------------------- diff --git a/sql/scrub_database.sql b/sql/scrub_database.sql index c0ecd5171ab..d7777f3519a 100644 --- a/sql/scrub_database.sql +++ b/sql/scrub_database.sql @@ -36,8 +36,8 @@ DELETE FROM "ConnectedAccounts" WHERE "UserId" IS NULL; UPDATE "ConnectedAccounts" SET "clientId"='*****',"secret"='*****'; /* We delete all notifications that don't have a CollectiveId anymore */ -DELETE FROM "Notifications" WHERE "CollectiveId" IS NULL; -UPDATE "Notifications" SET "webhookUrl"='http://****'; +DELETE FROM "ActivitySubscriptions" WHERE "CollectiveId" IS NULL; +UPDATE "ActivitySubscriptions" SET "webhookUrl"='http://****'; /* We delete all transactions that don't have a CollectiveId anymore */ DELETE FROM "Transactions" WHERE "CollectiveId" IS NULL; diff --git a/test/server/graphql/v2/mutation/UpdateMutations.test.js b/test/server/graphql/v2/mutation/UpdateMutations.test.js index 51ff1974a4a..c4b39d09abe 100644 --- a/test/server/graphql/v2/mutation/UpdateMutations.test.js +++ b/test/server/graphql/v2/mutation/UpdateMutations.test.js @@ -238,7 +238,7 @@ describe('server/graphql/v2/mutation/UpdateMutations', () => { unsubscribedUser = await fakeUser(); await org.addUserWithRole(unsubscribedUser, roles.BACKER, {}, { skipActivity: true }); - await models.Notification.unsubscribe( + await models.ActivitySubscription.unsubscribe( ActivityTypes.COLLECTIVE_UPDATE_PUBLISHED, 'email', unsubscribedUser.id, diff --git a/test/server/models/Notification.test.js b/test/server/models/Notification.test.js index 4cda8180c1a..c0d9bd2ac3b 100644 --- a/test/server/models/Notification.test.js +++ b/test/server/models/Notification.test.js @@ -8,9 +8,9 @@ import models from '../../../server/models'; import { fakeCollective, fakeNotification, fakeUser, multiple } from '../../test-helpers/fake-data'; import * as utils from '../../utils'; -const { User, Collective, Notification, Tier, Order } = models; +const { User, Collective, ActivitySubscription, Tier, Order } = models; -describe('server/models/Notification', () => { +describe('server/models/ActivitySubscription', () => { let host, collective, hostAdmin, sandbox, emailSendMessageSpy; beforeEach(() => utils.resetTestDB()); @@ -38,7 +38,7 @@ describe('server/models/Notification', () => { describe('unsubscribe', () => { it('should unsubscribe from ActivityType', async () => { const user = await fakeUser(); - const notification = await Notification.unsubscribe(ActivityTypes.COLLECTIVE_APPROVED, 'email', user.id); + const notification = await ActivitySubscription.unsubscribe(ActivityTypes.COLLECTIVE_APPROVED, 'email', user.id); expect(notification).to.have.property('type').equal(ActivityTypes.COLLECTIVE_APPROVED); expect(notification).to.have.property('channel').equal('email'); @@ -48,7 +48,12 @@ describe('server/models/Notification', () => { it('should unsubscribe from ActivityClass', async () => { const user = await fakeUser(); - const notification = await Notification.unsubscribe(ActivityClasses.EXPENSES, 'email', user.id, collective.id); + const notification = await ActivitySubscription.unsubscribe( + ActivityClasses.EXPENSES, + 'email', + user.id, + collective.id, + ); expect(notification).to.have.property('type').equal(ActivityClasses.EXPENSES); expect(notification).to.have.property('channel').equal('email'); @@ -59,16 +64,16 @@ describe('server/models/Notification', () => { it('should delete existing ActivityType when unsubscribe from ActivityClass of such type', async () => { const user = await fakeUser(); - await Notification.unsubscribe(ActivityTypes.COLLECTIVE_EXPENSE_CREATED, 'email', user.id, collective.id); - let userNotifications = await Notification.findAll({ where: { UserId: user.id } }); + await ActivitySubscription.unsubscribe(ActivityTypes.COLLECTIVE_EXPENSE_CREATED, 'email', user.id, collective.id); + let userNotifications = await ActivitySubscription.findAll({ where: { UserId: user.id } }); expect(userNotifications).to.have.length(1); expect(userNotifications[0]).to.have.property('type').equal(ActivityTypes.COLLECTIVE_EXPENSE_CREATED); expect(userNotifications[0]).to.have.property('channel').equal('email'); expect(userNotifications[0]).to.have.property('UserId').equal(user.id); expect(userNotifications[0]).to.have.property('CollectiveId').equal(collective.id); - await Notification.unsubscribe(ActivityClasses.EXPENSES, 'email', user.id, collective.id); - userNotifications = await Notification.findAll({ where: { UserId: user.id } }); + await ActivitySubscription.unsubscribe(ActivityClasses.EXPENSES, 'email', user.id, collective.id); + userNotifications = await ActivitySubscription.findAll({ where: { UserId: user.id } }); expect(userNotifications).to.have.length(1); expect(userNotifications[0]).to.have.property('type').equal(ActivityClasses.EXPENSES); expect(userNotifications[0]).to.have.property('channel').equal('email'); @@ -87,12 +92,12 @@ describe('server/models/Notification', () => { type: ActivityTypes.COLLECTIVE_EXPENSE_CREATED, }); - let userNotifications = await Notification.findAll({ where: { UserId: user.id } }); + let userNotifications = await ActivitySubscription.findAll({ where: { UserId: user.id } }); expect(userNotifications).to.have.length(1); - await Notification.subscribe(notification.type, notification.channel, user.id, notification.CollectiveId); + await ActivitySubscription.subscribe(notification.type, notification.channel, user.id, notification.CollectiveId); - userNotifications = await Notification.findAll({ where: { UserId: user.id } }); + userNotifications = await ActivitySubscription.findAll({ where: { UserId: user.id } }); expect(userNotifications).to.have.length(0); }); @@ -105,12 +110,17 @@ describe('server/models/Notification', () => { type: ActivityTypes.COLLECTIVE_EXPENSE_CREATED, }); - let userNotifications = await Notification.findAll({ where: { UserId: user.id } }); + let userNotifications = await ActivitySubscription.findAll({ where: { UserId: user.id } }); expect(userNotifications).to.have.length(1); - await Notification.subscribe(ActivityClasses.EXPENSES, notification.channel, user.id, notification.CollectiveId); + await ActivitySubscription.subscribe( + ActivityClasses.EXPENSES, + notification.channel, + user.id, + notification.CollectiveId, + ); - userNotifications = await Notification.findAll({ where: { UserId: user.id } }); + userNotifications = await ActivitySubscription.findAll({ where: { UserId: user.id } }); expect(userNotifications).to.have.length(0); }); }); @@ -125,11 +135,11 @@ describe('server/models/Notification', () => { it('getSubscribers to the backers mailinglist', async () => { await Promise.all(users.map(user => collective.addUserWithRole(user, 'BACKER'))); - const subscribers = await Notification.getSubscribersUsers(collective.slug, 'backers'); + const subscribers = await ActivitySubscription.getSubscribersUsers(collective.slug, 'backers'); expect(subscribers.length).to.equal(2); - await Notification.unsubscribe('mailinglist.backers', 'email', subscribers[0].id, collective.id); - const subscribers2 = await Notification.getSubscribers(collective.slug, 'backers'); + await ActivitySubscription.unsubscribe('mailinglist.backers', 'email', subscribers[0].id, collective.id); + const subscribers2 = await ActivitySubscription.getSubscribers(collective.slug, 'backers'); expect(subscribers2.length).to.equal(1); }); @@ -166,11 +176,11 @@ describe('server/models/Notification', () => { ), ); - const subscribers = await Notification.getSubscribers(event.slug, event.slug); + const subscribers = await ActivitySubscription.getSubscribers(event.slug, event.slug); expect(subscribers.length).to.equal(2); - await Notification.unsubscribe(`mailinglist.${event.slug}`, 'email', users[0].id, event.id); - const subscribers2 = await Notification.getSubscribers(event.slug, event.slug); + await ActivitySubscription.unsubscribe(`mailinglist.${event.slug}`, 'email', users[0].id, event.id); + const subscribers2 = await ActivitySubscription.getSubscribers(event.slug, event.slug); expect(subscribers2.length).to.equal(1); }); }); @@ -187,7 +197,7 @@ describe('server/models/Notification', () => { active: false, type: ActivityTypes.COLLECTIVE_APPLY, }); - const unsubscribers = await models.Notification.getUnsubscribers({ type: notification.type }); + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: notification.type }); expect(unsubscribers).to.containSubset([{ id: notification.UserId }]); }); @@ -199,7 +209,7 @@ describe('server/models/Notification', () => { active: false, type: ActivityTypes.COLLECTIVE_APPLY, }); - const unsubscribers = await models.Notification.getUnsubscribers({ + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: notification.type, attributes: ['email'], }); @@ -216,7 +226,7 @@ describe('server/models/Notification', () => { type: ActivityClasses.CONTRIBUTIONS, active: false, }); - const unsubscribers = await models.Notification.getUnsubscribers({ + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: ActivitiesPerClass[ActivityClasses.CONTRIBUTIONS][0], }); @@ -233,7 +243,7 @@ describe('server/models/Notification', () => { type: ActivityTypes.COLLECTIVE_APPLY, }); - const unsubscribers = await models.Notification.getUnsubscribers({ + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: notification.type, CollectiveId: collective.id, }); @@ -253,7 +263,7 @@ describe('server/models/Notification', () => { type: ActivityTypes.COLLECTIVE_APPLY, }); - const unsubscribers = await models.Notification.getUnsubscribers({ + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: notification.type, CollectiveId: collective.id, }); @@ -273,7 +283,7 @@ describe('server/models/Notification', () => { type: ActivityTypes.COLLECTIVE_COMMENT_CREATED, }); - const unsubscribers = await models.Notification.getUnsubscribers({ + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: notification.type, CollectiveId: collective.id, }); @@ -306,7 +316,7 @@ describe('server/models/Notification', () => { active: false, }); - const unsubscribers = await models.Notification.getUnsubscribers({ + const unsubscribers = await models.ActivitySubscription.getUnsubscribers({ type: notification.type, CollectiveId: collective.id, }); @@ -365,7 +375,7 @@ describe('server/models/Notification', () => { }); it("doesn't notify admin of host if unsubscribed", async () => { - await models.Notification.create({ + await models.ActivitySubscription.create({ CollectiveId: host.id, UserId: hostAdmin.id, type: 'collective.expense.paid.for.host', @@ -384,43 +394,43 @@ describe('server/models/Notification', () => { describe('webhookURL', () => { it('must be a valid URL', async () => { - await expect(Notification.create({ webhookUrl: 'xxxxxxx' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'xxxxxxx' })).to.be.rejectedWith( 'Validation error: Webhook URL must be a valid URL', ); - await expect(Notification.create({ webhookUrl: 'http://' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'http://' })).to.be.rejectedWith( 'Validation error: Webhook URL must be a valid URL', ); - await expect(Notification.create({ webhookUrl: 'https://' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'https://' })).to.be.rejectedWith( 'Validation error: Webhook URL must be a valid URL', ); }); it('cannot be an internal URL or an IP address', async () => { - await expect(Notification.create({ webhookUrl: '0.0.0.0' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: '0.0.0.0' })).to.be.rejectedWith( 'Validation error: IP addresses cannot be used as webhooks', ); - await expect(Notification.create({ webhookUrl: 'localhost' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'localhost' })).to.be.rejectedWith( 'Validation error: Webhook URL must be a valid URL', ); - await expect(Notification.create({ webhookUrl: 'http://localhost' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'http://localhost' })).to.be.rejectedWith( 'Validation error: Webhook URL must be a valid URL', ); - await expect(Notification.create({ webhookUrl: 'https://opencollective.com' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'https://opencollective.com' })).to.be.rejectedWith( 'Validation error: Open Collective URLs cannot be used as webhooks', ); - await expect(Notification.create({ webhookUrl: 'https://0.0.0.0' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'https://0.0.0.0' })).to.be.rejectedWith( 'Validation error: IP addresses cannot be used as webhooks', ); - await expect(Notification.create({ webhookUrl: 'https://12.12.12.12' })).to.be.rejectedWith( + await expect(ActivitySubscription.create({ webhookUrl: 'https://12.12.12.12' })).to.be.rejectedWith( 'Validation error: IP addresses cannot be used as webhooks', ); }); it('accepts valid URLs, adds the protocol automatically', async () => { - const notif1 = await Notification.create({ webhookUrl: 'https://google.com/stuff' }); + const notif1 = await ActivitySubscription.create({ webhookUrl: 'https://google.com/stuff' }); expect(notif1.webhookUrl).to.equal('https://google.com/stuff'); - const notif2 = await Notification.create({ webhookUrl: 'google.com/stuff' }); + const notif2 = await ActivitySubscription.create({ webhookUrl: 'google.com/stuff' }); expect(notif2.webhookUrl).to.equal('https://google.com/stuff'); }); }); diff --git a/test/server/routes/email.test.js b/test/server/routes/email.test.js index 5f9b434319e..563dfc26359 100644 --- a/test/server/routes/email.test.js +++ b/test/server/routes/email.test.js @@ -84,7 +84,7 @@ describe('server/routes/email', () => { const lists = usersData[index].lists || []; return await Promise.all( lists.map(list => - models.Notification.create({ + models.ActivitySubscription.create({ channel: 'email', UserId: user.id, CollectiveId: collective.id, @@ -127,7 +127,7 @@ describe('server/routes/email', () => { return request(expressApp) .get(generateUnsubscribeUrl(users[0].email)) - .then(() => models.Notification.count({ where })) + .then(() => models.ActivitySubscription.count({ where })) .then(count => expect(count).to.equal(0)); }); }); diff --git a/test/test-helpers/fake-data.ts b/test/test-helpers/fake-data.ts index 26f06e88008..ee6d166dbf9 100644 --- a/test/test-helpers/fake-data.ts +++ b/test/test-helpers/fake-data.ts @@ -37,7 +37,7 @@ import models, { ExpenseAttachedFile, ExpenseItem, Location, - Notification, + ActivitySubscription, PaypalProduct, PersonalToken, PlatformSubscription, @@ -858,8 +858,8 @@ export const fakeSubscription = (params = {}) => { }); }; -export const fakeNotification = async (data: Partial> = {}) => { - return models.Notification.create({ +export const fakeNotification = async (data: Partial> = {}) => { + return models.ActivitySubscription.create({ channel: sample(Object.values(channels)), type: sample(Object.values(activities)), active: true,