Skip to content

refactor(api-headless-cms): storage ops - #5490

Merged
brunozoric merged 68 commits into
nextfrom
bruno/refactor/api-headless-cms-storage-ops
Jul 29, 2026
Merged

refactor(api-headless-cms): storage ops#5490
brunozoric merged 68 commits into
nextfrom
bruno/refactor/api-headless-cms-storage-ops

Conversation

@brunozoric

@brunozoric brunozoric commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace monolithic storage operations with per-method DI abstractions — each of the 22 entry storage operations (create, update, publish, list, etc.) is now an independent abstraction with one implementation per adapter,
    registered via createImplementation/createDecorator
  • Delete StorageOperationsFactory, HeadlessCmsStorageOperations, CmsEntryStorageOperations interface, SqlEntryOperations monolith, and EntryWriteOperations — ~4,500 lines of legacy wiring removed
  • PG-OS write ops use decorator pattern — 11 decorators wrap SQL per-method implementations with OpenSearch sync, replacing the intermediate EntryWriteOperations delegation layer
  • OpenSearch index event handlers extracted as proper DI classesModelAfterCreateHandler, ModelAfterCreateFromHandler, ModelAfterDeleteHandler live in api-headless-cms-utils-os and are shared by both DDB-ES and PG-OS
    adapters
  • Clean up 24 test files referencing deleted HeadlessCmsStorageOperations type

Packages changed

