Skip to content

feat(#272): implement Azure Functions queue trigger handler registration#289

Open
nnoce14 wants to merge 19 commits into
mainfrom
feat/issue-272-queue-trigger-handler
Open

feat(#272): implement Azure Functions queue trigger handler registration#289
nnoce14 wants to merge 19 commits into
mainfrom
feat/issue-272-queue-trigger-handler

Conversation

@nnoce14

@nnoce14 nnoce14 commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Closes #272

Implements Azure Functions Storage Queue trigger handler registration in the Cellix framework, following the same patterns as HTTP handler registration.

Changes

New Package: @ocom/handler-queue-community-update

  • Queue trigger handler for community update messages
  • System-scoped application services (no user request context)

@apps/api — Cellix class (cellix.ts)

  • Added registerAzureFunctionQueueHandler() mirroring registerAzureFunctionHttpHandler()
  • Wires @ocom/handler-queue-community-update as the first queue trigger

@ocom/application-services

  • Added forSystem() to the AppServicesHost interface
  • Implemented forSystem() — constructs a system passport with minimal permissions scoped to community settings management (no user/request context)

@ocom/service-queue-storage

  • Added community-update inbound queue schema and registry entry

Package naming convention

New package follows the @ocom/handler-{trigger-type}-{purpose} convention established here:

  • @ocom/handler-queue-community-update (this PR)
  • Future: @ocom/handler-http-graphql, @ocom/handler-http-rest (rename of existing packages)

Test coverage

  • 117 tests passing (99 api + 18 handler)
  • 0 TypeScript errors
  • Biome clean

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Summary by Sourcery

Add first-class support for Azure Storage Queue-triggered functions in the Cellix framework and wire up a community settings update queue handler into the API app.

New Features:

  • Introduce a generic Azure Functions storage queue trigger registration API in Cellix alongside existing HTTP handler registration.
  • Add a new @ocom/handler-queue-community-update package that validates community update messages from the community-update queue and applies settings changes via application services.
  • Expose system-scoped application services creation via forSystem() to support queue-triggered, non-request workflows.
  • Register the community-update inbound queue schema and logging configuration, including nested payload logging support, in the queue storage service registry.

Enhancements:

  • Extend queue logging field helpers to support nested payload paths with type-safe proxies and dotted-path resolution.
  • Refine community settings update errors into a dedicated CommunityNotFoundError for clearer handling in background processing.
  • Add a shared helper for extracting queue trigger metadata from Azure Functions context and reuse it across handlers.
  • Update Cellix docs and tests to cover queue handler registration, lifecycle behavior, and phase validation, and broaden Domain passport exports for permission typing.
  • Include the new handler package in build/test configs and adjust minor formatting and dependency overrides across the repo.

Build:

  • Create build/test configuration for the new @ocom/handler-queue-community-update package and wire it into the workspace and TypeScript project references.

Documentation:

  • Document the Cellix queue handler registration pattern and provide HTTP/queue handler examples in the API instructions, plus usage docs for the new community update queue handler package.

Tests:

  • Add unit and feature tests for queue handler registration in Cellix, for community update queue handler behavior and error handling, and for queue trigger metadata extraction and nested payload logging resolution.

Chores:

  • Update Node.js version used in the docs deployment pipeline and tweak pnpm workspace overrides and formatting in existing tests.

- Add registerAzureFunctionQueueHandler() to Cellix class for Storage Queue triggers
- Add applicationServicesFactory.forSystem() for system-scoped passport (no user request context)
- Create @ocom/handler-queue-community-update package with queue trigger handler
- Add community-update queue schema to @ocom/service-queue-storage
- Wire @ocom/handler-queue-community-update into @apps/api

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nnoce14 nnoce14 requested a review from a team June 30, 2026 17:03
@nnoce14 nnoce14 requested a review from a team as a code owner June 30, 2026 17:03
@sourcery-ai

sourcery-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds first-class Azure Storage Queue trigger support to the Cellix Azure Functions integration and wires up a new community-update queue handler end-to-end, including system-scoped application services, queue schema/registry, logging utilities, and tests/docs updates.

Sequence diagram for community-update queue message processing

sequenceDiagram
    participant Queue as AzureStorageQueue
    participant AzureFn as AzureFunctionsHost
    participant Cellix as Cellix
    participant QueueHandler as communityUpdateQueueHandler
    participant QueueSvc as ServiceQueueStorage
    participant AppFactory as ApplicationServicesFactory
    participant CommunityApp as Community.updateSettings

    Queue->>AzureFn: Enqueue CommunityUpdatePayload
    AzureFn->>Cellix: app.storageQueue(communityUpdateQueueName, handler)
    Note over Cellix,QueueHandler: handler wrapped via deferHandlerCreation
    AzureFn->>QueueHandler: invoke(queueEntry, context)
    QueueHandler->>QueueSvc: receiveFromCommunityUpdateQueue(queueEntry, extractQueueTriggerMetadata(context.triggerMetadata))
    QueueSvc-->>QueueHandler: { id, payload }
    QueueHandler->>AppFactory: forSystem({ canManageCommunitySettings: true })
    AppFactory-->>QueueHandler: appServices
    QueueHandler->>CommunityApp: updateSettings({ id: communityId, ...partialFields })
    CommunityApp-->>QueueHandler: void
    QueueHandler-->>AzureFn: complete
Loading

File-Level Changes

Change Details Files
Extend Cellix to support both HTTP and Azure Storage Queue handlers with a shared lifecycle and system-scoped application services.
  • Generalize Cellix generics and RequestScopedHost/AppHost types to carry a permissions parameter used by forSystem.
  • Introduce forSystem on the request-scoped host shape and plumb it through Cellix initialization.
  • Add registerAzureFunctionQueueHandler with pendingQueueHandlers storage and validation of allowed phases.
  • Add a shared deferHandlerCreation helper used by both HTTP and queue handlers to lazily construct handlers after app services start.
  • Update setupLifecycle to register HTTP handlers via app.http and new queue handlers via app.storageQueue, plus expand tests and feature specs to cover queue registration, invalid phase, and startup wiring.
apps/api/src/cellix.ts
apps/api/src/cellix.test.ts
apps/api/src/features/cellix.feature
Introduce a dedicated community-update queue schema/registry and export queue/metadata types and helpers from the queue-storage packages.
  • Define CommunityUpdatePayload schema, queue name, and queue registration via defineQueue, including logging tags/metadata for communityId and updateType.
  • Register the new community-update inbound queue alongside existing queues and ensure it is provisioned.
  • Enhance logging-fields payload proxy to support nested payload paths with dotted-path resolution and type-safe nested key access, including helper types and runtime path reader.
  • Add extractQueueTriggerMetadata utility to normalize Azure Functions triggerMetadata into typed QueueTriggerMetadata, and export it plus QueueTriggerMetadata from public queue-storage APIs.
  • Update queue-storage tests for provisioning list, payload proxy behavior, nested resolution, and trigger metadata extraction.
packages/ocom/service-queue-storage/src/schemas/inbound/community-update.ts
packages/ocom/service-queue-storage/src/registry.ts
packages/ocom/service-queue-storage/src/service.test.ts
packages/cellix/service-queue-storage/src/logging-fields.ts
packages/cellix/service-queue-storage/src/interfaces.ts
packages/cellix/service-queue-storage/src/payload-proxy.test.ts
packages/cellix/service-queue-storage/src/trigger-metadata.ts
packages/cellix/service-queue-storage/src/trigger-metadata.test.ts
packages/cellix/service-queue-storage/src/index.ts
packages/ocom/service-queue-storage/src/index.ts
Add a new @ocom/handler-queue-community-update package providing the community-update Storage Queue handler and tests.
  • Create communityUpdateQueueHandlerCreator that wires Azure Functions StorageQueueHandler to queue-storage receiveFromCommunityUpdateQueue and application-services Community.updateSettings, including least-privilege forSystem usage and detailed error handling.
  • Add unit tests covering handler creation, metadata extraction, happy-path updateSettings calls, permission scoping, and various error paths (invalid payload, community not found, forSystem failures, and unexpected errors).
  • Set up package build/test config (tsconfig, vitest config, turbo, package.json) and documentation readme for handler behavior and usage.
packages/ocom/handler-queue-community-update/src/handler.ts
packages/ocom/handler-queue-community-update/src/index.ts
packages/ocom/handler-queue-community-update/src/handler.test.ts
packages/ocom/handler-queue-community-update/package.json
packages/ocom/handler-queue-community-update/readme.md
packages/ocom/handler-queue-community-update/tsconfig.json
packages/ocom/handler-queue-community-update/tsconfig.vitest.json
packages/ocom/handler-queue-community-update/vitest.config.ts
packages/ocom/handler-queue-community-update/turbo.json
.gitignore
Wire the new community-update queue handler into the API app bootstrap and align docs/tests to the new Cellix APIs.
  • Update apps/api bootstrap to add Domain dependency, pass PermissionsSpec type parameter to Cellix, and register the community-update queue handler via registerAzureFunctionQueueHandler using communityUpdateQueueName and ServiceQueueStorage.
  • Mock registerAzureFunctionQueueHandler and new handler/queue exports in index.test.ts and verify wiring.
  • Expand API instructions to clarify infrastructure vs HTTP vs queue handler registration, provide HTTP and queue handler examples, and add guidance on system-scoped forSystem usage and queue logging/error-handling semantics.
apps/api/src/index.ts
apps/api/src/index.test.ts
apps/api/.github/instructions/api.instructions.md
apps/api/package.json
apps/api/tsconfig.json
Expose system-scoped application services and community-specific error type for use by queue handlers and other background workloads.
  • Extend AppServicesHost interface and buildApplicationServicesFactory to implement forSystem, creating a system Passport with optional PermissionsSpec and assembling ApplicationServices without a verified user.
  • Export Domain.PermissionsSpec from domain, and CommunityNotFoundError from application-services so handlers can depend on them.
  • Refine community updateSettings to throw CommunityNotFoundError when the community is missing from the repository, and propagate the new error type through the community context exports.
  • Update mock application services factory in verification tests to expose forSystem for parity with real factory.
packages/ocom/application-services/src/index.ts
packages/ocom/application-services/src/contexts/community/community/update-settings.ts
packages/ocom/application-services/src/contexts/community/community/index.ts
packages/ocom/application-services/src/contexts/community/index.ts
packages/ocom/domain/src/domain/contexts/passport.ts
packages/ocom/domain/src/domain/index.ts
packages/ocom-verification/acceptance-api/src/mock-application-services.ts
Miscellaneous formatting, dependency, and CI/tooling adjustments.
  • Compact some test expectations and helper calls into single lines for readability in server-oauth2-mock-seedwork tests.
  • Relax the brace-expansion override version to a range in pnpm-workspace overrides.
  • Bump Node version used in docs deployment pipeline to 22.22.2.
  • Add workspace references and dependencies for the new handler package in API tsconfig and package.json, and add empty mock-oidc user files for new apps.
packages/cellix/server-oauth2-mock-seedwork/tests/file-user-store.test.ts
packages/cellix/server-oauth2-mock-seedwork/tests/portal-discovery.test.ts
pnpm-workspace.yaml
apps/docs/deploy-docs.yml
apps/api/package.json
apps/api/tsconfig.json
apps/ui-community/mock-oidc.users.json
apps/ui-staff/mock-oidc.users.json
.npmrc

Assessment against linked issues

Issue Objective Addressed Explanation
#272 Add Azure Functions queue trigger handler registration to the Cellix bootstrap flow, following the existing HTTP handler registration pattern and keeping HTTP registration working.
#272 Introduce a community-update Storage Queue and queue-triggered handler in the Owner Community sample app that validates messages via the queue storage service, updates existing communities, logs and skips missing communities, handles invalid messages predictably, and is wired end-to-end (including local/Azurite support and tests).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In Cellix.registerAzureFunctionQueueHandler, consider making PendingQueueHandler generic instead of casting options and handlerCreator to unknown so the queue handler types are preserved end-to-end and you avoid unsafe type assertions.
  • The communityUpdateQueueHandlerCreator error handling relies on err.message.toLowerCase().includes('community not found'); this is brittle—prefer using a well-defined error type or code from updateSettings to distinguish expected 'not found' cases from other failures.
  • The 'community-update' queue name is duplicated in the new handler package, queue schema, and registration in apps/api/src/index.ts; consider centralizing this identifier (e.g., exported constant from the queue schema) to avoid drift if the name ever changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Cellix.registerAzureFunctionQueueHandler`, consider making `PendingQueueHandler` generic instead of casting `options` and `handlerCreator` to `unknown` so the queue handler types are preserved end-to-end and you avoid unsafe type assertions.
- The `communityUpdateQueueHandlerCreator` error handling relies on `err.message.toLowerCase().includes('community not found')`; this is brittle—prefer using a well-defined error type or code from `updateSettings` to distinguish expected 'not found' cases from other failures.
- The `'community-update'` queue name is duplicated in the new handler package, queue schema, and registration in `apps/api/src/index.ts`; consider centralizing this identifier (e.g., exported constant from the queue schema) to avoid drift if the name ever changes.

## Individual Comments

### Comment 1
<location path="packages/ocom/handler-queue-community-update/src/handler.ts" line_range="36-39" />
<code_context>
+		const popReceipt = context.triggerMetadata?.['popReceipt'] as string | undefined;
+		// biome-ignore lint/complexity/useLiteralKeys: triggerMetadata uses an index signature type that requires bracket access
+		const dequeueCount = context.triggerMetadata?.['dequeueCount'] as number | undefined;
+		const metadata = {
+			id,
+			...(popReceipt === undefined ? {} : { popReceipt }),
+			...(dequeueCount === undefined ? {} : { dequeueCount }),
+		};
+		let message: Awaited<ReturnType<typeof queueService.receiveFromCommunityUpdateQueue>>;
</code_context>
<issue_to_address>
**issue (bug_risk):** Metadata object omits keys entirely when values are undefined, which may conflict with consumers expecting explicit undefined fields.

Because `metadata` only includes `popReceipt` and `dequeueCount` when they’re defined, `receiveFromCommunityUpdateQueue` may receive `{ id }` with those keys missing entirely when trigger metadata is absent. Tests and the implied contract suggest these keys should always exist, even if `undefined`. If downstream code relies on that shape, construct `metadata` as:

```ts
const metadata = {
  id,
  popReceipt,
  dequeueCount,
};
```

or otherwise ensure the object consistently contains these keys.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/ocom/handler-queue-community-update/src/handler.ts Outdated
Copilot Bot and others added 7 commits June 30, 2026 14:26
- Make PendingQueueHandler<T> generic; use any[] to store heterogeneous
  handlers without per-field unsafe casts
- Add CommunityNotFoundError class to @ocom/application-services;
  replace brittle string-match in handler with instanceof check
- Export communityUpdateQueueName constant from @ocom/service-queue-storage;
  use it in @apps/api to eliminate duplicated queue name string
- Add explicit QueueTriggerMetadata type annotation on metadata object;
  retain conditional spread (required by exactOptionalPropertyTypes: true)
- Export QueueTriggerMetadata from @ocom/service-queue-storage

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Test was throwing plain Error; handler now checks instanceof CommunityNotFoundError
so the mock must use the typed error class.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The manual mock was missing the new constant export, causing index.test.ts
to fail when index.ts consumed communityUpdateQueueName at import time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…install conflicts in CI

pnpm 11 defaults to verify-deps-before-run=install, causing each concurrent
turbo task to trigger pnpm install when workspace state is stale. Parallel
workers SIGINT each other, killing tasks like @ocom/service-queue-storage#gen.

CI already runs 'pnpm install --frozen-lockfile' before turbo tasks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nnoce14

nnoce14 commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In communityUpdateQueueHandlerCreator, the catch around receiveFromCommunityUpdateQueue treats all failures as an "invalid message payload" — consider narrowing this to known validation/deserialization failures or including the trigger metadata/queueId in the log so operational issues with the queue or storage account can be distinguished from bad messages.
  • The new forSystem() method on AppServicesHost is now a hard requirement (used by queue handlers); if you anticipate other kinds of system scopes in future, consider allowing optional parameters (e.g., capabilities or reason) so you don’t need further breaking changes to the host interface when new system-only operations appear.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `communityUpdateQueueHandlerCreator`, the catch around `receiveFromCommunityUpdateQueue` treats all failures as an "invalid message payload" — consider narrowing this to known validation/deserialization failures or including the trigger metadata/queueId in the log so operational issues with the queue or storage account can be distinguished from bad messages.
- The new `forSystem()` method on `AppServicesHost` is now a hard requirement (used by queue handlers); if you anticipate other kinds of system scopes in future, consider allowing optional parameters (e.g., capabilities or reason) so you don’t need further breaking changes to the host interface when new system-only operations appear.

## Individual Comments

### Comment 1
<location path="packages/ocom/handler-queue-community-update/src/handler.ts" line_range="29-38" />
<code_context>
+export const communityUpdateQueueHandlerCreator = (applicationServicesFactory: ApplicationServicesFactory, queueService: QueueStorageOperations): StorageQueueHandler<CommunityUpdatePayload> => {
</code_context>
<issue_to_address>
**suggestion:** Avoid hardcoding the queue name in log messages; reuse the shared `communityUpdateQueueName` constant.

The handler logs with a hardcoded `community-update:` prefix while the queue name is defined centrally as `communityUpdateQueueName` in `@ocom/service-queue-storage`. Using `communityUpdateQueueName` for the log prefix (e.g. ``context.error(`${communityUpdateQueueName}: invalid message payload: ...`)``) will keep logs and configuration in sync and avoid drift if the queue name changes.

Suggested implementation:

```typescript
 */
import { communityUpdateQueueName } from '@ocom/service-queue-storage';

export const communityUpdateQueueHandlerCreator = (applicationServicesFactory: ApplicationServicesFactory, queueService: QueueStorageOperations): StorageQueueHandler<CommunityUpdatePayload> => {

```

```typescript
`${communityUpdateQueueName}: 

```

```typescript
`${communityUpdateQueueName}: 

```

1. The second and third replacements are intentionally string-fragment replacements: they are meant to update any log messages (e.g. `context.error('community-update: invalid message payload: ...')`, `context.log("community-update: ...")`) to interpolate `communityUpdateQueueName` instead of hardcoding the prefix. If your log messages differ slightly (extra spaces, different punctuation), adjust the SEARCH fragments accordingly so they match the existing strings.
2. If `@ocom/service-queue-storage` is already imported elsewhere in this file, merge the new `communityUpdateQueueName` named import into the existing import statement instead of adding a separate one.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/ocom/handler-queue-community-update/src/handler.ts Outdated
Replace hardcoded 'community-update:' string literals with the imported
communityUpdateQueueName constant so logs stay in sync with config if
the queue name ever changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nnoce14

nnoce14 commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="packages/ocom/handler-queue-community-update/src/handler.ts" line_range="44" />
<code_context>
+			...(popReceipt === undefined ? {} : { popReceipt }),
+			...(dequeueCount === undefined ? {} : { dequeueCount }),
+		};
+		let message: Awaited<ReturnType<typeof queueService.receiveFromCommunityUpdateQueue>>;
+		try {
+			message = await queueService.receiveFromCommunityUpdateQueue(queueEntry, metadata);
</code_context>
<issue_to_address>
**suggestion:** Improve error logging by including the original error object for better diagnostics.

Right now only the error message string is logged, which loses stack traces and structured fields on the original error. To aid production debugging, log both the formatted message and the error object (e.g. `context.error(`${communityUpdateQueueName}: invalid message payload`, err)` if supported), or serialize selected non-sensitive fields with `JSON.stringify`. You can use the same approach for the `CommunityNotFoundError` branch if additional context would be helpful.

Suggested implementation:

```typescript
		let message: Awaited<ReturnType<typeof queueService.receiveFromCommunityUpdateQueue>>;
		try {
			message = await queueService.receiveFromCommunityUpdateQueue(queueEntry, metadata);
		} catch (err) {
			const errorMessage = err instanceof Error ? err.message : String(err);
			context.error(
				`${communityUpdateQueueName}: invalid message payload: ${errorMessage}`,
				err,
			);
			return;
		}

```

There is likely a second `try/catch` block around `appServices.Community.Community.updateSettings` that catches `CommunityNotFoundError` or other errors. To align with this change, update that `catch` block so that:
1. It still logs the formatted error message string (as it does today), and
2. It also passes the original error object as an additional argument to `context.error(...)` (or `context.warn(...)`/`context.info(...)` if that is what it currently uses), e.g.:

```ts
catch (err) {
  const errorMessage = err instanceof Error ? err.message : String(err);
  context.error(`${communityUpdateQueueName}: community update failed: ${errorMessage}`, err);
  // existing handling...
}
```

Adjust the log message text and log level to match the existing logging conventions in that branch.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/ocom/handler-queue-community-update/src/handler.ts
…gnostics

Log both the formatted message string and the original error so stack traces
and structured error fields are preserved in Application Insights.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nnoce14

nnoce14 commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

henry-casper pushed a commit that referenced this pull request Jul 7, 2026
…install conflicts in CI

pnpm 11 defaults to verify-deps-before-run=install, causing each concurrent
turbo task to trigger pnpm install when workspace state is stale. Parallel
workers SIGINT each other, killing tasks like @ocom/service-queue-storage#gen.

CI already runs 'pnpm install --frozen-lockfile' before turbo tasks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nnoce14

nnoce14 commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In communityUpdateQueueHandlerCreator, the README and code comments refer to building a "request-scoped" application services instance, but the implementation correctly uses forSystem; aligning the wording to consistently describe this as a system-scoped, non-request context would reduce confusion.
  • When throwing new Error('Application not started yet') in the queue handler registration path (setupLifecycleapp.storageQueue handler), consider including the queue function name in the message to make it easier to identify which trigger was invoked before startup.
  • The queue handler logging currently emits only the error message (e.g. invalid message payload or community not found); adding key trigger metadata such as id/dequeueCount into the logged message or structured data would make diagnosing problematic messages significantly easier in production.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `communityUpdateQueueHandlerCreator`, the README and code comments refer to building a "request-scoped" application services instance, but the implementation correctly uses `forSystem`; aligning the wording to consistently describe this as a system-scoped, non-request context would reduce confusion.
- When throwing `new Error('Application not started yet')` in the queue handler registration path (`setupLifecycle``app.storageQueue` handler), consider including the queue function name in the message to make it easier to identify which trigger was invoked before startup.
- The queue handler logging currently emits only the error message (e.g. `invalid message payload` or `community not found`); adding key trigger metadata such as `id`/`dequeueCount` into the logged message or structured data would make diagnosing problematic messages significantly easier in production.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@nnoce14

nnoce14 commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The trigger metadata extraction (id, popReceipt, dequeueCount) in communityUpdateQueueHandlerCreator is quite specific and will likely be reused for other queue handlers; consider moving this into a shared helper (e.g., in the queue storage service or a utility module) so future handlers don’t duplicate the same logic and conventions.
  • The RequestScopedHost/AppHost types in cellix.ts now include a generic forSystem(permissions?: P) while AppServicesHost in @ocom/application-services uses a concrete Partial<Domain.PermissionsSpec>; it may be worth aligning these typings so handlers using AppHost can benefit from the same permission type safety rather than dealing with unknown.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The trigger metadata extraction (`id`, `popReceipt`, `dequeueCount`) in `communityUpdateQueueHandlerCreator` is quite specific and will likely be reused for other queue handlers; consider moving this into a shared helper (e.g., in the queue storage service or a utility module) so future handlers don’t duplicate the same logic and conventions.
- The `RequestScopedHost`/`AppHost` types in `cellix.ts` now include a generic `forSystem(permissions?: P)` while `AppServicesHost` in `@ocom/application-services` uses a concrete `Partial<Domain.PermissionsSpec>`; it may be worth aligning these typings so handlers using `AppHost` can benefit from the same permission type safety rather than dealing with `unknown`.

## Individual Comments

### Comment 1
<location path="apps/api/src/cellix.ts" line_range="178-187" />
<code_context>
 type UninitializedServiceRegistry<ContextType = unknown, AppServices = unknown> = InfrastructureServiceRegistry<ContextType, AppServices>;

-type RequestScopedHost<S, H = unknown> = {
+type RequestScopedHost<S, H = unknown, P = unknown> = {
 	forRequest(rawAuthHeader?: string, hints?: H): Promise<S>;
+	/**
+	 * Builds a system-scoped application services instance, e.g. for background/queue-triggered work with
+	 * no request context. Callers should pass only the specific permissions their operation needs
+	 * (least privilege) rather than relying on an implicit default permission set.
+	 */
+	forSystem(permissions?: P): Promise<S>;
 };

 type AppHost<AppServices> = RequestScopedHost<AppServices, unknown>;
</code_context>
<issue_to_address>
**suggestion:** Propagate the permissions type through `AppHost` so `forSystem` calls can be strongly typed.

`RequestScopedHost` now has a `forSystem(permissions?: P)` overload, but `AppHost` is still `RequestScopedHost<AppServices, unknown>`, which fixes `P` to `unknown`. As a result, callers of `AppHost.forSystem` (e.g. queue handlers) don’t get type checking for `permissions`, even though `ApplicationServicesFactory.forSystem` expects `Partial<Domain.PermissionsSpec>`.

Propagate the permissions generic into `AppHost`, e.g. `type AppHost<AppServices, P = unknown> = RequestScopedHost<AppServices, unknown, P>;`, and update the relevant `Cellix` usages to supply the concrete permissions type. This preserves runtime behavior but aligns the static API with the permissions contract.

Suggested implementation:

```typescript
		options: Omit<StorageQueueFunctionOptions<T>, 'handler'>,
		handlerCreator: (
			applicationServicesHost: AppHost<AppServices, Partial<Domain.PermissionsSpec>>,
			infrastructureRegistry: InitializedServiceRegistry,
		) => StorageQueueHandler<T>,
	): AzureFunctionHandlerRegistry<ContextType, AppServices>;
	/**
	 * Finalizes configuration and starts the application.
	 *

type UninitializedServiceRegistry<ContextType = unknown, AppServices = unknown> = InfrastructureServiceRegistry<ContextType, AppServices>;

type RequestScopedHost<S, H = unknown, P = unknown> = {
	forRequest(rawAuthHeader?: string, hints?: H): Promise<S>;
	/**
	 * Builds a system-scoped application services instance, e.g. for background/queue-triggered work with
	 * no request context. Callers should pass only the specific permissions their operation needs
	 * (least privilege) rather than relying on an implicit default permission set.
	 */
	forSystem(permissions?: P): Promise<S>;
};

type AppHost<AppServices, P = unknown> = RequestScopedHost<AppServices, unknown, P>;
	handlerCreator: (
		applicationServicesHost: AppHost<AppServices, Partial<Domain.PermissionsSpec>>,
		infrastructureRegistry: InitializedServiceRegistry,
	) => HttpHandler;
}

