From a81dfd21395c74be539b254fffbf12d8ba2a67e1 Mon Sep 17 00:00:00 2001 From: apicircle-dev Date: Sun, 19 Jul 2026 08:07:02 +0530 Subject: [PATCH] feat(ui-components): export readAttachmentBytes for edition/overlay byte access Additive public accessor readAttachmentBytes(slotId) on @apicircle/ui-components: reads a stored attachment's raw bytes by its GlobalFileAsset.slotId (null when absent), re-exported from the package root. Lets an edition/overlay resolve an asset's bytes -- e.g. parse a stored OpenAPI spec for code-vs-spec drift -- without depending on the internal AttachmentRecord shape or the IndexedDB layer. Purely additive; no existing behavior changes. Co-located test + CHANGELOG (Unreleased). Typecheck + lint green. --- CHANGELOG.md | 12 ++++++++++++ packages/ui-components/src/index.ts | 1 + .../src/persistence/attachments.test.ts | 17 +++++++++++++++++ .../src/persistence/attachments.ts | 13 +++++++++++++ 4 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1841fba3..29b49f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,18 @@ > ship. Full per-platform walk-through: > [`docs/installing.md`](docs/installing.md). +## Unreleased + +### Added + +- **`readAttachmentBytes(slotId)` public accessor (`@apicircle/ui-components`).** + A narrow, additive export that reads a stored attachment's raw bytes by its + `GlobalFileAsset.slotId` (or `null` when absent), so an edition / overlay can + resolve an asset's bytes — e.g. parse a stored OpenAPI spec for code-vs-spec + drift — without reaching into the internal `AttachmentRecord` shape or the + IndexedDB layer. Purely additive; no existing behavior changes. + (`@apicircle/ui-components`) + ## 1.3.0 - 2026-07-18 _All workspace packages move to **1.3.0** in lockstep — the published diff --git a/packages/ui-components/src/index.ts b/packages/ui-components/src/index.ts index 3037a0e9..c65aa258 100644 --- a/packages/ui-components/src/index.ts +++ b/packages/ui-components/src/index.ts @@ -4,6 +4,7 @@ export { applyFont, ALL_FONTS } from './theme/applyFont'; export type { FontFamilyId, FontFamilyDef } from './theme/applyFont'; export { applyFontSize, clampFontSizePercent } from './theme/applyFontSize'; export { useWorkspaceStore } from './store/workspaceStore'; +export { readAttachmentBytes } from './persistence/attachments'; export { Button, Input, Modal, cn } from './primitives'; export { PANELS, getPanel } from './layout/panels'; export type { PanelDef } from './layout/panels'; diff --git a/packages/ui-components/src/persistence/attachments.test.ts b/packages/ui-components/src/persistence/attachments.test.ts index 99aa8121..a4a9f17e 100644 --- a/packages/ui-components/src/persistence/attachments.test.ts +++ b/packages/ui-components/src/persistence/attachments.test.ts @@ -6,6 +6,7 @@ import { getAttachment, materializeAttachment, putAttachment, + readAttachmentBytes, } from './attachments'; const sampleBytes = (str: string) => new TextEncoder().encode(str); @@ -32,6 +33,22 @@ describe('attachments IDB store', () => { expect(await getAttachment('nope')).toBeNull(); }); + it('readAttachmentBytes returns the stored bytes, or null when absent', async () => { + await putAttachment({ + slotId: 'slot-bytes', + filename: 'spec.json', + mimeType: 'application/json', + size: 5, + sha256: 'ab', + savedAt: '2026-04-27T00:00:00.000Z', + bytes: sampleBytes('hello'), + }); + const bytes = await readAttachmentBytes('slot-bytes'); + expect(bytes).not.toBeNull(); + expect(new TextDecoder().decode(bytes!)).toBe('hello'); + expect(await readAttachmentBytes('missing')).toBeNull(); + }); + it('deleteAttachment removes the record', async () => { await putAttachment({ slotId: 'slot-2', diff --git a/packages/ui-components/src/persistence/attachments.ts b/packages/ui-components/src/persistence/attachments.ts index 83dc3ba4..30ad4412 100644 --- a/packages/ui-components/src/persistence/attachments.ts +++ b/packages/ui-components/src/persistence/attachments.ts @@ -64,6 +64,19 @@ export async function getAttachment(slotId: string): Promise { + const record = await getAttachment(slotId); + return record ? record.bytes : null; +} + export async function deleteAttachment(slotId: string): Promise { const db = await openDb(); return new Promise((resolve, reject) => {