From 648002db410e4cdb56bd66358606e7585b33a24c Mon Sep 17 00:00:00 2001 From: yuta0709 Date: Fri, 10 Jul 2026 17:49:36 +0900 Subject: [PATCH] Keep posting campaign when frontmatter has no campaign keys Co-Authored-By: Claude Fable 5 --- src/lib/check-frontmatter-type.test.ts | 22 +++++++ src/lib/check-frontmatter-type.ts | 13 ++-- src/lib/entities/qiita-item.ts | 8 +-- src/lib/file-system-repo.test.ts | 89 ++++++++++++++++++++++++++ src/lib/file-system-repo.ts | 26 +++++--- src/lib/validators/item-validator.ts | 4 +- src/qiita-api/index.ts | 8 +-- src/server/api/items.ts | 2 +- 8 files changed, 147 insertions(+), 25 deletions(-) diff --git a/src/lib/check-frontmatter-type.test.ts b/src/lib/check-frontmatter-type.test.ts index a9fd4633..a6488c55 100644 --- a/src/lib/check-frontmatter-type.test.ts +++ b/src/lib/check-frontmatter-type.test.ts @@ -143,6 +143,17 @@ describe("checkFrontmatterType", () => { }); }); + describe("when postingCampaignUuid is undefined", () => { + const errorMessages = checkFrontmatterType({ + ...frontMatter, + postingCampaignUuid: undefined, + }); + + it("returns no errors", () => { + expect(errorMessages).toEqual([]); + }); + }); + describe("when postingCampaignUuid is string", () => { const errorMessages = checkFrontmatterType({ ...frontMatter, @@ -178,6 +189,17 @@ describe("checkFrontmatterType", () => { }); }); + describe("when agreedPostingCampaignTerm is undefined", () => { + const errorMessages = checkFrontmatterType({ + ...frontMatter, + agreedPostingCampaignTerm: undefined, + }); + + it("returns no errors", () => { + expect(errorMessages).toEqual([]); + }); + }); + describe("when agreedPostingCampaignTerm is String", () => { const errorMessages = checkFrontmatterType({ ...frontMatter, diff --git a/src/lib/check-frontmatter-type.ts b/src/lib/check-frontmatter-type.ts index cd0678e2..0f7e7431 100644 --- a/src/lib/check-frontmatter-type.ts +++ b/src/lib/check-frontmatter-type.ts @@ -6,8 +6,8 @@ interface FrontMatter { id: string | null; organizationUrlName: string | null; slide: boolean; - postingCampaignUuid: string | null; - agreedPostingCampaignTerm: boolean; + postingCampaignUuid: string | null | undefined; + agreedPostingCampaignTerm: boolean | undefined; } interface CheckType { @@ -79,7 +79,9 @@ const checkPostingCampaignUuid: CheckType = { getMessage: () => "posting_campaign_uuidは文字列で入力してください", isValid: ({ postingCampaignUuid }) => { return ( - postingCampaignUuid === null || typeof postingCampaignUuid === "string" + postingCampaignUuid === undefined || + postingCampaignUuid === null || + typeof postingCampaignUuid === "string" ); }, }; @@ -88,7 +90,10 @@ const checkAgreedPostingCampaignTerm: CheckType = { getMessage: () => "agreed_posting_campaign_termの設定はtrue/falseで入力してください", isValid: ({ agreedPostingCampaignTerm }) => { - return typeof agreedPostingCampaignTerm === "boolean"; + return ( + agreedPostingCampaignTerm === undefined || + typeof agreedPostingCampaignTerm === "boolean" + ); }, }; diff --git a/src/lib/entities/qiita-item.ts b/src/lib/entities/qiita-item.ts index 5eda6b4a..100aad71 100644 --- a/src/lib/entities/qiita-item.ts +++ b/src/lib/entities/qiita-item.ts @@ -16,8 +16,8 @@ export class QiitaItem { public readonly itemPath: string; public readonly slide: boolean; public readonly ignorePublish: boolean; - public readonly postingCampaignUuid: string | null; - public readonly agreedPostingCampaignTerm: boolean; + public readonly postingCampaignUuid: string | null | undefined; + public readonly agreedPostingCampaignTerm: boolean | undefined; constructor({ id, @@ -53,8 +53,8 @@ export class QiitaItem { itemPath: string; slide: boolean; ignorePublish: boolean; - postingCampaignUuid: string | null; - agreedPostingCampaignTerm: boolean; + postingCampaignUuid: string | null | undefined; + agreedPostingCampaignTerm: boolean | undefined; }) { this.id = id; this.title = title; diff --git a/src/lib/file-system-repo.test.ts b/src/lib/file-system-repo.test.ts index c77ce48a..338624c9 100644 --- a/src/lib/file-system-repo.test.ts +++ b/src/lib/file-system-repo.test.ts @@ -140,6 +140,95 @@ tags: [] }); }); }); + + describe("when posting campaign keys are absent in frontmatter", () => { + it("keeps them undefined", () => { + const instance = new FileSystemRepo({ dataRootDir: "data_root_dir" }); + const basename = "abc"; + + const mockFs = fs as jest.Mocked; + mockFs.readdir.mockResolvedValueOnce([`${basename}.md`] as any[]); + mockFs.readFile.mockResolvedValue(`--- +id: this_is_id +tags: [] +---`); + + return instance.loadItemByBasename(basename).then((item) => { + expect(item?.postingCampaignUuid).toBeUndefined(); + expect(item?.agreedPostingCampaignTerm).toBeUndefined(); + }); + }); + + it("does not detect modification against remote data with default values", () => { + const instance = new FileSystemRepo({ dataRootDir: "data_root_dir" }); + const basename = "abc"; + + const mockFs = fs as jest.Mocked; + mockFs.readdir.mockResolvedValueOnce([`${basename}.md`] as any[]); + mockFs.readFile.mockResolvedValueOnce(`--- +title: title +tags: [] +private: false +id: this_is_id +slide: false +--- +body`); + mockFs.readFile.mockResolvedValueOnce(`--- +title: title +tags: [] +private: false +id: this_is_id +slide: false +posting_campaign_uuid: null +agreed_posting_campaign_term: false +--- +body`); + + return instance.loadItemByBasename(basename).then((item) => { + expect(item?.modified).toBe(false); + }); + }); + }); + + describe("when posting campaign keys are explicitly set in frontmatter", () => { + it("returns the explicit values", () => { + const instance = new FileSystemRepo({ dataRootDir: "data_root_dir" }); + const basename = "abc"; + + const mockFs = fs as jest.Mocked; + mockFs.readdir.mockResolvedValueOnce([`${basename}.md`] as any[]); + mockFs.readFile.mockResolvedValue(`--- +id: this_is_id +tags: [] +posting_campaign_uuid: abcde12345fghij67890 +agreed_posting_campaign_term: true +---`); + + return instance.loadItemByBasename(basename).then((item) => { + expect(item?.postingCampaignUuid).toBe("abcde12345fghij67890"); + expect(item?.agreedPostingCampaignTerm).toBe(true); + }); + }); + + it("returns null for an explicit null", () => { + const instance = new FileSystemRepo({ dataRootDir: "data_root_dir" }); + const basename = "abc"; + + const mockFs = fs as jest.Mocked; + mockFs.readdir.mockResolvedValueOnce([`${basename}.md`] as any[]); + mockFs.readFile.mockResolvedValue(`--- +id: this_is_id +tags: [] +posting_campaign_uuid: null +agreed_posting_campaign_term: false +---`); + + return instance.loadItemByBasename(basename).then((item) => { + expect(item?.postingCampaignUuid).toBeNull(); + expect(item?.agreedPostingCampaignTerm).toBe(false); + }); + }); + }); }); describe("loadItems", () => { diff --git a/src/lib/file-system-repo.ts b/src/lib/file-system-repo.ts index a686c19a..932ec000 100644 --- a/src/lib/file-system-repo.ts +++ b/src/lib/file-system-repo.ts @@ -15,8 +15,8 @@ class FileContent { public readonly rawBody: string; public readonly slide: boolean; public readonly ignorePublish: boolean; - public readonly postingCampaignUuid: string | null; - public readonly agreedPostingCampaignTerm: boolean; + public readonly postingCampaignUuid: string | null | undefined; + public readonly agreedPostingCampaignTerm: boolean | undefined; constructor({ title, @@ -40,8 +40,8 @@ class FileContent { rawBody: string; slide: boolean; ignorePublish: boolean; - postingCampaignUuid: string | null; - agreedPostingCampaignTerm: boolean; + postingCampaignUuid: string | null | undefined; + agreedPostingCampaignTerm: boolean | undefined; }) { this.title = title; this.tags = tags; @@ -68,8 +68,8 @@ class FileContent { organizationUrlName: data.organization_url_name, slide: data.slide, ignorePublish: data.ignorePublish ?? false, - postingCampaignUuid: data.posting_campaign_uuid ?? null, - agreedPostingCampaignTerm: data.agreed_posting_campaign_term ?? false, + postingCampaignUuid: data.posting_campaign_uuid, + agreedPostingCampaignTerm: data.agreed_posting_campaign_term, }); } @@ -138,8 +138,12 @@ class FileContent { organization_url_name: this.organizationUrlName, slide: this.slide, ignorePublish: this.ignorePublish, - posting_campaign_uuid: this.postingCampaignUuid, - agreed_posting_campaign_term: this.agreedPostingCampaignTerm, + ...(this.postingCampaignUuid !== undefined && { + posting_campaign_uuid: this.postingCampaignUuid, + }), + ...(this.agreedPostingCampaignTerm !== undefined && { + agreed_posting_campaign_term: this.agreedPostingCampaignTerm, + }), }); } @@ -161,8 +165,10 @@ class FileContent { this.rawBody === aFileContent.rawBody && this.slide === aFileContent.slide && this.ignorePublish === aFileContent.ignorePublish && - this.postingCampaignUuid === aFileContent.postingCampaignUuid && - this.agreedPostingCampaignTerm === aFileContent.agreedPostingCampaignTerm + (this.postingCampaignUuid ?? null) === + (aFileContent.postingCampaignUuid ?? null) && + (this.agreedPostingCampaignTerm ?? false) === + (aFileContent.agreedPostingCampaignTerm ?? false) ); } diff --git a/src/lib/validators/item-validator.ts b/src/lib/validators/item-validator.ts index 19ed25ab..ceb24ec2 100644 --- a/src/lib/validators/item-validator.ts +++ b/src/lib/validators/item-validator.ts @@ -4,8 +4,8 @@ interface Item { tags: string[]; secret: boolean; organizationUrlName: string | null; - postingCampaignUuid: string | null; - agreedPostingCampaignTerm: boolean; + postingCampaignUuid: string | null | undefined; + agreedPostingCampaignTerm: boolean | undefined; } interface Validator { diff --git a/src/qiita-api/index.ts b/src/qiita-api/index.ts index 83392f30..0f9b67cf 100644 --- a/src/qiita-api/index.ts +++ b/src/qiita-api/index.ts @@ -227,8 +227,8 @@ export class QiitaApi { isPrivate: boolean; organizationUrlName: string | null; slide: boolean; - postingCampaignUuid: string | null; - agreedPostingCampaignTerm: boolean; + postingCampaignUuid: string | null | undefined; + agreedPostingCampaignTerm: boolean | undefined; }) { const data = JSON.stringify({ body: rawBody, @@ -271,8 +271,8 @@ export class QiitaApi { isPrivate: boolean; organizationUrlName: string | null; slide: boolean; - postingCampaignUuid: string | null; - agreedPostingCampaignTerm: boolean; + postingCampaignUuid: string | null | undefined; + agreedPostingCampaignTerm: boolean | undefined; }) { const data = JSON.stringify({ body: rawBody, diff --git a/src/server/api/items.ts b/src/server/api/items.ts index 66316c75..9efa10a4 100644 --- a/src/server/api/items.ts +++ b/src/server/api/items.ts @@ -100,7 +100,7 @@ const itemsShow = async (req: Express.Request, res: Express.Response) => { ? { title: campaign.title, link_url: campaign.link_url, - agreed: item.agreedPostingCampaignTerm, + agreed: item.agreedPostingCampaignTerm ?? false, } : null;