Skip to content

blob_showcase_infra - #314

Open
dani-vaibhav wants to merge 1 commit into
rohit-r-kumar/issue258from
blob_showcase
Open

blob_showcase_infra#314
dani-vaibhav wants to merge 1 commit into
rohit-r-kumar/issue258from
blob_showcase

Conversation

@dani-vaibhav

@dani-vaibhav dani-vaibhav commented Aug 6, 2026

Copy link
Copy Markdown

Summary by Sourcery

Add a tech-admin blob storage explorer feature spanning backend services, GraphQL schema/resolvers, permissions, and staff UI, including hierarchical Azure Blob listings and bounded blob content download with metadata and tag support.

New Features:

  • Expose container listing, hierarchical blob listing with filters, and bounded blob download in the shared blob storage service API.
  • Introduce tech-admin application-service commands and GraphQL queries for listing blob containers, exploring blob hierarchies, and fetching blob content with signed download URLs where available.
  • Add a staff portal Blob Storage Explorer page with filtering, pagination, and rich preview support for text, image, and PDF blobs, gated by a new canViewBlobExplorer permission flag.

Enhancements:

  • Extend existing tech-admin authorization to cover blob storage queries and integrate blob explorer access into shared staff navigation and permission checks.
  • Refine test utilities and no-op blob storage implementations to support the new blob exploration and download operations.

Documentation:

  • Update blob storage service manifest and README to document hierarchical exploration and download capabilities.

Tests:

  • Add unit and integration-style tests for blob storage service hierarchy listing and download behavior, including size limits and tag/metadata filters.
  • Add GraphQL resolver tests covering authorization and data mapping for the new tech-admin blob storage operations.
  • Add UI tests and Storybook stories for the Blob Storage Explorer component and its preview interactions.

@sourcery-ai

sourcery-ai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds a tech-admin Blob Storage Explorer feature spanning backend, GraphQL, and UI layers, including new hierarchical blob listing, container enumeration, bounded blob download, and permission-gated access for staff users.

Sequence diagram for techAdminBlobList hierarchical listing

sequenceDiagram
    actor StaffUser
    participant BlobStorageExplorerContainer
    participant GraphQLServer
    participant TechAdminResolvers
    participant TechAdminApplicationService as TechAdminService
    participant BlobStorageOperations as BlobStorageService

    StaffUser->>BlobStorageExplorerContainer: Select container / Apply filters
    BlobStorageExplorerContainer->>GraphQLServer: TechAdminBlobStorageExplorerList
    GraphQLServer->>TechAdminResolvers: techAdminBlobList(args)

    TechAdminResolvers->>TechAdminResolvers: assertCanViewBlobExplorer(context)
    TechAdminResolvers->>TechAdminResolvers: buildBlobListQueryCommand(args)
    TechAdminResolvers->>TechAdminApplicationService: TechAdminService.ListBlobHierarchy(command)

    TechAdminApplicationService->>BlobStorageOperations: listBlobHierarchy(command)
    BlobStorageOperations-->>TechAdminApplicationService: BlobHierarchyPage
    TechAdminApplicationService-->>TechAdminResolvers: BlobHierarchyPage

    TechAdminResolvers-->>GraphQLServer: BlobHierarchyPage mapped to GraphQL types
    GraphQLServer-->>BlobStorageExplorerContainer: techAdminBlobList result
    BlobStorageExplorerContainer-->>StaffUser: Render folders, blobs, continuationToken
Loading

Sequence diagram for techAdminBlobContent preview and SAS download URL

