Skip to content

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

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

feat(#272): implement Azure Functions queue trigger handler registration#289
nnoce14 wants to merge 15 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 application.

New Features:

  • Introduce Cellix queue handler registration via registerAzureFunctionQueueHandler and underlying app.storageQueue integration for Azure Functions.
  • Add a new @ocom/handler-queue-community-update package providing a Storage Queue handler that validates messages and applies community settings updates using system-scoped application services.
  • Extend @ocom/service-queue-storage with a typed community-update inbound queue definition, schema, and queue name export.
  • Expose system-scoped forSystem() application service construction and permissions typing for background/queue-triggered work, and integrate it into the API bootstrap.

Enhancements:

  • Improve payload logging helpers to support nested dotted-path payload field references with stronger TypeScript typing and runtime resolution.
  • Refine community update error handling by introducing a dedicated CommunityNotFoundError and propagating it through application services.
  • Expand Cellix and API documentation to cover infrastructure service registration, separate HTTP vs queue handler registration, and queue-handling best practices.

Build:

  • Add the @ocom/handler-queue-community-update package to the workspace, including TypeScript, Vitest, and build configuration.
  • Update the docs deployment pipeline Node.js version and relax the brace-expansion dependency range in the workspace overrides.

Tests:

  • Add unit and feature tests covering Cellix queue handler registration, startup behaviour, and Azure Functions storageQueue wiring.
  • Add tests for nested payload logging field resolution and for the new community-update queue handler, including its error-handling paths.

- 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

Introduce first-class Azure Storage Queue trigger support in the Cellix API app, including a new community-update queue handler package, queue registration in the queue storage service, system-scoped application services for background work, and enhanced queue logging utilities with nested payload support.

Sequence diagram for registering and starting queue handlers in Cellix

sequenceDiagram
    participant ApiBootstrap as ApiBootstrap(index.ts)
    participant Cellix as Cellix
    participant AppServicesFactory as ApplicationServicesFactory
    participant AzureApp as app(@azure/functions)

    ApiBootstrap->>Cellix: initializeInfrastructureServices(registry => registerInfrastructureService(...))
    Cellix-->>ApiBootstrap: Cellix

    ApiBootstrap->>Cellix: setContext(contextCreator)
    ApiBootstrap->>Cellix: initializeApplicationServices(context => buildApplicationServicesFactory(context))
    Cellix-->>ApiBootstrap: AzureFunctionHandlerRegistry

    ApiBootstrap->>Cellix: registerAzureFunctionHttpHandler("graphql", options, graphHandlerCreator)
    Cellix-->>ApiBootstrap: AzureFunctionHandlerRegistry

    ApiBootstrap->>Cellix: registerAzureFunctionHttpHandler("rest", options, restHandlerCreator)
    Cellix-->>ApiBootstrap: AzureFunctionHandlerRegistry

    ApiBootstrap->>Cellix: registerAzureFunctionQueueHandler(communityUpdateQueueName, { queueName, connection }, handlerCreator)
    Cellix-->>ApiBootstrap: AzureFunctionHandlerRegistry

    ApiBootstrap->>Cellix: startUp()
    activate Cellix
    Cellix->>Cellix: setupLifecycle()

    loop pendingHandlers
        Cellix->>AzureApp: http(name, { ...options, handler })
    end

    loop pendingQueueHandlers
        Cellix->>AzureApp: storageQueue(name, { ...options, handler })
    end

    Cellix-->>ApiBootstrap: StartedApplication
    deactivate Cellix
Loading

Sequence diagram for the community-update queue message handling

sequenceDiagram
    participant AzureRuntime as AzureFunctionsRuntime
    participant Handler as StorageQueueHandler
    participant QueueService as QueueStorageOperations
    participant AppFactory as ApplicationServicesFactory
    participant AppServices as ApplicationServices
    participant CommunitySvc as Community.updateSettings
    participant Ctx as InvocationContext

    AzureRuntime->>Handler: handler(queueEntry, Ctx)

    Handler->>QueueService: receiveFromCommunityUpdateQueue(queueEntry, metadata)
    alt invalid payload
        QueueService--x Handler: throws Error
        Handler->>Ctx: error("community-update: invalid message payload...", Error)
        Handler-->>AzureRuntime: return
    else valid payload
        QueueService-->>Handler: message(payload)
        Handler->>AppFactory: forSystem({ canManageCommunitySettings: true })
        AppFactory-->>Handler: AppServices
        Handler->>CommunitySvc: updateSettings({ id, name?, domain?, whiteLabelDomain?, handle? })
        alt CommunityNotFoundError
            CommunitySvc--x Handler: throws CommunityNotFoundError
            Handler->>Ctx: error("community-update: community not found...", CommunityNotFoundError)
            Handler-->>AzureRuntime: return
        else other error
            CommunitySvc--x Handler: throws Error
            Handler--x AzureRuntime: rethrow Error
        end
    end
Loading

File-Level Changes

Change Details Files
Add Azure Storage Queue trigger handler registration to Cellix and wire it into the API bootstrap alongside HTTP handlers.
  • Extend AzureFunctionHandlerRegistry and Cellix to support registerAzureFunctionQueueHandler using StorageQueueFunctionOptions and StorageQueueHandler.
  • Track pending queue handlers, enforce valid registration phases, and register them with app.storageQueue during setupLifecycle with better startup error messages.
  • Update Cellix tests and Gherkin feature specs to cover queue handler registration, invalid phase errors, and startup wiring of both HTTP and queue handlers.
  • Mock app.storageQueue in tests and expose registerAzureFunctionQueueHandler from the API index surface and its tests.
apps/api/src/cellix.ts
apps/api/src/cellix.test.ts
apps/api/src/features/cellix.feature
apps/api/src/index.ts
apps/api/src/index.test.ts
apps/api/.github/instructions/api.instructions.md
Introduce a new @ocom/handler-queue-community-update package implementing the community-update queue handler with least-privilege system access and robust error handling.
  • Create communityUpdateQueueHandlerCreator that maps Azure trigger metadata to QueueTriggerMetadata, validates messages via receiveFromCommunityUpdateQueue, and calls Community.updateSettings through ApplicationServicesFactory.forSystem with scoped permissions.
  • Implement error handling that logs and swallows invalid payloads and CommunityNotFoundError while rethrowing unexpected errors, to avoid poison-message loops.
  • Add unit tests, TS/Vitest/Biome configuration, and package metadata for the new handler package.
  • Wire the community-update handler into the API bootstrap using registerAzureFunctionQueueHandler and ServiceQueueStorage from the infrastructure registry.
packages/ocom/handler-queue-community-update/src/handler.ts
packages/ocom/handler-queue-community-update/src/handler.test.ts
packages/ocom/handler-queue-community-update/src/index.ts
packages/ocom/handler-queue-community-update/package.json
packages/ocom/handler-queue-community-update/tsconfig.json
packages/ocom/handler-queue-community-update/vitest.config.ts
packages/ocom/handler-queue-community-update/readme.md
apps/api/src/index.ts
apps/api/package.json
apps/api/tsconfig.json
Extend application services with a system-scoped factory and expose community-specific error/permission types for background handlers.
  • Add forSystem to ApplicationServicesFactory/AppServicesHost that builds a system Passport with optional fine-grained permissions via Domain.PassportFactory.forSystem.
  • Use forSystem in the community-update handler to request only canManageCommunitySettings permissions, and update acceptance mocks to support forSystem.
  • Introduce CommunityNotFoundError in community update settings application service and re-export it from community and application-services entry points.
  • Export PermissionsSpec from the domain package for use in AppServicesHost.forSystem typing.
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-verification/acceptance-api/src/mock-application-services.ts
packages/ocom/domain/src/domain/contexts/passport.ts
packages/ocom/domain/src/domain/index.ts
Define and register a new inbound community-update queue in the queue storage service and enhance logging utilities to support nested payload paths.
  • Add community-update inbound queue schema, name constant, and definition using defineQueue, including logging tags/metadata and a sample data file.
  • Register the community-update queue in the queue storage registry and ensure it is provisioned by ServiceQueueStorage, updating tests accordingly.
  • Enhance PayloadFieldProxy and LoggingFieldSpec typing to support nested dotted paths, treating appropriate value types as leaves.
  • Implement a recursive payloadField proxy and readPayloadPath helper so $payload and payloadFields can generate and resolve nested dotted paths; update resolveLoggingFields and add tests for nested behavior and omission semantics.
packages/ocom/service-queue-storage/src/schemas/inbound/community-update.ts
packages/ocom/service-queue-storage/src/schemas/inbound/community-update.sample.dat
packages/ocom/service-queue-storage/src/schemas/inbound/community-update.schema.json
packages/ocom/service-queue-storage/src/registry.ts
packages/ocom/service-queue-storage/src/service.test.ts
packages/cellix/service-queue-storage/src/interfaces.ts
packages/cellix/service-queue-storage/src/logging-fields.ts
packages/cellix/service-queue-storage/src/payload-proxy.test.ts
packages/ocom/service-queue-storage/src/index.ts
Update documentation, workspace config, and CI tooling to reflect the new queue handler pattern and dependencies.
  • Revise API instructions to document initializeInfrastructureServices, HTTP vs queue handler registration, queue handler examples, and queue-specific error-handling guidance.
  • Add the new handler package to pnpm workspace and API app dependencies/TypeScript references; relax brace-expansion override version to a range.
  • Adjust docs deployment pipeline to pin Node.js to 22.22.2 and add minor workspace/tooling files needed by the new package (e.g., .npmrc stubs).
apps/api/.github/instructions/api.instructions.md
apps/api/package.json
apps/api/tsconfig.json
pnpm-workspace.yaml
apps/docs/deploy-docs.yml
.npmrc
packages/ocom/handler-queue-community-update/.gitignore
packages/ocom/handler-queue-community-update/tsconfig.vitest.json
packages/ocom/handler-queue-community-update/turbo.json
pnpm-lock.yaml

Assessment against linked issues

Issue Objective Addressed Explanation
#272 Add first-class Azure Functions queue trigger handler registration to the Cellix bootstrap flow, following the existing HTTP handler registration pattern and refining shared handler registration concepts.
#272 Introduce a community-update storage queue and a queue-triggered handler in the Owner Community sample app that validates messages, updates existing communities with provided fields, logs and ignores missing communities, and handles invalid messages predictably.
#272 Keep the implementation wired through @apps/api/src/cellix.ts (app-local), ensuring existing HTTP handler registration continues to work and that local development uses the shared queue storage service/registry (including Azurite provisioning).

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
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
Comment on lines +178 to 187
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>;
};

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: 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:

		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.

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