Skip to content

blob_showcase_infra - #314

Open
dani-vaibhav wants to merge 2 commits into
rohit-r-kumar/issue258from
blob_showcase
Open

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

Conversation

@dani-vaibhav

@dani-vaibhavdani-vaibhav commented Aug 6, 2026

Copy link
Copy Markdown

Summary by Sourcery

Enable authorized staff to explore, preview, and download Azure Blob Storage content through the Tech Admin portal while hardening local test-server startup and port management.

New Features:

  • Add a permission-gated staff Blob Storage Explorer for browsing containers and hierarchical blob listings with filtering, pagination, metadata, and tags.
  • Expose blob container listing, hierarchy exploration, bounded downloads, previews, and signed download URLs through application services and GraphQL.
  • Add a Tech Admin UI route with folder navigation, blob previews, filtering, refresh, and download actions.

Bug Fixes:

  • Make test-server port cleanup asynchronous and wait for ports to become available before starting replacement processes.
  • Make portless route pruning best-effort so stale route-lock contention does not prevent the test proxy from starting.

Enhancements:

  • Extend blob-storage service contracts with explorer and download operations while enforcing a 5 MB in-memory download limit.
  • Propagate canViewBlobExplorer permissions through staff authentication, navigation, routing, and GraphQL authorization.

Build:

  • Update workspace dependency overrides and security audit exceptions.

Documentation:

  • Document the expanded blob-storage operations and hierarchical explorer capabilities.

Tests:

  • Add coverage for blob hierarchy listing, filtering, tag queries, downloads, GraphQL authorization, explorer UI behavior, port cleanup retries, and portless startup recovery.

@sourcery-ai

sourcery-aiBot 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

ChangeDetailsFiles
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-aisourcery-aiBot 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
<locationpath="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
<locationpath="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.:
```tsconst previewObjectUrl =useMemo(() => {
if (!preview?.contentBase64) returnnull;
if (!isImageContentType(preview.contentType) &&!isPdfContentType(preview.contentType)) returnnull;
const binary =atob(preview.contentBase64);
const bytes =Uint8Array.from(binary, (char) =>char.charCodeAt(0));
const blob =newBlob([bytes], { type: preview.contentType??'application/octet-stream' });
returnURL.createObjectURL(blob);
}, [preview]);
useEffect(() => () => {
if (previewObjectUrl) URL.revokeObjectURL(previewObjectUrl);
}, [previewObjectUrl]);
```
</issue_to_address>
### Comment 3
<locationpath="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:
```tsexpect(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.:

constpreviewObjectUrl=useMemo(()=>{if(!preview?.contentBase64)returnnull;if(!isImageContentType(preview.contentType)&&!isPdfContentType(preview.contentType))returnnull;constbinary=atob(preview.contentBase64);constbytes=Uint8Array.from(binary,(char)=>char.charCodeAt(0));constblob=newBlob([bytes],{type: preview.contentType??'application/octet-stream'});returnURL.createObjectURL(blob);},[preview]);useEffect(()=>()=>{if(previewObjectUrl)URL.revokeObjectURL(previewObjectUrl);},[previewObjectUrl]);

Comment threadpackages/cellix/service-blob-storage/tests/index.test.ts
Sign up for freeto 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

@dani-vaibhav