sequenceDiagram
    actor StaffUser
    participant BlobStorageExplorer as BlobStorageExplorerUI
    participant BlobStorageExplorerContainer
    participant GraphQLServer
    participant TechAdminResolvers
    participant TechAdminApplicationService as TechAdminService
    participant BlobStorageOperations as BlobStorageService
    participant ClientUploadOperations as ClientUploadService

    StaffUser->>BlobStorageExplorer: Click View on blob
    BlobStorageExplorer->>BlobStorageExplorerContainer: onViewBlob(blob)
    BlobStorageExplorerContainer->>GraphQLServer: TechAdminBlobStorageExplorerContent
    GraphQLServer->>TechAdminResolvers: techAdminBlobContent(args)

    TechAdminResolvers->>TechAdminResolvers: assertCanViewBlobExplorer(context)
    TechAdminResolvers->>TechAdminApplicationService: TechAdminService.GetBlobContent({containerName, blobName})

    TechAdminApplicationService->>BlobStorageOperations: downloadBlob({containerName, blobName})
    BlobStorageOperations-->>TechAdminApplicationService: BlobDownloadResult
    TechAdminApplicationService->>ClientUploadOperations: generateReadSasToken({containerName, blobName, expiresOn})
    ClientUploadOperations-->>TechAdminApplicationService: sasToken
    TechAdminApplicationService-->>TechAdminResolvers: BlobContentResult (contentBase64, metadata, tags, downloadUrl)

    TechAdminResolvers-->>GraphQLServer: BlobContent
    GraphQLServer-->>BlobStorageExplorerContainer: techAdminBlobContent result
    BlobStorageExplorerContainer-->>BlobStorageExplorer: preview
    BlobStorageExplorer-->>StaffUser: Show preview and Download action
Loading

File-Level Changes

Change Details Files
Extend framework blob-storage service with hierarchical listing, container enumeration, and bounded in-memory download support.
  • Introduce new blob interfaces for containers, folders, explorer items, hierarchy pagination, and download results, plus a global max download size constant.
  • Implement ServiceBlobStorage.listContainers, listBlobHierarchy (prefix-based and tag-based variants), and downloadBlob with content-type heuristics and size enforcement.
  • Add internal helpers for prefix normalization, name/metadata/tag filtering, tag value escaping, text-compatibility detection, and streaming a readable into a bounded Uint8Array.
  • Export new types and BLOB_DOWNLOAD_MAX_BYTES through both cellix and ocom service-blob-storage indexes and extend BlobStorageOperations/ClientUploadOperations contracts.
  • Update blob storage tests and mocks to cover hierarchy listing, tag-based filtering paths, container enumeration, and bounded download behavior.
packages/cellix/service-blob-storage/src/service-blob-storage.ts
packages/cellix/service-blob-storage/src/interfaces.ts
packages/cellix/service-blob-storage/src/index.ts
packages/cellix/service-blob-storage/tests/index.test.ts
packages/ocom/service-blob-storage/src/index.ts
packages/ocom/service-blob-storage/src/blob-storage.contract.ts
packages/cellix/service-blob-storage/manifest.md
packages/cellix/service-blob-storage/README.md
packages/ocom-verification/acceptance-api/src/mock-application-services.ts
Expose new blob-explorer capabilities via application services and GraphQL tech-admin resolvers with strong input validation and authorization.
  • Add TechAdmin ListBlobContainers, ListBlobHierarchy, and GetBlobContent application-service functions that wrap the blob-storage operations, validate inputs, map records to key/value arrays, and optionally generate short-lived SAS URLs for downloads.
  • Introduce buildBlobListQueryCommand to validate/normalize GraphQL blob list query arguments (container, prefix, continuationToken, pageSize, filters).
  • Extend the ApplicationServices factory to pass blobStorageService and clientOperationsService into User and TechAdmin contexts, and wire new methods into TechAdminApplicationService and UserContextApplicationService.
  • Add GraphQL schema types and queries for BlobContainer, BlobFolder, BlobExplorerBlob, BlobHierarchyPage, BlobContent, and resolvers techAdminBlobContainers, techAdminBlobList, techAdminBlobContent with shared permission checks (assertCanViewBlobExplorer) and error mapping to BAD_USER_INPUT/UNAUTHENTICATED.
  • Update tech-admin resolver tests and Gherkin feature specs to cover authorization for blob queries, list/preview behavior, and database tests adjustments.
