Files
suaveplan/openspec/archive/2026-04-24-enterprise-storage-s3/tasks.md
2026-05-27 23:29:53 +01:00

9.1 KiB
Raw Blame History

Tasks: enterprise-storage-s3

Change ID: enterprise-storage-s3 Package: @suaveplan/storage-s3 Target path: universal/storage/storage-s3 Prerequisite packages on Verdaccio: @suaveplan/storage, @suaveplan/error, @suaveplan/types, @suaveplan/retry


1. Scaffold

  • Create directory universal/storage/storage-s3/ with src/, tests/, src/__benchmarks__/, and all feature subdirectories per the layout in proposal.md.
  • Write package.json at version 0.1.0 with:
    • "type": "module", "sideEffects": false
    • exports map: top-level "." barrel plus subpaths ./s3/s3, ./types/s3.types, ./types/schemas
    • dependencies: @suaveplan/storage workspace:^, @suaveplan/error workspace:^, @suaveplan/types workspace:^, @suaveplan/retry workspace:^, @aws-sdk/client-s3 catalog:storage, @aws-sdk/s3-request-presigner catalog:storage, zod catalog:forms, @opentelemetry/api catalog:observability, tslib catalog:build
    • devDependencies: vitest catalog:testing, @vitest/coverage-istanbul catalog:testing, fast-check catalog:testing, mitata catalog:testing, @suaveplan/testing workspace:^
  • Write tsconfig.json extending @suaveplan/typescript-config/library.json.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config (pool: forks).
  • Add lint, typecheck, test, test:coverage, build, docs:check, bench scripts to package.json.

2. Types + errors

2a. S3 types (src/types/s3.types.ts + src/types/s3.types.md)

  • Define S3Options: bucket, region, accessKeyId?, secretAccessKey?, endpoint?, keyPrefix? — preserved from main.
  • Add genesis-only optional fields: retry?: RetryStrategyOptions, logger?: LoggerLike, signal?: AbortSignal (default for all ops).
  • Define PresignOptions: expiresIn?: number, method?: 'GET' | 'PUT'.
  • Define MultipartUploadOptions: partSize?: number, concurrency?: number, signal?: AbortSignal.
  • Write src/types/s3.types.md (≥200 words, API reference, examples).

2b. Error codes (src/error-codes.ts)

  • Import CodeMetadata, ErrorCodes, SuaveplanError from @suaveplan/error.
  • Define S3ErrorCode union — ten codes: S3_CREDENTIALS_MISSING, S3_CREDENTIALS_INVALID, S3_OBJECT_NOT_FOUND, S3_BUCKET_NOT_FOUND, S3_UPLOAD_FAILED, S3_DELETE_FAILED, S3_LIST_FAILED, S3_PRESIGN_FAILED, S3_MULTIPART_FAILED, S3_REGION_MISMATCH.
  • Define S3_ERROR_METADATA with httpStatus, severity, retryable, category, description per code.
  • Export registerS3ErrorCodes(). Idempotent; called eagerly at module scope.
  • Export S3StorageError extending SuaveplanError carrying S3ErrorCode discriminant.

2c. Zod schemas (src/types/schemas.ts + src/types/schemas.md)

  • Write S3OptionsSchema, PresignOptionsSchema, MultipartUploadOptionsSchema.
  • Write src/types/schemas.md (≥200 words).

3. Constants (src/constants/index.ts)

  • Export DEFAULT_PRESIGN_EXPIRES_SECONDS = 3600, DEFAULT_MULTIPART_PART_SIZE = 5 * 1024 * 1024, DEFAULT_MULTIPART_CONCURRENCY = 4.

4. Credential validator (src/utils/credential-validator.ts)

  • Implement isAwsS3(endpoint?: string): boolean — returns true if endpoint is undefined or hostname contains amazonaws.com.
  • Implement validateCredentials(options: S3Options): void:
    • Throws S3StorageError(S3_CREDENTIALS_MISSING) if accessKeyId or secretAccessKey absent (neither in options nor env).
    • Throws S3StorageError(S3_CREDENTIALS_INVALID) for invalid AWS AKIA key format when isAwsS3(options.endpoint) is true.
    • No validation for S3-compatible endpoints.
  • Write credential-validator.test.ts: valid AWS creds, invalid format, missing creds, S3-compatible endpoint skips validation.
  • Write credential-validator.md (≥200 words).