interface PendingQueueHandler<AppServices, T = unknown> {
	name: string;
	options: Omit<StorageQueueFunctionOptions<T>, 'handler'>;
	handlerCreator: (
		applicationServicesHost: AppHost<AppServices, Partial<Domain.PermissionsSpec>>,
		infrastructureRegistry: InitializedServiceRegistry,
	) => StorageQueueHandler<T>;
}

type Phase = 'infrastructure' | 'context' | 'app-services' | 'handlers' | 'started';

```

1. Ensure `Domain.PermissionsSpec` is imported in `apps/api/src/cellix.ts` (e.g. `import { Domain } from '...';`) if it is not already available in this file.
2. Review other usages of `AppHost<AppServices>` and `RequestScopedHost<AppServices, unknown>` in `cellix.ts` (and related modules) to:
   - Replace `RequestScopedHost<AppServices, unknown>` with `AppHost<AppServices, P>` where appropriate.
   - Thread through a concrete `P` type (such as `Partial<Domain.PermissionsSpec>`) for any code paths that call `forSystem`, so that all `forSystem` use sites benefit from strong typing on `permissions`.
3. If there are generic types or factory functions that construct `AppHost` instances, consider adding a `P` generic parameter to them and defaulting to `unknown` to preserve backwards compatibility while enabling stricter typing where desired.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread apps/api/src/cellix.ts
…mproved type safety for configuring permissions forSystem
@nnoce14

nnoce14 commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The generic parameter P on Cellix and AppHost is threaded throughout but only concretely used once for Partial<Domain.PermissionsSpec>; consider constraining or defaulting it closer to that shape to avoid overly generic APIs that may hide misuses of forSystem.
  • In communityUpdateQueueHandlerCreator, the trigger metadata extraction and optional property spreading pattern is quite specific; if more queue handlers will follow, consider extracting a shared helper for building QueueTriggerMetadata to keep behavior consistent and reduce duplication.
  • The queue handler registration logic in Cellix.setupLifecycle mirrors the HTTP handler registration; consider extracting a small internal helper for handler registration to reduce repetition and make it easier to apply future changes (e.g., additional lifecycle checks) to both handler types uniformly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The generic parameter `P` on `Cellix` and `AppHost` is threaded throughout but only concretely used once for `Partial<Domain.PermissionsSpec>`; consider constraining or defaulting it closer to that shape to avoid overly generic APIs that may hide misuses of `forSystem`.
- In `communityUpdateQueueHandlerCreator`, the trigger metadata extraction and optional property spreading pattern is quite specific; if more queue handlers will follow, consider extracting a shared helper for building `QueueTriggerMetadata` to keep behavior consistent and reduce duplication.
- The queue handler registration logic in `Cellix.setupLifecycle` mirrors the HTTP handler registration; consider extracting a small internal helper for handler registration to reduce repetition and make it easier to apply future changes (e.g., additional lifecycle checks) to both handler types uniformly.

## Individual Comments

### Comment 1
<location path="packages/ocom/handler-queue-community-update/src/handler.ts" line_range="36-38" />
<code_context>
+			context.error(`${communityUpdateQueueName}: invalid message payload (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'}): ${err instanceof Error ? err.message : String(err)}`, err);
+			return;
+		}
+		const appServices = await applicationServicesFactory.forSystem({ canManageCommunitySettings: true });
+		const { communityId, name, domain, whiteLabelDomain, handle } = message.payload.eventPayload;
+		try {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Errors from `forSystem` will currently crash the function without logging.

While you handle errors from `receiveFromCommunityUpdateQueue` and `updateSettings`, any exception thrown inside `applicationServicesFactory.forSystem` (e.g., misconfiguration, passport factory issues) will escape unlogged. Please wrap the `forSystem` call in its own try/catch, log via `context.error` with relevant queue/trigger metadata, and then rethrow so operational failures are visible and traceable.

```suggestion
		let appServices;
		try {
			appServices = await applicationServicesFactory.forSystem({ canManageCommunitySettings: true });
		} catch (err) {
			context.error(
				`${communityUpdateQueueName}: failed to create application services (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'})`,
				err,
			);
			throw err;
		}
		const { communityId, name, domain, whiteLabelDomain, handle } = message.payload.eventPayload;
		try {
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/ocom/handler-queue-community-update/src/handler.ts Outdated
…ers to use it for improved metadata extraction
@nnoce14

nnoce14 commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="packages/ocom/handler-queue-community-update/src/handler.ts" line_range="26-35" />
<code_context>
+			context.error(`${communityUpdateQueueName}: invalid message payload (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'}): ${err instanceof Error ? err.message : String(err)}`, err);
+			return;
+		}
+		let appServices: Awaited<ReturnType<typeof applicationServicesFactory.forSystem>>;
+		try {
+			appServices = await applicationServicesFactory.forSystem({ canManageCommunitySettings: true });
+		} catch (err) {
+			context.error(`${communityUpdateQueueName}: failed to create application services (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'}): ${err instanceof Error ? err.message : String(err)}`, err);
+			throw err;
+		}
+		const { communityId, name, domain, whiteLabelDomain, handle } = message.payload.eventPayload;
+		try {
+			await appServices.Community.Community.updateSettings({
+				id: communityId,
+				...(name === undefined ? {} : { name }),
</code_context>
<issue_to_address>
**suggestion:** Consider logging metadata for non-`CommunityNotFoundError` failures before rethrowing to aid diagnostics.

The non-`CommunityNotFoundError` path currently rethrows without logging any queue context, even though `id` and `dequeueCount` are already available. You could add a single `context.error` before rethrowing, including these fields, for example:

```ts
} catch (err) {
  if (err instanceof CommunityNotFoundError) {
    context.error(
      `${communityUpdateQueueName}: community not found: ${communityId} (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'})`,
      err,
    );
    return;
  }

  context.error(
    `${communityUpdateQueueName}: updateSettings failed for ${communityId} (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'})`,
    err,
  );
  throw err;
}
```

This preserves existing retry behavior while improving log correlation for failed messages.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +26 to +35
let appServices: Awaited<ReturnType<typeof applicationServicesFactory.forSystem>>;
try {
appServices = await applicationServicesFactory.forSystem({ canManageCommunitySettings: true });
} catch (err) {
context.error(`${communityUpdateQueueName}: failed to create application services (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'}): ${err instanceof Error ? err.message : String(err)}`, err);
throw err;
}
const { communityId, name, domain, whiteLabelDomain, handle } = message.payload.eventPayload;
try {
await appServices.Community.Community.updateSettings({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider logging metadata for non-CommunityNotFoundError failures before rethrowing to aid diagnostics.

The non-CommunityNotFoundError path currently rethrows without logging any queue context, even though id and dequeueCount are already available. You could add a single context.error before rethrowing, including these fields, for example:

} catch (err) {
  if (err instanceof CommunityNotFoundError) {
    context.error(
      `${communityUpdateQueueName}: community not found: ${communityId} (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'})`,
      err,
    );
    return;
  }

  context.error(
    `${communityUpdateQueueName}: updateSettings failed for ${communityId} (id=${id}, dequeueCount=${dequeueCount ?? 'unknown'})`,
    err,
  );
  throw err;
}

This preserves existing retry behavior while improving log correlation for failed messages.

henry-casper added a commit that referenced this pull request Jul 8, 2026
pnpm 11 only reads auth/registry settings from .npmrc; all other
settings must live in pnpm-workspace.yaml (camelCase). The .npmrc
added in 3b2105f was silently ignored, so parallel turbo tasks
still spawned concurrent 'pnpm install' runs that SIGINT each other
(runDepsStatusCheck in the CI stack trace). verifyDepsBeforeRun: false
now actually resolves (pnpm config get confirms).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
henry-casper added a commit that referenced this pull request Jul 8, 2026
pnpm 11 only reads auth/registry settings from .npmrc; all other
settings must live in pnpm-workspace.yaml (camelCase). The .npmrc
added in 3b2105f was silently ignored, so parallel turbo tasks
still spawned concurrent 'pnpm install' runs that SIGINT each other
(runDepsStatusCheck in the CI stack trace). verifyDepsBeforeRun: false
now actually resolves (pnpm config get confirms).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Azure Functions Queue Trigger Handler Registration

1 participant