Skip to content

feat(storage): support object version operations#103

Open
designcode wants to merge 1 commit into
mainfrom
feat/support-object-versions
Open

feat(storage): support object version operations#103
designcode wants to merge 1 commit into
mainfrom
feat/support-object-versions

Conversation

@designcode
Copy link
Copy Markdown
Collaborator

@designcode designcode commented May 12, 2026

Summary

  • Adds listVersions() to enumerate object versions + delete markers for a prefix, with key-marker / version-id-marker pagination.
  • Adds a versionId option to head, get, and remove — fetch / inspect a specific historical version, or permanently delete one (vs. the default delete-marker behavior on a versioned bucket).
  • Versioning is enabled implicitly by snapshots; pass enableSnapshot: true to createBucket. Closes TIG-8283.

All four operations route through the AWS SDK (ListObjectVersionsCommand / GetObjectCommand / HeadObjectCommand / DeleteObjectCommand), so SigV4 path encoding is handled by the SDK — the custom HTTP client gotcha documented in AGENTS.md does not apply here.

Test plan

  • pnpm --filter @tigrisdata/storage test — 150 / 150 passing
  • pnpm --filter @tigrisdata/storage build — clean
  • pnpm lint / pnpm format — clean
  • Integration test object-versions.integration.test.ts exercises:
    • listing multiple versions for the same key (incl. nested folder/key)
    • get(versionId) returning prior content; head(versionId) succeeding
    • remove() creating a delete marker on a versioned bucket
    • remove({ versionId }) permanently deleting one version
    • keyMarker / versionIdMarker pagination

Note

Medium Risk
Adds new version-listing API and changes get/head/remove to optionally target specific object versions, which can affect deletion semantics on snapshot/versioned buckets if misused.

Overview
Adds S3 object version support for snapshot-enabled buckets by introducing listVersions() to return object versions, delete markers, and pagination markers (nextKeyMarker/nextVersionIdMarker).

Extends get, head, and remove with an optional versionId that fetches/inspects a specific version or permanently deletes that version (otherwise remove follows S3 delete-marker behavior). Includes a new integration test suite covering version listing, historical reads, delete markers vs permanent deletes, and marker-based pagination.

Reviewed by Cursor Bugbot for commit 42fb3c9. Bugbot is set up for automated code reviews on this repo. Configure here.

Add S3 object-version operations on snapshot-enabled buckets:

- New `listVersions({ prefix, delimiter, limit, keyMarker, versionIdMarker })`
  returning the object versions and delete markers for a prefix, plus
  `nextKeyMarker` / `nextVersionIdMarker` for pagination.
- `head`, `get`, and `remove` now accept a `versionId` option. On `head` /
  `get` it selects the version; on `remove` it permanently deletes that
  version (without one, `remove` creates a delete marker on a versioned
  bucket, matching S3 semantics).

Versioning is enabled implicitly by snapshots — pass `enableSnapshot: true`
to `createBucket`.

Assisted-by: Claude Opus 4.7 (1M context) via Claude Code
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 12, 2026

Greptile Summary

This PR adds S3 object-version support to the storage SDK: a new listVersions() function and an optional versionId parameter on get, head, and remove. The additions follow existing SDK patterns closely, with one issue in the response-mapping logic of list-versions.ts that warrants attention before merging.

  • listVersions introduces ListObjectVersionsCommand with full pagination support (keyMarker/versionIdMarker) and correctly maps SDK results into typed ObjectVersion and DeleteMarker structures.
  • versionId is threaded into GetObjectCommand, HeadObjectCommand, and DeleteObjectCommand via minimal, consistent one-field additions; the signed URL in head correctly inherits the version ID.
  • The versionId field in mapped ObjectVersion/DeleteMarker results falls back to '' when the SDK returns undefined, which would silently produce an invalid version ID if a caller passes it directly into a subsequent get, head, or remove call.

Confidence Score: 3/5

Safe to merge after fixing the empty-string versionId fallback in list-versions.ts; the get/head/remove changes are minimal and low-risk.

The versionId ?? '' fallback in list-versions.ts would surface as a defect whenever callers chain listVersions results into downstream version-specific operations on buckets where the SDK returns undefined for the version ID. The same issue also affects the exported ObjectVersion and DeleteMarker types, which promise string but can silently carry an empty string. The rest of the changes are small and correct.

packages/storage/src/lib/object/list-versions.ts — specifically the versionId fallback value and the corresponding type declarations

Important Files Changed