packages/ocom/application-services/src/contexts/tech-admin/index.ts
packages/ocom/application-services/src/contexts/tech-admin/blob-list.command-mapper.ts
packages/ocom/application-services/src/contexts/tech-admin/list-blob-containers.ts
packages/ocom/application-services/src/contexts/tech-admin/list-blob-hierarchy.ts
packages/ocom/application-services/src/contexts/tech-admin/get-blob-content.ts
packages/ocom/application-services/src/index.ts
packages/ocom/graphql/src/schema/types/tech-admin.graphql
packages/ocom/graphql/src/schema/types/tech-admin.resolvers.ts
packages/ocom/graphql/src/schema/types/tech-admin.resolvers.test.ts
packages/ocom/graphql/src/schema/types/features/tech-admin.resolvers.feature
packages/ocom/application-services/src/contexts/user/index.ts
packages/ocom-verification/acceptance-api/src/mock-application-services.ts
Add a staff-portal Blob Storage Explorer UI (route, container, and presentational component) integrated with GraphQL and staff permissions.
  • Introduce a BlobStorageExplorer React component with container selection, breadcrumb-based prefix navigation, server-side filtering controls (name, metadata, tags), hierarchical table view, load-more pagination, and a rich preview modal (text/image/PDF) with download support.
  • Add a BlobStorageExplorerContainer that wires Apollo queries for containers, list, and content, manages accumulated paging state, filters, prefix/container changes, and preview loading.
  • Expose a Blob Storage Explorer page under /staff/tech/blob-storage-explorer, wrapped in RequireRole and using the shared SubPageLayout.
  • Update section layout, staff route shell, shared navigation, and permission hooks (useStaffPermissions, RequireRole, StaffAuth, SectionLayout) to recognize the new canViewBlobExplorer permission and show tech-admin nav when applicable.
  • Add tests and Storybook stories for the Blob Storage Explorer UI and tweak existing database-explorer tests to be more robust with multiple matching elements.
packages/ocom/ui-staff-route-tech-admin/src/components/blob-storage-explorer.tsx
packages/ocom/ui-staff-route-tech-admin/src/components/blob-storage-explorer.container.tsx
packages/ocom/ui-staff-route-tech-admin/src/components/blob-storage-explorer.test.tsx
packages/ocom/ui-staff-route-tech-admin/src/components/blob-storage-explorer.stories.tsx
packages/ocom/ui-staff-route-tech-admin/src/pages/blob-storage-explorer.tsx
packages/ocom/ui-staff-route-tech-admin/src/index.tsx
packages/ocom/ui-staff-route-tech-admin/src/section-layout.tsx
packages/ocom/ui-staff-shared/src/section-layout.tsx
packages/ocom/ui-staff-shared/src/staff-route-shell.tsx
apps/ui-staff/src/hooks/use-staff-permissions.ts
apps/ui-staff/src/App.tsx
packages/ocom/ui-staff-shared/src/require-role.tsx
packages/ocom/ui-staff-shared/src/staff-route-shell.tsx
packages/ocom/ui-staff-route-tech-admin/src/components/database-explorer.test.tsx
apps/ui-staff/src/hooks/use-staff-permissions.ts

Possibly linked issues

  • #(none provided): PR fully implements the requested Blob Storage Explorer page, including auth, GraphQL, navigation, filtering, preview, download, and tests.

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 3 issues, and left some high level feedback:

  • In listBlobHierarchyPage, the folder loop skips all folders when request.metadataKey is set (if (request.metadataKey?.trim()) continue;), which contradicts the comment that folders should still appear for navigation; consider either removing this condition or aligning the comment and behavior so filtered listings still expose folder paths as intended.
  • The BlobStorageExplorer component creates object URLs for previews via URL.createObjectURL but never revokes them, which can leak memory over time; wrap this in a useEffect that revokes the URL in a cleanup function when the preview changes or the component unmounts.
  • BlobStorageExplorer maintains its own draftFilters state while the container also tracks filters and passes filters/onChangeFilters, leading to duplicated and potentially out-of-sync state; simplifying this so the container is the single source of truth (and the presentational component is fully controlled) will make filter behavior easier to reason about.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `listBlobHierarchyPage`, the folder loop skips all folders when `request.metadataKey` is set (`if (request.metadataKey?.trim()) continue;`), which contradicts the comment that folders should still appear for navigation; consider either removing this condition or aligning the comment and behavior so filtered listings still expose folder paths as intended.
