diff --git a/.changeset/notification-subscription-expansion-not-wired.md b/.changeset/notification-subscription-expansion-not-wired.md new file mode 100644 index 0000000000..e0f34c5634 --- /dev/null +++ b/.changeset/notification-subscription-expansion-not-wired.md @@ -0,0 +1,24 @@ +--- +"@objectstack/service-messaging": patch +--- + +docs(service-messaging): mark the `sys_notification_subscription` expansion not-yet-wired and align the `principal` description with the resolver (#9807) + +The object header described a live routing control that does not exist: "where a +producer emits with `audience: 'subscribers'` … the resolver expands the topic's +subscriptions into recipients". Nothing implements that. `AudienceSpec` +(`messaging-service.ts`) has no `'subscribers'` member, `EmitInput.audience` is +required, and `RecipientResolver` has no branch that reads +`sys_notification_subscription` — the literal `'subscribers'` occurs exactly once +in the repo, in that sentence. Every delivery today comes from the explicit +`audience` a producer passes to `emit()`, so the Setup "Notification +Subscriptions" grid is admin-authored data, not a live routing control. The +header now says so, per the maintainer ruling on #9807 (annotate now; the +ADR-0030 Layer-3 expansion stays future work, deferred on measured zero pull). + +The `principal` field description shipped a four-form list (`'role:x'` | +`'team:x'` | `'user:id'` | bare user id) narrower than what +`RecipientResolver.resolveOne()` accepts for the same string shape; it now also +names `'owner_of:object:id'` and the email form (matched against `sys_user`). +This is a user-visible string: it ships into `dist/` and into the generated `en` +translation bundle as the field's help text in the Setup grid. diff --git a/packages/services/service-messaging/src/objects/notification-subscription.object.ts b/packages/services/service-messaging/src/objects/notification-subscription.object.ts index 642d31030b..e02bb04680 100644 --- a/packages/services/service-messaging/src/objects/notification-subscription.object.ts +++ b/packages/services/service-messaging/src/objects/notification-subscription.object.ts @@ -6,11 +6,18 @@ import { ObjectSchema, Field } from '@objectstack/spec/data'; * `sys_notification_subscription` — who is subscribed to a topic (ADR-0030 * Layer 3). * - * Declares standing interest in a `topic` by a `principal` (`role:x`, `team:x`, - * `user:id`, or a bare user id). Where a producer emits with `audience: - * 'subscribers'` (or no explicit audience), the resolver expands the topic's - * subscriptions into recipients — the opt-in counterpart to the explicit - * audience most producers pass today. + * Declares standing interest in a `topic` by a `principal` (see that field for + * the accepted selector forms). + * + * ⚠️ [#9807] The subscription→recipient expansion is **NOT WIRED in this + * repo**: `AudienceSpec` (`messaging-service.ts`) has no `'subscribers'` + * member, `EmitInput.audience` is REQUIRED, and no `RecipientResolver` branch + * expands a topic's subscriptions — so nothing here reads these rows at + * runtime. Every delivery today comes from the explicit `audience` a producer + * passes to `emit()`, which makes the Setup "Notification Subscriptions" grid + * admin-authored data, NOT a live routing control. The ADR-0030 Layer-3 + * expansion is future work, deferred on measured zero pull; ADR-0012/0030 + * describe that target state, not today's behaviour. * * Distinct from `sys_notification_preference`: a subscription says "include me * for this topic"; a preference says "but mute it on this channel". @@ -45,7 +52,13 @@ export const NotificationSubscription = ObjectSchema.create({ label: 'Principal', required: true, searchable: true, - description: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | bare user id.", + // [#9807] Kept in step with what `RecipientResolver.resolveOne()` really + // accepts for a string spec, so this does not under-describe the day the + // expansion above is wired: an email-shaped value is matched against + // `sys_user` (kept verbatim when no user matches), and anything otherwise + // unrecognized falls through as a bare user id. + description: + "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | 'owner_of:object:id' | an email | a bare user id.", }), enabled: Field.boolean({ diff --git a/packages/services/service-messaging/src/translations/en.objects.generated.ts b/packages/services/service-messaging/src/translations/en.objects.generated.ts index ac3330d9a0..4f4da6f80f 100644 --- a/packages/services/service-messaging/src/translations/en.objects.generated.ts +++ b/packages/services/service-messaging/src/translations/en.objects.generated.ts @@ -238,7 +238,7 @@ export const enObjects: NonNullable = { }, principal: { label: "Principal", - help: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | bare user id." + help: "Subscriber selector: 'role:x' | 'team:x' | 'user:id' | 'owner_of:object:id' | an email | a bare user id." }, enabled: { label: "Enabled",