Filename Overview
packages/storage/src/lib/object/list-versions.ts New listVersions implementation; versionId fallback to '' in mapped results could silently produce invalid version IDs when passed downstream, and the type declarations should reflect the optional nature of versionId.
packages/storage/src/lib/object/get.ts Adds optional versionId to GetObjectCommand; straightforward one-field addition consistent with existing patterns.
packages/storage/src/lib/object/head.ts Adds optional versionId to HeadObjectCommand; getSignedUrl will embed the version ID in the signed URL, which is expected S3 behavior.
packages/storage/src/lib/object/remove.ts Adds optional versionId to DeleteObjectCommand; minimal, correct change.
packages/storage/src/server.ts Exports new listVersions types and function from list-versions.ts; ordering is alphabetically consistent with the rest of the file.
packages/storage/src/test/object-versions.integration.test.ts Integration tests cover listing, versioned get/head, delete-marker creation, permanent version deletion, and key-marker/version-id-marker pagination; test always pairs both markers together (correct S3 usage).
.changeset/object-versions.md Changeset entry correctly marks the @tigrisdata/storage package as a minor bump and accurately describes the new API surface.

Reviews (1): Last reviewed commit: "feat(storage): support object version op..." | Re-trigger Greptile

Comment on lines +67 to +81
versions:
res.Versions?.map((v) => ({
name: v.Key ?? '',
versionId: v.VersionId ?? '',
isLatest: v.IsLatest ?? false,
size: v.Size ?? 0,
lastModified: v.LastModified ?? new Date(),
})) ?? [],
deleteMarkers:
res.DeleteMarkers?.map((m) => ({
name: m.Key ?? '',
versionId: m.VersionId ?? '',
isLatest: m.IsLatest ?? false,
lastModified: m.LastModified ?? new Date(),
})) ?? [],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 versionId fallback to empty string in ObjectVersion and DeleteMarker is dangerous. When VersionId is undefined (which can occur for objects in unversioned buckets or delete markers on suspended-versioning buckets), the returned versionId is ''. A caller who then passes v.versionId directly into get, head, or remove will send an empty string as the VersionId parameter, which S3 interprets as a malformed request and returns an error — rather than quietly fetching/deleting the latest version as the caller probably intended.

Suggested change
versions:
res.Versions?.map((v) => ({
name: v.Key ?? '',
versionId: v.VersionId ?? '',
isLatest: v.IsLatest ?? false,
size: v.Size ?? 0,
lastModified: v.LastModified ?? new Date(),
})) ?? [],
deleteMarkers:
res.DeleteMarkers?.map((m) => ({
name: m.Key ?? '',
versionId: m.VersionId ?? '',
isLatest: m.IsLatest ?? false,
lastModified: m.LastModified ?? new Date(),
})) ?? [],
versions:
res.Versions?.map((v) => ({
name: v.Key ?? '',
versionId: v.VersionId ?? undefined,
isLatest: v.IsLatest ?? false,
size: v.Size ?? 0,
lastModified: v.LastModified ?? new Date(),
})) ?? [],
deleteMarkers:
res.DeleteMarkers?.map((m) => ({
name: m.Key ?? '',
versionId: m.VersionId ?? undefined,
isLatest: m.IsLatest ?? false,
lastModified: m.LastModified ?? new Date(),
})) ?? [],

Comment on lines +16 to +29
export type ObjectVersion = {
name: string;
versionId: string;
isLatest: boolean;
size: number;
lastModified: Date;
};

export type DeleteMarker = {
name: string;
versionId: string;
isLatest: boolean;
lastModified: Date;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 The ObjectVersion type declares versionId as string (non-optional), but the mapped value can be undefined when the SDK omits VersionId. The type definitions should be updated to reflect that versionId may be absent so callers are forced to handle the undefined case before passing it downstream.

Suggested change
export type ObjectVersion = {
name: string;
versionId: string;
isLatest: boolean;
size: number;
lastModified: Date;
};
export type DeleteMarker = {
name: string;
versionId: string;
isLatest: boolean;
lastModified: Date;
};
export type ObjectVersion = {
name: string;
versionId: string | undefined;
isLatest: boolean;
size: number;
lastModified: Date;
};
export type DeleteMarker = {
name: string;
versionId: string | undefined;
isLatest: boolean;
lastModified: Date;
};

Comment on lines +7 to +14
export type ListVersionsOptions = {
delimiter?: string;
prefix?: string;
limit?: number;
keyMarker?: string;
versionIdMarker?: string;
config?: TigrisStorageConfig;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 VersionIdMarker silently ignored without KeyMarker

The S3 API states that version-id-marker is silently ignored when key-marker is not also present, causing the listing to restart from the beginning of the bucket. The current type exposes both fields independently, so a caller who passes versionIdMarker alone will get results starting from the top of the bucket with no error. Consider documenting this coupling in a JSDoc comment or asserting that keyMarker must be set whenever versionIdMarker is provided.

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