Package What changed
api-headless-cms 24 per-method storage abstractions (group, model, 22 entry); deleted CmsEntryStorageOperations interface, StorageOperationsFactory, legacy bridge code
api-headless-cms-ddb 22 entry DI classes (DdbCreateEntry, etc.), DataLoadersHandler as DI impl, DdbGroupStorageOperations, DdbModelStorageOperations
api-headless-cms-ddb-es 22 entry DI classes (DdbEsCreateEntry, etc.), CmsDdbEsDataLoaders abstraction, test converted from legacy plugin to DI
api-headless-cms-sql 22 entry DI classes (SqlCreateEntry, etc.), deleted 728-line SqlEntryOperations monolith
api-headless-cms-pg-os 11 write decorators + 3 search impls + 8 SQL reuse; deleted EntryWriteOperations, write/*.ts files; dropped unused HeadlessCmsPgOsFeature export
api-headless-cms-utils-os 3 model lifecycle event handler DI classes for OpenSearch index create/delete
api-headless-cms-testing Removed dead HeadlessCmsStorageOperations type
13 test packages Removed dead type imports from test handlers

Architecture

Before: Adapters implemented a monolithic CmsEntryStorageOperations interface (22 methods). A StorageOperationsFactory wired the monolith at runtime. PG-OS delegated writes through EntryWriteOperations
SqlEntryOperations monolith.

After: Each storage operation is an independent DI abstraction. Adapters register per-method implementations via createImplementation. PG-OS write ops are decorators (registerDecorator) that wrap SQL implementations with
OpenSearch sync. No factories, no registrars, no runtime wiring.

Core abstraction (CreateEntryStorageOperation)
→ SqlCreateEntry (SQL adapter, via createImplementation)
→ PgOsCreateEntry (PG-OS decorator, wraps SQL + adds OS sync)

Rules established

  • Entry ops registered at app-scope via adapter features (container is per-request, singleton = per-request)
  • One abstraction per file, one implementation per file
  • Shared utils (convertToStorageEntry, createKeys, etc.) as importable functions, not DI-injected
  • OpenSearch index lifecycle handlers are shared DI classes in api-headless-cms-utils-os

Test plan

  • DDB: 64/64 tests passing (yarn test packages/api-headless-cms-ddb)
  • SQL: build passing (no unit tests in package)
  • DDB-ES: 109/110 passing (yarn test:os packages/api-headless-cms-ddb-es) — 1 intermittent ordering failure in createIndexTask (passes in isolation)
  • PG-OS: 9/9 passing (yarn test:os packages/api-headless-cms-pg-os) — syncStream tests require running Postgres
  • All 5 affected packages build clean
  • Full CI

brunozoric and others added 30 commits July 26, 2026 10:27
…ch 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…storage abstractions (batch 1)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d barrel export

Adds registerCmsStorageOperations(container, registry) and the
ICmsStorageOperationsRegistry interface, which TypeScript uses to enforce
that a storage adapter provides all 24 per-method CMS storage abstractions
(group, model, 22 entry abstractions) in one call. Also adds the
storageOperations barrel export and re-exports from exports/api/cms/storage.ts.
…dlessCmsFeature

Calls registerCmsStorageOperations() right after the legacy StorageOperations
registration, wiring the group/model storage ops and all 22 entry methods
(wrapped as { execute } objects) from the existing storageOperations object
into the new per-method DI abstractions. Both registrations coexist during
the migration.
…perations consumers

Migrates the last consumers of the legacy monolithic StorageOperations
abstraction to the per-method storage operation abstractions:
CmsEntriesCrudDecorators (getRevisionById, getLatestRevisionByEntryId),
DeleteModel (list), and two webhooks integration tests that also relied
on the legacy abstraction to inspect raw stored entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s abstraction

All consumers now resolve the per-method storage operation abstractions
via registerCmsStorageOperations. Drops the StorageOperations DI token
and namespace from features/shared/abstractions.ts, and removes its
registration/resolution from HeadlessCmsFeature.ts. The facade's
`storageOperations` field is now populated directly from the local
variable instead of a container round-trip. StorageOperationsFactory
and the storage bootstrapping (create/beforeInit/bridge) are unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…peration abstractions

CmsModel and other types are re-exported from ~/types/index.js, not
directly from ~/types/types.js where they are defined.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…allback

HeadlessCmsFeature now tries CmsEntryStorageOpsRegistrar first (new-style
adapters like DDB). Falls back to StorageOperationsFactory for legacy
adapters (ddb-es, sql, pg-os). StorageOperationsFactory kept with
@deprecated marker until all adapters migrate.

Removed storageOperations field from HeadlessCms interface/facade.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tional deprecated field

- Separate resolve from register call so bugs in registrar surface directly
- Keep storageOperations on HeadlessCms interface as optional+deprecated
  for backward compat with existing tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…I abstractions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ly via DI

Replace StorageOperationsFactory with direct DI registration, same
pattern as DDB adapter. Group and model ops are DI classes
(app-scoped singletons), entry ops use the registrar pattern
(per-request via CmsEntryStorageOpsRegistrar).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…via DI

Replace StorageOperationsFactory with direct DI registration.
Group and model ops are DI classes (app-scoped singletons),
entry ops bridged via SqlCmsEntryStorageOpsRegistrar.
Remove dead factory functions and types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…geOperations

Arrow wrappers instead of bare method refs — class-based entry ops
(SQL adapter) use this internally, which breaks when methods are
detached. Also fix SQL registrar to resolve entry ops per-request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y via DI

Replace StorageOperationsFactory with direct DI registration. Reuse
SqlGroupStorageOperations and SqlModelStorageOperations for group/model
ops. Entry ops bridged via PgOsCmsEntryStorageOpsRegistrar composing
EntryWriteOperations + EntrySearchOperations + SqlEntryOperations.

All four adapters (DDB, DDB+ES, SQL, PG+OS) now use direct DI
registration. Legacy StorageOperationsFactory path kept as fallback
for external adapters only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
brunozoric and others added 22 commits July 27, 2026 15:10
… DI implementations

PG-OS entry storage ops now use named impl classes with
createImplementation — 14 PG-OS specific (delegating to WriteOps
and SearchOps) + 8 SQL DI classes registered directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Delete SqlEntryOperations 728-line monolith — PG-OS write ops now
decorate SQL per-method implementations directly via registerDecorator,
adding OpenSearch sync as a layer instead of delegating through an
intermediate EntryWriteOperations abstraction.

Remove CmsEntryStorageOperations monolithic interface from types (param
types kept). Remove DDB/DDB-ES extended interfaces that were defined but
never imported.

Clean up HeadlessCmsStorageOperations dead type from 24 test files and
createCmsTestHandler.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…export

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…nSearch index configs

Resolve CmsEntryOpenSearchIndexCreate lazily in model event handlers
instead of eagerly during feature registration. Eager resolution
captured only the index configs registered at that point, preventing
later registrations (custom CmsEntryOpenSearchIndex impls) from being
picked up.

Convert DDB-ES test createIndexConfigurationPlugin from legacy
RegisterExtensionPlugin to pure DI (createImplementation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…andlers as proper DI classes

Replace registerFactory + anonymous objects in DDB-ES and PG-OS features
with proper DI implementation classes: ModelAfterCreateHandler,
ModelAfterCreateFromHandler, ModelAfterDeleteHandler.

These are now registered centrally in CmsEntryOpenSearchUtilsFeature,
eliminating duplicate wiring in each adapter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r-method DI classes

Same pattern as entry ops — each of the 5 group methods (get, list,
create, update, delete) and 5 model methods is now an independent DI
abstraction with one implementation per adapter.

Delete monolithic GroupStorageOperations and ModelStorageOperations
interfaces and their adapter implementations. Rewire 10 consumer
repositories to resolve per-method abstractions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…egistration

Each adapter now has DdbGroupStorageOpsFeature, DdbModelStorageOpsFeature
(and DdbEs/Sql variants) that encapsulate per-method DI registration.
Root features just call Feature.register(container) instead of listing
10 individual container.register() calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…operations/

Consistent file naming: all adapter storage ops features are now
at operations/{group,model,entry}/feature.ts instead of named files
at src/ root.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace registerDynamoDbStorageOperations, registerCmsOpenSearchStorageOperations,
and registerSqlStorageOperations with direct Feature.register() calls. All callers
migrated to use HeadlessCmsDdbFeature, HeadlessCmsDdbEsFeature, and
HeadlessCmsSqlFeature directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move infrastructure registrations into HeadlessCmsPgOsFeature.register()
with config param, matching HeadlessCmsSqlFeature pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…e createImplementation()

Replace standalone `createImplementation({ abstraction: X, ... })` calls with
`X.createImplementation({ ... })` across all headless CMS storage operation
packages. The standalone import from `@webiny/feature/api` belongs to the
abstraction layer and should not be used directly in implementation packages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ModelStorageOperations was removed when storage ops were split into
per-method DI classes. Update the test helper to resolve and call the
new DeleteModelStorageOperation abstraction instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same fix as helpers.ts — ModelStorageOperations was removed, replaced
with per-method abstractions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tenant index creation order is non-deterministic. Use
expect.arrayContaining instead of exact array match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When OPENSEARCH_SHARED_INDEXES is enabled, deleting a model must not
delete the OpenSearch index because other tenants still use it. Add
an early return guard in CmsEntryOpenSearchIndexDeleteImpl.

Also remove unused CmsModel type import in pg-os EntrySearchOperations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brunozoric brunozoric self-assigned this Jul 29, 2026
@brunozoric brunozoric added this to the 6.6.0 milestone Jul 29, 2026
brunozoric and others added 4 commits July 29, 2026 09:22
- Alias abstraction imports as XAbstraction when export name matches
- Make impl classes private (not exported), named XImpl
- Export name matches abstraction name (no Impl suffix)
- index.ts exports only abstractions and features, not implementations
- Apply to api-headless-cms-utils-os (7 files), api-graphql (1 file),
  api-opensearch (1 file)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drop Impl suffix from filenames — file matches class/export name.
Update feature.ts import paths accordingly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createFeature<Foo | undefined> now makes register's context parameter
optional at the type level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Container type is inferred from createFeature's register signature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@brunozoric
brunozoric marked this pull request as ready for review July 29, 2026 08:44
@brunozoric
brunozoric merged commit 8a670dd into next Jul 29, 2026
118 of 121 checks passed
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