From 26471d8b0ec717a91ad657053c095f46e44801dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20FAMIBELLE-PRONZOLA?= Date: Sun, 5 Jul 2026 00:59:59 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20d=C3=A9placer=20le=20miroir=20bokante=20?= =?UTF-8?q?de=20beforeUpdate=20vers=20afterCreate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avec draftAndPublish sur Strapi 5, publier une entrée crée une nouvelle ligne (document-service publishEntry -> createEntry) au lieu de mettre à jour la ligne existante : beforeUpdate ne se déclenche donc jamais sur une vraie action Publier, seulement sur l'édition d'une entrée déjà publiée. Le miroir bokante est maintenant posté dans afterCreate, sur la condition result.publishedAt, avec synchronisation brouillon/publié pour rester idempotent à travers les cycles dépublier/republier. --- .../parole/__tests__/lifecycles.test.js | 76 +++++++++++-------- .../parole/content-types/parole/lifecycles.js | 32 +++++--- 2 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js index 10b2c3a..e6252d1 100644 --- a/src/api/parole/content-types/parole/__tests__/lifecycles.test.js +++ b/src/api/parole/content-types/parole/__tests__/lifecycles.test.js @@ -118,14 +118,14 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => { }); }); -describe('beforeUpdate — publication miroir bokante', () => { +describe('afterCreate — publication miroir bokante', () => { const originalEnv = {...process.env}; const bokanteMastodon = require('../../../../../utils/bokante-mastodon'); const originalCreateStatus = bokanteMastodon.createStatus; - function buildStrapi(previous) { + function buildStrapi() { const dbQuery = { - findOne: vi.fn(async () => previous), + findOne: vi.fn(async () => null), updateMany: vi.fn() }; @@ -136,11 +136,16 @@ describe('beforeUpdate — publication miroir bokante', () => { }; } - function buildEvent(overrides = {}) { + function buildEvent(resultOverrides = {}) { return { - state: {}, - params: { - data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides} + params: {data: {titre: 'Mon titre'}}, + result: { + documentId: 'doc-1', + titre: 'Mon titre', + slug: 'mon-titre', + publishedAt: '2026-07-04T00:00:00.000Z', + bokanteStatusId: null, + ...resultOverrides } }; } @@ -151,33 +156,43 @@ describe('beforeUpdate — publication miroir bokante', () => { delete global.strapi; }); - it('publie un statut miroir et stocke bokanteStatusId sur data', async () => { + it('publie un statut miroir et synchronise bokanteStatusId sur le documentId', async () => { process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; bokanteMastodon.createStatus = vi.fn(async () => ({id: '112233'})); - const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null}; - const strapiMock = buildStrapi(previous); + const strapiMock = buildStrapi(); + const {afterCreate} = await loadLifecycles(strapiMock); - const {beforeUpdate} = await loadLifecycles(strapiMock); - const event = buildEvent(); - - await beforeUpdate(event); + await afterCreate(buildEvent()); expect(bokanteMastodon.createStatus).toHaveBeenCalledTimes(1); expect(bokanteMastodon.createStatus.mock.calls[0][0]).toContain('Mon titre'); - expect(event.params.data.bokanteStatusId).toBe('112233'); + expect(strapiMock.db.query('api::parole.parole').updateMany).toHaveBeenCalledWith({ + where: {documentId: 'doc-1'}, + data: {bokanteStatusId: '112233'} + }); + }); + + it('ne publie rien si la ligne créée n\'est pas publiée (simple brouillon)', async () => { + process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; + bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'})); + + const strapiMock = buildStrapi(); + const {afterCreate} = await loadLifecycles(strapiMock); + + await afterCreate(buildEvent({publishedAt: null})); + + expect(bokanteMastodon.createStatus).not.toHaveBeenCalled(); }); it('ne republie pas si bokanteStatusId existe déjà', async () => { process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'})); - const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: '112233'}; - const strapiMock = buildStrapi(previous); + const strapiMock = buildStrapi(); + const {afterCreate} = await loadLifecycles(strapiMock); - const {beforeUpdate} = await loadLifecycles(strapiMock); - - await beforeUpdate(buildEvent()); + await afterCreate(buildEvent({bokanteStatusId: '112233'})); expect(bokanteMastodon.createStatus).not.toHaveBeenCalled(); }); @@ -186,32 +201,27 @@ describe('beforeUpdate — publication miroir bokante', () => { delete process.env.BOKANTE_ACCESS_TOKEN; bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'})); - const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null}; - const strapiMock = buildStrapi(previous); + const strapiMock = buildStrapi(); + const {afterCreate} = await loadLifecycles(strapiMock); - const {beforeUpdate} = await loadLifecycles(strapiMock); - - await beforeUpdate(buildEvent()); + await afterCreate(buildEvent()); expect(bokanteMastodon.createStatus).not.toHaveBeenCalled(); }); - it('n\'interrompt pas la publication quand bokante échoue', async () => { + it('n\'interrompt pas la création quand bokante échoue', async () => { process.env.BOKANTE_ACCESS_TOKEN = 'fake-token'; bokanteMastodon.createStatus = vi.fn(async () => { throw new Error('boom'); }); - const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null}; - const strapiMock = buildStrapi(previous); + const strapiMock = buildStrapi(); + const {afterCreate} = await loadLifecycles(strapiMock); - const {beforeUpdate} = await loadLifecycles(strapiMock); - const event = buildEvent(); - - await expect(beforeUpdate(event)).resolves.not.toThrow(); + await expect(afterCreate(buildEvent())).resolves.not.toThrow(); expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('bokante')); - expect(event.params.data.bokanteStatusId).toBeUndefined(); + expect(strapiMock.db.query('api::parole.parole').updateMany).not.toHaveBeenCalled(); }); }); diff --git a/src/api/parole/content-types/parole/lifecycles.js b/src/api/parole/content-types/parole/lifecycles.js index 0d5abc5..3718327 100644 --- a/src/api/parole/content-types/parole/lifecycles.js +++ b/src/api/parole/content-types/parole/lifecycles.js @@ -192,17 +192,6 @@ module.exports = { const previousPublishedAt = previousData.publishedAt; const currentPublished_at = data.publishedAt; if (currentPublished_at != previousPublishedAt) { - if (!previousData.bokanteStatusId && process.env.BOKANTE_ACCESS_TOKEN) { - try { - const status = await bokanteMastodon.createStatus( - `"${previousData.titre}" — nouvelle parole sur pawol.nu\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}` - ); - data.bokanteStatusId = status.id; - } catch (err) { - strapi.log.error(`Publication bokante : ${err.message}`); - } - } - const message = `Nouvelle publication ❤️ \n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`; if (previousData.user) { @@ -284,6 +273,27 @@ module.exports = { }, afterCreate: async event => { const {data} = event.params; + + // Avec draftAndPublish, "publier" crée une nouvelle ligne (la version publiée) + // au lieu de mettre à jour la ligne existante : c'est ici, et non dans + // beforeUpdate, qu'un événement de publication est détectable. + if (event.result?.publishedAt && !event.result?.bokanteStatusId && process.env.BOKANTE_ACCESS_TOKEN) { + try { + const status = await bokanteMastodon.createStatus( + `"${event.result.titre}" — nouvelle parole sur pawol.nu\n${process.env.WEBSITE_URL}/paroles/${event.result.slug}` + ); + // Synchronise brouillon et version publiée pour éviter une republication + // en double au prochain cycle dépublier/republier (qui recrée la ligne + // publiée à partir du brouillon). + await strapi.db.query('api::parole.parole').updateMany({ + where: {documentId: event.result.documentId}, + data: {bokanteStatusId: status.id} + }); + } catch (err) { + strapi.log.error(`Publication bokante : ${err.message}`); + } + } + const user = await jwennUserEpiId(data?.user?.id); const userAdmin = await jwennUserAdminEpiId(data?.createdBy); const superAdmin = await jwennSuperAdminEpiId(data?.createdBy);