From 6b57a34c0c213acd0a9001c85b5741e0162c87e9 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Fri, 8 May 2026 16:33:14 +0200 Subject: [PATCH] feat(marketing-pages): subscribe to auth.user.signed-up Generated handler + Payload event-task via gen event consume, threaded through symbols / both binders / cms re-export at the configured anchors. bus.subscribe wires the in-memory delivery in dev-seed; the __events.auth.user.signed-up.marketing-pages Payload task closes the production-bus loop. Also fixes two generator-level issues found during Phase 8: - Drop publisher prompt's `when` clause so --args can supply the 4th positional argument (Plop limitation: --args cannot bypass conditional prompts). Validate runs only in consume mode. - Switch event-task.ts.hbs from TaskConfig<"slug-string"> to TaskConfig<{ input: ...; output: object }> since runtime-generated event slugs are not keys of TypedJobs['tasks']. --- packages/marketing-pages/package.json | 1 + .../marketing-pages/src/di/bind-dev-seed.ts | 21 +++++++++++++++ .../marketing-pages/src/di/bind-production.ts | 21 +++++++++++++++ packages/marketing-pages/src/di/symbols.ts | 1 + .../on-auth-user-signed-up.handler.test.ts | 15 +++++++++++ .../on-auth-user-signed-up.handler.ts | 14 ++++++++++ .../src/integrations/cms/index.ts | 1 + .../jobs/__events-auth-user-signed-up.task.ts | 26 +++++++++++++++++++ pnpm-lock.yaml | 3 +++ turbo/generators/config.ts | 11 ++++---- .../templates/event/consume/event-task.ts.hbs | 7 +++-- 11 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.test.ts create mode 100644 packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.ts create mode 100644 packages/marketing-pages/src/integrations/cms/jobs/__events-auth-user-signed-up.task.ts diff --git a/packages/marketing-pages/package.json b/packages/marketing-pages/package.json index 5bacd49..ce7c7da 100644 --- a/packages/marketing-pages/package.json +++ b/packages/marketing-pages/package.json @@ -18,6 +18,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@repo/auth": "workspace:*", "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", diff --git a/packages/marketing-pages/src/di/bind-dev-seed.ts b/packages/marketing-pages/src/di/bind-dev-seed.ts index 3ea9d75..a25c7fa 100644 --- a/packages/marketing-pages/src/di/bind-dev-seed.ts +++ b/packages/marketing-pages/src/di/bind-dev-seed.ts @@ -17,6 +17,8 @@ import { getSiteSettingsController } from "../interface-adapters/controllers/get import { getPageBySlugController } from "../interface-adapters/controllers/get-page-by-slug.controller.js"; import type { IPagesRepository } from "../application/repositories/pages.repository.interface.js"; import type { ISiteSettingsRepository } from "../application/repositories/site-settings.repository.interface.js"; +import { userSignedUpEvent } from "@repo/auth"; +import { onAuthUserSignedUpHandler } from "../events/handlers/on-auth-user-signed-up.handler.js"; /** * Replace the default empty mocks with populated ones for dev mode + storybook. @@ -132,5 +134,24 @@ export async function bindDevSeedMarketingPages( void bus; void queue; // + // onAuthUserSignedUpHandler subscription — generated, edit the handler file (not this block) for behavior. + const wrappedAuthUserSignedUp = withSpan( + tracer, + { name: "marketing-pages.onAuthUserSignedUpHandler", op: "event-handler" }, + withCapture( + logger, + { + feature: "marketing-pages", + layer: "event-handler", + name: "marketing-pages.onAuthUserSignedUpHandler", + }, + onAuthUserSignedUpHandler(), + ), + ); + if (marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler)) { + marketingPagesContainer.unbind(MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler); + } + marketingPagesContainer.bind(MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler).toConstantValue(wrappedAuthUserSignedUp); + bus.subscribe(userSignedUpEvent, "marketing-pages", wrappedAuthUserSignedUp); // } diff --git a/packages/marketing-pages/src/di/bind-production.ts b/packages/marketing-pages/src/di/bind-production.ts index 598e4b2..add8704 100644 --- a/packages/marketing-pages/src/di/bind-production.ts +++ b/packages/marketing-pages/src/di/bind-production.ts @@ -16,6 +16,8 @@ import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settin import { getPageBySlugUseCase } from "../application/use-cases/get-page-by-slug.use-case"; import { getSiteSettingsController } from "../interface-adapters/controllers/get-site-settings.controller"; import { getPageBySlugController } from "../interface-adapters/controllers/get-page-by-slug.controller"; +import { userSignedUpEvent } from "@repo/auth"; +import { onAuthUserSignedUpHandler } from "../events/handlers/on-auth-user-signed-up.handler"; export function bindProductionMarketingPages( config: SanitizedConfig, @@ -122,5 +124,24 @@ export function bindProductionMarketingPages( void bus; void queue; // + // onAuthUserSignedUpHandler subscription — generated, edit the handler file (not this block) for behavior. + const wrappedAuthUserSignedUp = withSpan( + tracer, + { name: "marketing-pages.onAuthUserSignedUpHandler", op: "event-handler" }, + withCapture( + logger, + { + feature: "marketing-pages", + layer: "event-handler", + name: "marketing-pages.onAuthUserSignedUpHandler", + }, + onAuthUserSignedUpHandler(), + ), + ); + if (marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler)) { + marketingPagesContainer.unbind(MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler); + } + marketingPagesContainer.bind(MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler).toConstantValue(wrappedAuthUserSignedUp); + bus.subscribe(userSignedUpEvent, "marketing-pages", wrappedAuthUserSignedUp); // } diff --git a/packages/marketing-pages/src/di/symbols.ts b/packages/marketing-pages/src/di/symbols.ts index caf5e14..795a6d2 100644 --- a/packages/marketing-pages/src/di/symbols.ts +++ b/packages/marketing-pages/src/di/symbols.ts @@ -9,5 +9,6 @@ export const MARKETING_PAGES_SYMBOLS = { IGetPageBySlugController: Symbol.for("marketing-pages:IGetPageBySlugController"), IGetSiteSettingsController: Symbol.for("marketing-pages:IGetSiteSettingsController"), // + IOnAuthUserSignedUpHandler: Symbol.for("@repo/marketing-pages/onAuthUserSignedUp"), // } as const; diff --git a/packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.test.ts b/packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.test.ts new file mode 100644 index 0000000..1eab3d2 --- /dev/null +++ b/packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.test.ts @@ -0,0 +1,15 @@ +// packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.test.ts +import { describe, it, expect } from "vitest"; +import { onAuthUserSignedUpHandler } from "@/events/handlers/on-auth-user-signed-up.handler"; + +describe("onAuthUserSignedUpHandler", () => { + it("returns a function (factory shape)", () => { + const handler = onAuthUserSignedUpHandler(); + expect(typeof handler).toBe("function"); + }); + + it("does not throw on a valid stub event", async () => { + const handler = onAuthUserSignedUpHandler(); + await expect(handler({} as never)).resolves.toBeUndefined(); + }); +}); diff --git a/packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.ts b/packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.ts new file mode 100644 index 0000000..ec16c5f --- /dev/null +++ b/packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.ts @@ -0,0 +1,14 @@ +// packages/marketing-pages/src/events/handlers/on-auth-user-signed-up.handler.ts +import type { UserSignedUpEvent } from "@repo/auth"; + +export type IOnAuthUserSignedUpHandler = ReturnType< + typeof onAuthUserSignedUpHandler +>; + +export const onAuthUserSignedUpHandler = + () => + async (_event: UserSignedUpEvent): Promise => { + // TODO: implement the reaction. Inject dependencies via the factory's + // constructor and use them here. The handler runs inside the consumer's + // span+capture sandwich, so just throwing on failure is the right shape. + }; diff --git a/packages/marketing-pages/src/integrations/cms/index.ts b/packages/marketing-pages/src/integrations/cms/index.ts index 5f031b6..4b8b87e 100644 --- a/packages/marketing-pages/src/integrations/cms/index.ts +++ b/packages/marketing-pages/src/integrations/cms/index.ts @@ -1,3 +1,4 @@ export { pages } from "./collections/pages"; export { siteSettings } from "./globals/site-settings"; // +export { onAuthUserSignedUpEventTask } from "./jobs/__events-auth-user-signed-up.task"; diff --git a/packages/marketing-pages/src/integrations/cms/jobs/__events-auth-user-signed-up.task.ts b/packages/marketing-pages/src/integrations/cms/jobs/__events-auth-user-signed-up.task.ts new file mode 100644 index 0000000..d1fb1db --- /dev/null +++ b/packages/marketing-pages/src/integrations/cms/jobs/__events-auth-user-signed-up.task.ts @@ -0,0 +1,26 @@ +// packages/marketing-pages/src/integrations/cms/jobs/__events-auth-user-signed-up.task.ts +// Generated by `gen event consume`. The PayloadJobsEventBus enqueues this task +// when auth publishes `auth.user.signed-up`. The handler +// resolves the consumer's wrapped event handler from the per-feature container +// and invokes it with the typed payload. +import type { TaskConfig } from "payload"; +import { marketingPagesContainer } from "../../../di/container"; +import { MARKETING_PAGES_SYMBOLS } from "../../../di/symbols"; +import type { IOnAuthUserSignedUpHandler } from "../../../events/handlers/on-auth-user-signed-up.handler"; +import type { UserSignedUpEvent } from "@repo/auth"; + +export const onAuthUserSignedUpEventTask: TaskConfig<{ + input: UserSignedUpEvent; + output: object; +}> = { + slug: "__events.auth.user.signed-up.marketing-pages", + inputSchema: [], + retries: { attempts: 3, backoff: { type: "exponential", delay: 1000 } }, + handler: async ({ input }) => { + const handler = marketingPagesContainer.get( + MARKETING_PAGES_SYMBOLS.IOnAuthUserSignedUpHandler, + ); + await handler(input); + return { output: {} }; + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e7ed3b4..4df1f24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -775,6 +775,9 @@ importers: packages/marketing-pages: dependencies: + '@repo/auth': + specifier: workspace:* + version: link:../auth '@repo/core-events': specifier: workspace:* version: link:../core-events diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts index fe7f827..74abd8e 100644 --- a/turbo/generators/config.ts +++ b/turbo/generators/config.ts @@ -378,11 +378,12 @@ export default function generator(plop: PlopTypes.NodePlopAPI): void { { type: "input", name: "publisher", - message: "Publisher feature (kebab-case; only for consume mode):", - when(answers: { mode: string }) { - return answers.mode === "consume"; - }, - validate(input: string, answers: { event: string }) { + message: + "Publisher feature (kebab-case; required for consume, ignored for publish):", + // Always shown — `--args` cannot bypass conditional prompts (Plop limitation). + // Publish mode accepts any value (it's ignored by publishActions). + validate(input: string, answers: { mode: string; event: string }) { + if (answers.mode === "publish") return true; if (!/^[a-z][a-z0-9-]*$/.test(input)) return "Must be kebab-case"; const eventKebab = answers.event.replace(/\./g, "-"); const path = join( diff --git a/turbo/generators/templates/event/consume/event-task.ts.hbs b/turbo/generators/templates/event/consume/event-task.ts.hbs index 8c75264..101fc47 100644 --- a/turbo/generators/templates/event/consume/event-task.ts.hbs +++ b/turbo/generators/templates/event/consume/event-task.ts.hbs @@ -9,7 +9,10 @@ import { {{constantCase feature}}_SYMBOLS } from "../../../di/symbols"; import type { IOn{{pascalCase publisher}}{{pascalCase event}}Handler } from "../../../events/handlers/on-{{kebabCase publisher}}-{{kebabCase event}}.handler"; import type { {{pascalCase event}}Event } from "@repo/{{kebabCase publisher}}"; -export const on{{pascalCase publisher}}{{pascalCase event}}EventTask: TaskConfig<"__events.{{kebabCase publisher}}.{{event}}.{{kebabCase feature}}"> = { +export const on{{pascalCase publisher}}{{pascalCase event}}EventTask: TaskConfig<{ + input: {{pascalCase event}}Event; + output: object; +}> = { slug: "__events.{{kebabCase publisher}}.{{event}}.{{kebabCase feature}}", inputSchema: [], retries: { attempts: 3, backoff: { type: "exponential", delay: 1000 } }, @@ -17,7 +20,7 @@ export const on{{pascalCase publisher}}{{pascalCase event}}EventTask: TaskConfig const handler = {{camelCase feature}}Container.get( {{constantCase feature}}_SYMBOLS.IOn{{pascalCase publisher}}{{pascalCase event}}Handler, ); - await handler(input as {{pascalCase event}}Event); + await handler(input); return { output: {} }; }, };