- The `BlobStorageExplorer` component creates object URLs for previews via `URL.createObjectURL` but never revokes them, which can leak memory over time; wrap this in a `useEffect` that revokes the URL in a cleanup function when the preview changes or the component unmounts.
- `BlobStorageExplorer` maintains its own `draftFilters` state while the container also tracks filters and passes `filters`/`onChangeFilters`, leading to duplicated and potentially out-of-sync state; simplifying this so the container is the single source of truth (and the presentational component is fully controlled) will make filter behavior easier to reason about.

## Individual Comments

### Comment 1
<location path="packages/ocom/ui-staff-route-tech-admin/src/components/blob-storage-explorer.tsx" line_range="165" />
<code_context>
+	onViewBlob,
+	onClosePreview,
+}) => {
+	const [draftFilters, setDraftFilters] = useState(filters);
+
+	const breadcrumbItems = useMemo(() => {
</code_context>
<issue_to_address>
**issue (bug_risk):** Local filter state is not synced with `filters` prop, leading to potential stale UI when parent resets filters.

`draftFilters` is only initialized from `filters` and never updated when `filters` changes, so the component and its parent can diverge (e.g. after a parent reset or state restore). Consider either making `BlobStorageExplorer` fully controlled (use `filters` directly and call `onChangeFilters` on every change), or syncing `draftFilters` with `filters` via `useEffect(() => setDraftFilters(filters), [filters])` so external updates are reflected.
</issue_to_address>

### Comment 2
<location path="packages/ocom/ui-staff-route-tech-admin/src/components/blob-storage-explorer.tsx" line_range="291-300" />
<code_context>
+		},
+	];
+
+	const previewObjectUrl = useMemo(() => {
+		if (!preview?.contentBase64) {
+			return null;
+		}
+		if (!isImageContentType(preview.contentType) && !isPdfContentType(preview.contentType)) {
+			return null;
+		}
+		const binary = atob(preview.contentBase64);
+		const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
+		const blob = new Blob([bytes], { type: preview.contentType ?? 'application/octet-stream' });
+		return URL.createObjectURL(blob);
+	}, [preview]);
+
+	const handleDownload = () => {
</code_context>
<issue_to_address>
**issue (performance):** Object URL created for previews is never revoked, which can leak memory over time.

`previewObjectUrl` is created with `URL.createObjectURL` but never revoked, so repeated preview opens in a long-lived session can accumulate blob URLs and increase memory usage. Consider cleaning it up in an effect that runs when the URL changes/unmounts, e.g.:

```ts
const previewObjectUrl = useMemo(() => {
  if (!preview?.contentBase64) return null;
  if (!isImageContentType(preview.contentType) && !isPdfContentType(preview.contentType)) return null;
  const binary = atob(preview.contentBase64);
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
  const blob = new Blob([bytes], { type: preview.contentType ?? 'application/octet-stream' });
  return URL.createObjectURL(blob);
}, [preview]);

useEffect(() => () => {
  if (previewObjectUrl) URL.revokeObjectURL(previewObjectUrl);
}, [previewObjectUrl]);
```
</issue_to_address>

### Comment 3
<location path="packages/cellix/service-blob-storage/tests/index.test.ts" line_range="272-281" />
<code_context>
+			expect(result).toEqual([{ name: 'member-assets' }, { name: 'private' }]);
+		});
+
+		it('lists one hierarchy level with folders, blob properties, and a continuation token', async () => {
+			const service = new ServiceBlobStorage({ accountName });
+			await service.startUp();
+
+			const result = await service.listBlobHierarchy({
+				containerName: 'member-assets',
+				prefix: '',
+				pageSize: 20,
+			});
+
+			expect(listBlobsByHierarchyMock).toHaveBeenCalledWith('/', {
+				prefix: undefined,
+				includeMetadata: true,
</code_context>
<issue_to_address>
**suggestion (testing):** Hierarchy listing assertion is over‑specific about the `prefix` option and may become brittle.

In the `lists one hierarchy level with folders...` test, this assertion couples the test to the exact options shape. If the implementation simply omits a falsy `prefix`, the behaviour remains correct but this test will fail. To make it more robust, assert only the relevant fields:

```ts
expect(listBlobsByHierarchyMock).toHaveBeenCalledWith(
  '/',
  expect.objectContaining({
    includeMetadata: true,
    includeTags: true,
  }),
);
```

You can then add a separate test with a non‑empty prefix that explicitly checks the `prefix` value is forwarded.
</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.

onViewBlob,
onClosePreview,
}) => {
const [draftFilters, setDraftFilters] = useState(filters);

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.

issue (bug_risk): Local filter state is not synced with filters prop, leading to potential stale UI when parent resets filters.

draftFilters is only initialized from filters and never updated when filters changes, so the component and its parent can diverge (e.g. after a parent reset or state restore). Consider either making BlobStorageExplorer fully controlled (use filters directly and call onChangeFilters on every change), or syncing draftFilters with filters via useEffect(() => setDraftFilters(filters), [filters]) so external updates are reflected.

Comment on lines +291 to +300
const previewObjectUrl = useMemo(() => {
if (!preview?.contentBase64) {
return null;
}
if (!isImageContentType(preview.contentType) && !isPdfContentType(preview.contentType)) {
return null;
}
const binary = atob(preview.contentBase64);
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
const blob = new Blob([bytes], { type: preview.contentType ?? 'application/octet-stream' });

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.

issue (performance): Object URL created for previews is never revoked, which can leak memory over time.

previewObjectUrl is created with URL.createObjectURL but never revoked, so repeated preview opens in a long-lived session can accumulate blob URLs and increase memory usage. Consider cleaning it up in an effect that runs when the URL changes/unmounts, e.g.:

const previewObjectUrl = useMemo(() => {
  if (!preview?.contentBase64) return null;
  if (!isImageContentType(preview.contentType) && !isPdfContentType(preview.contentType)) return null;
  const binary = atob(preview.contentBase64);
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
  const blob = new Blob([bytes], { type: preview.contentType ?? 'application/octet-stream' });
  return URL.createObjectURL(blob);
}, [preview]);

useEffect(() => () => {
  if (previewObjectUrl) URL.revokeObjectURL(previewObjectUrl);
}, [previewObjectUrl]);

Comment on lines +272 to +281
it('lists one hierarchy level with folders, blob properties, and a continuation token', async () => {
const service = new ServiceBlobStorage({ accountName });
await service.startUp();

const result = await service.listBlobHierarchy({
containerName: 'member-assets',
prefix: '',
pageSize: 20,
});

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 (testing): Hierarchy listing assertion is over‑specific about the prefix option and may become brittle.

In the lists one hierarchy level with folders... test, this assertion couples the test to the exact options shape. If the implementation simply omits a falsy prefix, the behaviour remains correct but this test will fail. To make it more robust, assert only the relevant fields:

expect(listBlobsByHierarchyMock).toHaveBeenCalledWith(
  '/',
  expect.objectContaining({
    includeMetadata: true,
    includeTags: true,
  }),
);

You can then add a separate test with a non‑empty prefix that explicitly checks the prefix value is forwarded.

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.

1 participant