From 749b398d63e1868618c8523db97a56d846ca5faa Mon Sep 17 00:00:00 2001 From: gitwoz <177856586+gitwoz@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:36:43 +0700 Subject: [PATCH 1/2] feat(campaign): support article w/o campaign stage --- schema.graphql | 2 +- src/connectors/campaignService.ts | 48 +++++++++++++++++++++++++--- src/connectors/queue/publication.ts | 2 +- src/definitions/campaign.d.ts | 2 +- src/definitions/schema.d.ts | 2 +- src/mutations/article/editArticle.ts | 45 +++----------------------- src/mutations/draft/putDraft.ts | 32 +------------------ src/queries/draft/campaigns.ts | 6 ++-- src/types/__test__/1/draft.test.ts | 9 +++++- src/types/article.ts | 6 ++-- src/types/campaign.ts | 4 +-- src/types/comment.ts | 2 +- src/types/moment.ts | 2 +- src/types/system.ts | 12 +++---- src/types/user.ts | 6 ++-- 15 files changed, 82 insertions(+), 98 deletions(-) diff --git a/schema.graphql b/schema.graphql index 347ef8b68..cfb718672 100644 --- a/schema.graphql +++ b/schema.graphql @@ -661,7 +661,7 @@ input EditArticleInput { input ArticleCampaignInput { campaign: ID! - stage: ID! + stage: ID } input AppreciateArticleInput { diff --git a/src/connectors/campaignService.ts b/src/connectors/campaignService.ts index 11bb5350b..0b69f0efa 100644 --- a/src/connectors/campaignService.ts +++ b/src/connectors/campaignService.ts @@ -31,6 +31,7 @@ import { shortHash, toDatetimeRangeString, fromDatetimeRangeString, + fromGlobalId, // excludeSpam, } from 'common/utils' import { AtomService, NotificationService } from 'connectors' @@ -298,7 +299,7 @@ export class CampaignService { public updateArticleCampaigns = async ( article: Pick, - newCampaigns: Array<{ campaignId: string; campaignStageId: string }> + newCampaigns: Array<{ campaignId: string; campaignStageId?: string }> ) => { const mutatedCampaignIds = [] const knexRO = this.connections.knexRO @@ -326,7 +327,7 @@ export class CampaignService { await this.models.update({ table: 'campaign_article', where: { articleId: article.id, campaignId }, - data: { campaignStageId }, + data: { campaignStageId: campaignStageId ?? null }, }) mutatedCampaignIds.push(campaignId) } @@ -353,7 +354,7 @@ export class CampaignService { public submitArticleToCampaign = async ( article: Pick, campaignId: string, - campaignStageId: string + campaignStageId?: string ) => { await this.validate({ userId: article.authorId, @@ -372,7 +373,7 @@ export class CampaignService { userId, }: { campaignId: string - campaignStageId: string + campaignStageId?: string userId: string }) => { const campaign = await this.models.campaignIdLoader.load(campaignId) @@ -392,6 +393,10 @@ export class CampaignService { throw new ActionFailedError(`user not applied to campaign ${campaignId}`) } + if (!campaignStageId) { + return + } + const stage = await this.models.campaignStageIdLoader.load(campaignStageId) if (!stage) { throw new CampaignStageNotFoundError('stage not found') @@ -522,4 +527,39 @@ export class CampaignService { update: { campaignId: id, boost }, where: { campaignId: id }, }) + + public validateCampaigns = async ( + campaigns: Array<{ campaign: string; stage?: string }>, + userId: string + ) => { + const _campaigns = campaigns.map( + ({ campaign: campaignGlobalId, stage: stageGlobalId }) => { + const { id: campaignId, type: campaignIdType } = + fromGlobalId(campaignGlobalId) + if (campaignIdType !== NODE_TYPES.Campaign) { + throw new UserInputError('invalid campaign id') + } + + if (!stageGlobalId) { + return { campaign: campaignId } + } + + const { id: stageId, type: stageIdType } = fromGlobalId(stageGlobalId) + if (stageIdType !== NODE_TYPES.CampaignStage) { + throw new UserInputError('invalid stage id') + } + + return { campaign: campaignId, stage: stageId } + } + ) + + for (const { campaign, stage } of _campaigns) { + await this.validate({ + userId, + campaignId: campaign, + campaignStageId: stage, + }) + } + return _campaigns + } } diff --git a/src/connectors/queue/publication.ts b/src/connectors/queue/publication.ts index 68f328142..dc94107fb 100644 --- a/src/connectors/queue/publication.ts +++ b/src/connectors/queue/publication.ts @@ -506,7 +506,7 @@ export class PublicationQueue { campaigns, }: { article: Article - campaigns: Array<{ campaign: string; stage: string }> + campaigns: Array<{ campaign: string; stage?: string }> }) => { const campaignService = new CampaignService(this.connections) for (const { campaign, stage } of campaigns) { diff --git a/src/definitions/campaign.d.ts b/src/definitions/campaign.d.ts index 4af2ae7f0..1a19f7b92 100644 --- a/src/definitions/campaign.d.ts +++ b/src/definitions/campaign.d.ts @@ -44,7 +44,7 @@ export interface CampaignUser { export interface CampaignArticle { id: string campaignId: string - campaignStageId: string | null + campaignStageId?: string | null articleId: string createdAt: Date featured: boolean diff --git a/src/definitions/schema.d.ts b/src/definitions/schema.d.ts index c8f86e36a..2472081fc 100644 --- a/src/definitions/schema.d.ts +++ b/src/definitions/schema.d.ts @@ -436,7 +436,7 @@ export type GQLArticleCampaign = { export type GQLArticleCampaignInput = { campaign: Scalars['ID']['input'] - stage: Scalars['ID']['input'] + stage?: InputMaybe } export type GQLArticleConnection = GQLConnection & { diff --git a/src/mutations/article/editArticle.ts b/src/mutations/article/editArticle.ts index 6134a71de..e45e46578 100644 --- a/src/mutations/article/editArticle.ts +++ b/src/mutations/article/editArticle.ts @@ -1,10 +1,4 @@ -import type { - Article, - Draft, - Circle, - GQLMutationResolvers, - DataSources, -} from 'definitions' +import type { Article, Draft, Circle, GQLMutationResolvers } from 'definitions' import { invalidateFQC } from '@matters/apollo-response-cache' import { stripHtml } from '@matters/ipns-site-generator' @@ -330,9 +324,10 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( * campaigns */ if (campaigns !== undefined) { - const _campaigns = await validateCampaigns(campaigns ?? [], viewer.id, { - campaignService, - }) + const _campaigns = await campaignService.validateCampaigns( + campaigns ?? [], + viewer.id + ) const mutated = await campaignService.updateArticleCampaigns( article, _campaigns.map(({ campaign, stage }) => ({ @@ -423,34 +418,4 @@ const resolver: GQLMutationResolvers['editArticle'] = async ( return node } -const validateCampaigns = async ( - campaigns: Array<{ campaign: string; stage: string }>, - userId: string, - { campaignService }: Pick -) => { - const _campaigns = campaigns.map( - ({ campaign: campaignGlobalId, stage: stageGlobalId }) => { - const { id: campaignId, type: campaignIdType } = - fromGlobalId(campaignGlobalId) - if (campaignIdType !== NODE_TYPES.Campaign) { - throw new UserInputError('invalid campaign id') - } - const { id: stageId, type: stageIdType } = fromGlobalId(stageGlobalId) - if (stageIdType !== NODE_TYPES.CampaignStage) { - throw new UserInputError('invalid stage id') - } - - return { campaign: campaignId, stage: stageId } - } - ) - for (const { campaign, stage } of _campaigns) { - await campaignService.validate({ - userId, - campaignId: campaign, - campaignStageId: stage, - }) - } - return _campaigns -} - export default resolver diff --git a/src/mutations/draft/putDraft.ts b/src/mutations/draft/putDraft.ts index 22ec81c90..a6b271f4e 100644 --- a/src/mutations/draft/putDraft.ts +++ b/src/mutations/draft/putDraft.ts @@ -166,7 +166,7 @@ const resolver: GQLMutationResolvers['putDraft'] = async ( campaigns: campaigns && JSON.stringify( - await validateCampaigns(campaigns, viewer.id, { campaignService }) + await campaignService.validateCampaigns(campaigns, viewer.id) ), }, isUndefined // to drop only undefined @@ -380,34 +380,4 @@ const validateConnections = async ({ ) } -const validateCampaigns = async ( - campaigns: Array<{ campaign: string; stage: string }>, - userId: string, - { campaignService }: Pick -) => { - const _campaigns = campaigns.map( - ({ campaign: campaignGlobalId, stage: stageGlobalId }) => { - const { id: campaignId, type: campaignIdType } = - fromGlobalId(campaignGlobalId) - if (campaignIdType !== NODE_TYPES.Campaign) { - throw new UserInputError('invalid campaign id') - } - const { id: stageId, type: stageIdType } = fromGlobalId(stageGlobalId) - if (stageIdType !== NODE_TYPES.CampaignStage) { - throw new UserInputError('invalid stage id') - } - - return { campaign: campaignId, stage: stageId } - } - ) - for (const { campaign, stage } of _campaigns) { - await campaignService.validate({ - userId, - campaignId: campaign, - campaignStageId: stage, - }) - } - return _campaigns -} - export default resolver diff --git a/src/queries/draft/campaigns.ts b/src/queries/draft/campaigns.ts index f912f4e17..223dd9ad0 100644 --- a/src/queries/draft/campaigns.ts +++ b/src/queries/draft/campaigns.ts @@ -10,9 +10,11 @@ const resolver: GQLDraftResolvers['campaigns'] = ( } return Promise.all( campaigns.map( - async ({ campaign, stage }: { campaign: string; stage: string }) => ({ + async ({ campaign, stage }: { campaign: string; stage?: string }) => ({ campaign: await atomService.campaignIdLoader.load(campaign), - stage: await atomService.campaignStageIdLoader.load(stage), + stage: stage + ? await atomService.campaignStageIdLoader.load(stage) + : null, }) ) ) diff --git a/src/types/__test__/1/draft.test.ts b/src/types/__test__/1/draft.test.ts index 0dcb55e75..cacc0ec57 100644 --- a/src/types/__test__/1/draft.test.ts +++ b/src/types/__test__/1/draft.test.ts @@ -567,7 +567,7 @@ describe('put draft', () => { type: NODE_TYPES.CampaignStage, id: stages[0].id, }) - const { campaigns } = await putDraft( + const { id: draftId, campaigns } = await putDraft( { draft: { title: Math.random().toString(), @@ -588,5 +588,12 @@ describe('put draft', () => { ) expect(campaigns[0].campaign.id).toBe(campaignGlobalId) expect(campaigns[0].stage.id).toBe(stageGlobalId) + + // remove stage + const { campaigns: campaigns2 } = await putDraft( + { draft: { id: draftId, campaigns: [{ campaign: campaignGlobalId }] } }, + connections + ) + expect(campaigns2[0].stage).toBeNull() }) }) diff --git a/src/types/article.ts b/src/types/article.ts index 4df8eb9d4..1a713246f 100644 --- a/src/types/article.ts +++ b/src/types/article.ts @@ -13,7 +13,7 @@ export default /* GraphQL */ ` # Article # ############## "Publish an article onto IPFS." - publishArticle(input: PublishArticleInput!): Draft! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level2}") @purgeCache(type: "${NODE_TYPES.Draft}") @rateLimit(limit:${PUBLISH_ARTICLE_RATE_LIMIT}, period:720) + publishArticle(input: PublishArticleInput!): Draft! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level2}") @purgeCache(type: "${NODE_TYPES.Draft}") @rateLimit(limit: ${PUBLISH_ARTICLE_RATE_LIMIT}, period: 720) "Edit an article." editArticle(input: EditArticleInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.Article}") @@ -23,7 +23,7 @@ export default /* GraphQL */ ` toggleBookmarkArticle(input: ToggleItemInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level1}") @purgeCache(type: "${NODE_TYPES.Article}") "Appreciate an article." - appreciateArticle(input: AppreciateArticleInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.Article}") @rateLimit(limit:5, period:60) + appreciateArticle(input: AppreciateArticleInput!): Article! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @purgeCache(type: "${NODE_TYPES.Article}") @rateLimit(limit: 5, period: 60) "Read an article." readArticle(input: ReadArticleInput!): Article! @@ -418,7 +418,7 @@ export default /* GraphQL */ ` input ArticleCampaignInput { campaign: ID! - stage: ID! + stage: ID } input AppreciateArticleInput { diff --git a/src/types/campaign.ts b/src/types/campaign.ts index 3ce8990c2..2e440a0ec 100644 --- a/src/types/campaign.ts +++ b/src/types/campaign.ts @@ -3,7 +3,7 @@ import { AUTH_MODE, NODE_TYPES, CACHE_TTL } from 'common/enums' export default /* GraphQL */ ` extend type Query { campaign(input: CampaignInput!): Campaign @logCache(type: "${NODE_TYPES.Campaign}") - campaigns(input:CampaignsInput!): CampaignConnection! + campaigns(input: CampaignsInput!): CampaignConnection! } extend type Mutation { @@ -100,7 +100,7 @@ export default /* GraphQL */ ` announcements: [Article!]! applicationPeriod: DatetimeRange - writingPeriod:DatetimeRange + writingPeriod: DatetimeRange stages: [CampaignStage!]! state: CampaignState! diff --git a/src/types/comment.ts b/src/types/comment.ts index 6abad4e04..3727243c4 100644 --- a/src/types/comment.ts +++ b/src/types/comment.ts @@ -6,7 +6,7 @@ const PUT_COMMENT_RATE_LIMIT = isProd ? 6 : 1000 export default /* GraphQL */ ` extend type Mutation { "Publish or update a comment." - putComment(input: PutCommentInput!): Comment! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level2}") @purgeCache(type: "${NODE_TYPES.Comment}") @rateLimit(limit:${PUT_COMMENT_RATE_LIMIT}, period:60) + putComment(input: PutCommentInput!): Comment! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level2}") @purgeCache(type: "${NODE_TYPES.Comment}") @rateLimit(limit: ${PUT_COMMENT_RATE_LIMIT}, period: 60) "Remove a comment." deleteComment(input: DeleteCommentInput!): Comment! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level2}") @purgeCache(type: "${NODE_TYPES.Comment}") diff --git a/src/types/moment.ts b/src/types/moment.ts index 8c620a8af..b9238aca5 100644 --- a/src/types/moment.ts +++ b/src/types/moment.ts @@ -8,7 +8,7 @@ export default /* GraphQL */ ` moment(input: MomentInput!): Moment } extend type Mutation { - putMoment(input: PutMomentInput!): Moment! @auth(mode: "${AUTH_MODE.oauth}") @rateLimit(limit:${POST_MOMENT_RATE_LIMIT}, period:300) @logCache(type: "${NODE_TYPES.Moment}") + putMoment(input: PutMomentInput!): Moment! @auth(mode: "${AUTH_MODE.oauth}") @rateLimit(limit: ${POST_MOMENT_RATE_LIMIT}, period: 300) @logCache(type: "${NODE_TYPES.Moment}") deleteMoment(input: DeleteMomentInput!): Moment! @auth(mode: "${AUTH_MODE.oauth}") @purgeCache(type: "${NODE_TYPES.Moment}") likeMoment(input: LikeMomentInput!): Moment! @auth(mode: "${AUTH_MODE.oauth}") @purgeCache(type: "${NODE_TYPES.Moment}") diff --git a/src/types/system.ts b/src/types/system.ts index eb7b82e2b..0cc9ad76e 100644 --- a/src/types/system.ts +++ b/src/types/system.ts @@ -22,17 +22,17 @@ export default /* GraphQL */ ` extend type Mutation { "Upload a single file." - singleFileUpload(input: SingleFileUploadInput!): Asset! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @rateLimit(limit:${UPLOAD_RATE_LIMIT}, period:720) - directImageUpload(input: DirectImageUploadInput!): Asset! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @rateLimit(limit:${UPLOAD_RATE_LIMIT}, period:720) + singleFileUpload(input: SingleFileUploadInput!): Asset! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @rateLimit(limit: ${UPLOAD_RATE_LIMIT}, period: 720) + directImageUpload(input: DirectImageUploadInput!): Asset! @auth(mode: "${AUTH_MODE.oauth}", group: "${SCOPE_GROUP.level3}") @rateLimit(limit: ${UPLOAD_RATE_LIMIT}, period: 720) "Add specific user behavior record." logRecord(input: LogRecordInput!): Boolean "Add blocked search keyword to blocked_search_word db" - addBlockedSearchKeyword(input:KeywordInput!): BlockedSearchKeyword! @auth(mode: "${AUTH_MODE.admin}") + addBlockedSearchKeyword(input: KeywordInput!): BlockedSearchKeyword! @auth(mode: "${AUTH_MODE.admin}") "Delete blocked search keywords from search_history db" - deleteBlockedSearchKeywords(input:KeywordsInput!): Boolean @auth(mode: "${AUTH_MODE.admin}") + deleteBlockedSearchKeywords(input: KeywordsInput!): Boolean @auth(mode: "${AUTH_MODE.admin}") "Submit inappropriate content report" submitReport(input: SubmitReportInput!): Report! @auth(mode: "${AUTH_MODE.oauth}") @@ -48,7 +48,7 @@ export default /* GraphQL */ ` putAnnouncement(input: PutAnnouncementInput!): Announcement! @auth(mode: "${AUTH_MODE.admin}") deleteAnnouncements(input: DeleteAnnouncementsInput!): Boolean! @auth(mode: "${AUTH_MODE.admin}") putRestrictedUsers(input: PutRestrictedUsersInput!): [User!]! @complexity(value: 1, multipliers: ["input.ids"]) @auth(mode: "${AUTH_MODE.admin}") - putIcymiTopic(input:PutIcymiTopicInput!): IcymiTopic @auth(mode: "${AUTH_MODE.admin}") + putIcymiTopic(input: PutIcymiTopicInput!): IcymiTopic @auth(mode: "${AUTH_MODE.admin}") setSpamStatus(input: SetSpamStatusInput!): Article! @auth(mode: "${AUTH_MODE.admin}") } @@ -139,7 +139,7 @@ export default /* GraphQL */ ` badgedUsers(input: BadgedUsersInput!): UserConnection! restrictedUsers(input: ConnectionArgs!): UserConnection! reports(input: ConnectionArgs!): ReportConnection! - icymiTopics(input:ConnectionArgs!): IcymiTopicConnection! + icymiTopics(input: ConnectionArgs!): IcymiTopicConnection! } diff --git a/src/types/user.ts b/src/types/user.ts index 68aff3452..d23f34f42 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -142,7 +142,7 @@ export default /* GraphQL */ ` articles(input: UserArticlesInput!): ArticleConnection! @complexity(multipliers: ["input.first"], value: 1) "Articles and moments authored by current user." - writings(input:WritingInput!): WritingConnection! + writings(input: WritingInput!): WritingConnection! "collections authored by current user." collections(input: ConnectionArgs!): CollectionConnection! @complexity(multipliers: ["input.first"], value: 1) @@ -193,7 +193,7 @@ export default /* GraphQL */ ` analytics: UserAnalytics! @auth(mode: "${AUTH_MODE.oauth}") "active applied campaigns" - campaigns(input:ConnectionArgs!): CampaignConnection! + campaigns(input: ConnectionArgs!): CampaignConnection! "Status of current user." status: UserStatus @@ -1068,7 +1068,7 @@ export default /* GraphQL */ ` } input RecommendationFollowingFilterInput { - type:RecommendationFollowingFilterType + type: RecommendationFollowingFilterType } enum RecommendationFollowingFilterType { From 8cefa7b69a24189a497e84eeeb08199a1fced339 Mon Sep 17 00:00:00 2001 From: gitwoz <177856586+gitwoz@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:54:27 +0700 Subject: [PATCH 2/2] feat(campaign): trigger write_challenge_applied notices with campaign as entity --- src/connectors/campaignService.ts | 1 + src/definitions/notification.d.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/connectors/campaignService.ts b/src/connectors/campaignService.ts index 0b69f0efa..cea803b7e 100644 --- a/src/connectors/campaignService.ts +++ b/src/connectors/campaignService.ts @@ -442,6 +442,7 @@ export class CampaignService { application.createdAt.getTime() < end.getTime() ? OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_applied : OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_applied_late_bird, + entities: [{ type: 'target', entityTable: 'campaign', entity: campaign }], recipientId: updated.userId, data: { link: campaign.link ?? '' }, }) diff --git a/src/definitions/notification.d.ts b/src/definitions/notification.d.ts index 062d69b83..e110b857d 100644 --- a/src/definitions/notification.d.ts +++ b/src/definitions/notification.d.ts @@ -21,6 +21,7 @@ type NoticeEntityType = | 'tag' | 'article' | 'circle' + | 'campaign' type NotificationType = | BaseNoticeType @@ -373,6 +374,7 @@ interface NoticeWriteChallengeAppliedParams extends NotificationRequiredParams { event: | OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_applied | OFFICIAL_NOTICE_EXTEND_TYPE.write_challenge_applied_late_bird + entities: [NotificationEntity<'target', 'campaign'>] recipientId: string data: { link: string } }