5. S3 class (src/s3/s3/)

  • Implement S3 extends AbstractStorage (import from @suaveplan/storage):
    • Constructor calls validateCredentials(options), builds S3Client with { region, credentials: { accessKeyId, secretAccessKey }, endpoint }.
    • Internal _resolveKey(key): string — applies options.keyPrefix stripping double-slashes.
    • get(key, options?):
      • Sends GetObjectCommand; reads response body as Uint8Array.
      • Throws S3StorageError(S3_OBJECT_NOT_FOUND) on NoSuchKey.
      • OTel span + counter. Retry wrapper if configured.
    • set(key, value, options?):
      • Sends PutObjectCommand.
      • Throws S3StorageError(S3_UPLOAD_FAILED) on error.
      • OTel span + histogram (object size attribute).
    • delete(key):
      • Sends DeleteObjectCommand.
      • Throws S3StorageError(S3_DELETE_FAILED) on error.
    • list(prefix?, options?):
      • Sends ListObjectsV2Command with pagination (handles NextContinuationToken).
      • Throws S3StorageError(S3_LIST_FAILED) on error.
    • exists(key): sends HeadObjectCommand; returns true on 200, false on NoSuchKey.
    • metadata(key): parses HeadObjectCommand response into StorageMetadata.
    • copy(source, destination): sends CopyObjectCommand.
    • move(source, destination): copy then delete.
    • presign(key, options?): uses getSignedUrl from @aws-sdk/s3-request-presigner. Throws S3StorageError(S3_PRESIGN_FAILED).
    • setMultipart(key, stream, options?):
      • CreateMultipartUploadCommand → upload parts with concurrency cap → CompleteMultipartUploadCommand.
      • On any error: AbortMultipartUploadCommand then rethrow as S3StorageError(S3_MULTIPART_FAILED).
      • Respects options.signal (abort mid-upload).
    • All methods accept per-call signal?: AbortSignal that overrides the instance-level default.
    • Log via injected logger at debug level.
  • Write s3.test.ts (unit: inject a mock S3Client via constructor option; no vi.mock — DI only). Cover: get hit, get miss (404), set, delete, list with pagination, exists true/false, copy, move, presign, credential validation failure.
  • Write s3.md (≥200 words, both basic + advanced example with retry config).

6. Registry (src/registry/registry/)

  • registry.ts — imports S3 and calls StorageFactory.register('s3', S3) as a side-effect on import.
  • Write registry.md explaining the side-effect and tree-shaking implications.

7. Public barrel (src/index.ts)

  • Re-export: S3, S3StorageError, S3OptionsSchema, PresignOptionsSchema, MultipartUploadOptionsSchema, all types, error-codes, constants.
  • Import ./registry/registry to trigger provider registration.

8. Property-based tests (tests/property.test.ts)

Use fast-check. All timeouts: { timeout: 30000 }.

  • Key prefix normalisation: for arbitrary key and prefix strings (alphanum + /), assert resolveKey(prefix, key) never produces double slashes and always starts with prefix.
  • isAwsS3 detection: for arbitrary endpoint strings, assert isAwsS3 returns true iff hostname contains amazonaws.com.
  • List pagination assembly: given M pages of N/M keys each, assert the assembled list equals the concatenation of all pages in order.

9. Integration tests (tests/integration.test.ts)

  • Gate all tests behind process.env.GARAGE_ENDPOINT skip guard.
  • Test: put → get → exists → metadata → list → copy → move → delete against a real Garage S3-compatible server.
  • Test: multipart upload of a 15 MiB buffer (3 × 5 MiB parts).
  • Test: presigned GET URL is accessible without credentials.

10. Benchmarks (src/__benchmarks__/s3.bench.ts)

Use mitata. Gate IO benchmarks behind SKIP_S3_BENCHMARKS env flag.

  • get_hot_100 (conditional): 100 get() calls on a pre-existing 1 KiB object.
  • set_1mb (conditional): upload a 1 MiB buffer.
  • list_1000 (conditional): list 1000 objects.
  • credential_validate_1k (pure): 1000 credential validation calls.

11. Docs

  • src/types/s3.types.md
  • src/types/schemas.md
  • src/s3/s3/s3.md
  • src/registry/registry/registry.md
  • src/utils/credential-validator.md
  • Root universal/storage/storage-s3/README.md.

12. Verify and publish

  • Run bunx turbo lint --filter=@suaveplan/storage-s3 — zero errors.
  • Run bunx turbo typecheck --filter=@suaveplan/storage-s3 — zero errors.
  • Run bunx turbo test --filter=@suaveplan/storage-s3 — all tests pass.
  • Run bunx turbo test:coverage --filter=@suaveplan/storage-s3 — 100% on all touched modules.
  • Run bun run docs:check --filter=@suaveplan/storage-s3 — zero errors.
  • Confirm no TODO, FIXME, @deprecated, untyped any, ts-ignore, or .skip in src/ or tests/.
  • Confirm "sideEffects": false in package.json.
  • Run bun scripts/publish.ts --package @suaveplan/storage-s3.
  • Update CLAUDE.md package listing to include @suaveplan/storage-s3.
  • Archive: move openspec/changes/enterprise-storage-s3/ to openspec/archive/2026-04-24-enterprise-storage-s3/ and fold spec delta into openspec/specs/storage/spec.md.