Skip to content

chore(api): migrate all api-* packages to DI-native handler architecture#5313

Open
adrians5j wants to merge 270 commits into
nextfrom
cloudi-with-next
Open

chore(api): migrate all api-* packages to DI-native handler architecture#5313
adrians5j wants to merge 270 commits into
nextfrom
cloudi-with-next

Conversation

@adrians5j

@adrians5j adrians5j commented Jun 19, 2026

Copy link
Copy Markdown
Member

What changed

This PR finishes the rollout of the DI-native (createHandler / createFeature) approach across all api-* packages. Every package that previously used the legacy createLambdaHandler + ApiGatewayFeature or createRawHandler patterns has been migrated to wire up through the DI container instead. That includes both the production entry-points (stream handlers, GraphQL handlers, task runners) and the test helpers that exercise them.

A few correctness issues surfaced during the migration and are fixed here:

  • ApiCoreContextEnhancer ordering: The core enhancer (which sets up ctx.security, ctx.tenancy, ctx.wcp) is now registered as an instance so that resolveAll(GraphQLContextEnhancer) always places it before other enhancers that depend on those context fields.
  • ModelCache race condition in @webiny/testing: RegisterExtensionPlugin instances are pre-registered into the DI container before HeadlessCmsFeature enhancers run, so private models (e.g. wbyWorkflow) reach ModelCache before AcoContextEnhancer populates it.
  • Static plugin bridging in @webiny/testing: Non-apply plugins (CmsModelPlugin, CmsGroupPlugin, etc.) are forwarded as extraPlugins to HeadlessCmsFeature.register so that PluginModelsProvider can find them via ctx.plugins.
  • CMS test routing: useGraphQLHandler.invoke in api-headless-cms was posting to /graphql, but the full CMS schema (createContentModelGroup and all model-specific mutations) is only served at /cms/<type>. Requests now go to the correct endpoint.
  • GraphQLEngineImpl base types: Base type Query / type Mutation root types are now injected automatically with assumeValidSDL: true, so individual features can use extend type Mutation without needing to define the root themselves.
  • api-dynamodb-to-elasticsearch: Added createDdbToEsStreamHandler factory that wires the DynamoDB stream handler through the DI container, replacing the ad-hoc construction in call-sites.

Changelog

Title line: Migrate all api-* packages to DI-native handler architecture

Body: All backend packages now use a single, consistent dependency-injection pattern for bootstrapping Lambda handlers, GraphQL engines, and background task runners. Previously each package had its own ad-hoc wiring; they now all go through the same createHandler / createFeature pipeline. Several bugs in context-enhancer ordering and model registration were fixed as part of this work.

Squash Merge Commit

chore(api): migrate all api-* packages to DI-native handler architecture (#5313)
feat(api): adopt createHandler/createFeature across all api-* packages (#5313)

adrians5j and others added 30 commits December 22, 2025 10:59
# Conflicts:
#	extensions/MyApiKeyAfterUpdate.ts
#	packages/api-core/package.json
#	packages/api-core/src/ApiCoreFeature.ts
#	packages/api-core/src/features/logger/LoggerService.ts
#	packages/api-core/src/features/logger/abstractions.ts
#	packages/api-core/src/features/logger/feature.ts
#	packages/api-core/src/features/logger/index.ts
#	packages/handler-aws/package.json
#	packages/handler-aws/src/createHandler.ts
#	packages/webiny/package.json
#	yarn.lock
…routing

- @webiny/event-handler-core: EventHandler, HttpEventHandler, EventType, EventContext,
  HttpRouter, IHttpRoute, SecureHeadersDecorator, HttpFeature, ErrorHandler,
  NotFoundHandler, HttpTenantInitializer, HttpTenantIdExtractor
- @webiny/event-handler-aws: createLambdaHandler, ApiGatewayEventType,
  FunctionUrlEventType, S3/SQS/SNS/EventBridge/DynamoDB event types,
  ApiGatewayTranslator, FunctionUrlTranslator, AwsHttpTranslator,
  S3TenantIdExtractor, S3TenantInitializer, S3Feature
- @webiny/event-handler-node: createNodeHandler, NodeHttpEventType, NodeHttpTranslator
- packages/ev-test: example app wiring both Node and Lambda setups with tenant context

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
… ErrorHandler, SecureHeaders)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…outer, not a chain handler

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…rs, S3TenantIdExtractor)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Exports TestHttpEventType and createTestHandler from @webiny/event-handler-core/testing.
Feed IHttpRequest directly into the chain in tests — no AWS/Node transport needed.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- @webiny/event-handler-core: RequestContainer abstraction (per-request child container),
  registered automatically in createHandler/createLambdaHandler on each request
- @webiny/event-handler-core: HttpRouterImpl changed to transient so per-request
  routes (e.g. GraphQLRoute) resolve from child container
- @webiny/handler-graphql: IGraphQLEngine abstraction + GraphQLEngineImpl (wraps
  GraphQLSchemaComposer + graphql execution, passes {container} as contextValue)
- @webiny/handler-graphql: GraphQLRoute IHttpRoute (POST /graphql → GraphQLEngine)
- @webiny/handler-graphql: GraphQLEngineFeature groups all registrations

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

- @webiny/event-handler-core: RequestContainer (per-request child container injection),
  fix createTestHandler to await async root setup, remove console.error from ErrorHandler
- @webiny/handler-graphql: IGraphQLContextEnhancer abstraction, GraphQLContextEnhancer
  extensibility point so packages can add context.security/tenancy/wcp to resolver context
- @webiny/api-core: ApiCoreContextEnhancer (adds security/tenancy/wcp to GraphQL context),
  ApiCoreSchemaFactory (CoreGraphQLSchemaFactory bridge for old GraphQLSchemaPlugin schemas),
  HttpTenantInitializer moved from event-handler-core (breaks circular dep),
  WcpFeature + NullLicense added to ApiCoreFeature, ApiCoreSchemaFactory in ApiCoreFeature
- packages/api-core/__tests__/useGqlHandler.ts: rewritten using createTestHandler —
  no more @webiny/handler-aws, uses DI-native event handler chain

138/144 api-core tests pass with new handler. 4 remaining relate to WCP AACL mock
integration and test state persistence (pre-existing behavior differences).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	packages/background-tasks/package.json
#	yarn.lock
…arate files

- __tests__/mocks/TestAuthenticator.ts
- __tests__/mocks/TestAuthorizer.ts
- __tests__/handlers/AuthTriggerHandler.ts
- __tests__/handlers/RootTenantInitializer.ts
- ExtraSchemaFactory named class, registered via registerFactory

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…y in tests

- parallelQueries.ts: convert withoutAuthorizationPlugin to withoutAuthorizationFactory
  (GraphQLSchemaFactory implementation — proper DI-native pattern)
- useGqlHandler.ts: remove GraphQLSchemaPlugin bridge entirely, add schemaFactories option
  for registering GraphQLSchemaFactory implementations directly

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
…extPlugin items directly

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

TenantContext, IdentityContext and other per-request singletons from ApiCoreFeature
now live in the child container automatically — no special workarounds needed.
159/162 api-core tests pass (1 remaining: WCP/AACL mock integration).

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

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

- roles.test.ts: RoleFactory.createImplementation replaces ContextPlugin wrapper
- teams.test.ts: RoleFactory + TeamFactory createImplementation
- lifecycleEvents.ts: 12 named tracker handler classes + createImplementation,
  functions return arrays of implementations instead of a ContextPlugin
- useGqlHandler.ts: support arrow function setup callbacks and DI class implementations
  (distinguish via plugin.prototype check), remove ContextPlugin import

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
adrians5j and others added 27 commits June 25, 2026 13:12
…x schemaPlugins build-type read

- 12 test files: context.cms.* -> context.container.resolve(HeadlessCms).* (token).
  Validated: entry create/publish, storage-ops, traverser tests pass via the token.
  (generateSchema.manage/read failures are pre-existing, confirmed at baseline.)
- schemaPlugins: revert to reading the (forked) context.cms for type/READ. These are
  BUILD-time schema params set per endpoint by CmsSchemaExecutor's fork, NOT the
  request-fixed facade, so resolve(HeadlessCms) was incorrect here. Added a TODO to
  thread `type` through getSchema -> generateSchema -> generateSchemaPlugins, which is
  the remaining blocker to fully drop the ctx.cms bag.

State: HeadlessCms token in place; all query-time consumers + tests use it; ctx.cms
still built as a bridge until the build-type threading lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…nly (HeadlessCms)

Removes the `cms` property from the CmsContext type and stops setting `ctx.cms` in both
init paths. The CMS facade is resolved exclusively via container.resolve(HeadlessCms).

- Thread the BUILD-time endpoint `type` through getSchema -> generateSchema ->
  buildSchemaPlugins -> generateSchemaPlugins, so schema generation no longer reads
  context.cms.type/READ (those are per-endpoint build params, forked by CmsSchemaExecutor).
- HeadlessCmsContextEnhancer / legacy context.ts: build the facade as a local, register it
  as HeadlessCms, drop `ctx.cms = {...}`; simplify the CmsSchemaExecutor fork (no cms override).
- HeadlessCmsContextualSchema: take `type` from HeadlessCmsEnhancerConfig instead of cms.type.
- DeleteModelWithEntryCleanup, modelFields validation: resolve(HeadlessCms).
- Tests (hcms + hcms-tasks): context.cms.* -> resolve(HeadlessCms); drop obsolete
  `if (!context.cms)` guards.
- CmsContext is now `Context & DbContext & ApiCoreContext` (no cms) so any stray reader is a
  compile error.

Validated: hcms + api-aco build clean (cms removed from type); read+manage schema-build,
entry create/publish, storage-ops tests pass via the token. Pre-existing failures
(generateSchema.manage/read, republish.entries, tasks crud "No registration for DbInstance")
confirmed at baseline — unrelated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…text.ts/extension.ts)

Removes the pre-DI CMS init: src/context.ts (createContextPlugin — the legacy
ContextPlugin whose apply(context) set up the CMS on the old plugin handler) and
extension.ts (createCmsExtension). Production has used the DI-native HeadlessCmsFeature
for some time; the only remaining consumer was the converter test helper (usePlugins),
which over-pulled the entire extension just to get field converters.

- converters test helper: build a PluginsContainer from createFieldConverters() directly
  (ConverterCollection only needs CmsModelFieldConverterPlugin instances)
- delete usePlugins.ts test helper
- drop createCmsExtension / ICreateCmsExtensionParams from the package public exports

hcms builds clean; converter tests pass (the 2 transient file-level failures are the
known parallel-run port-conflict flakiness — both pass in isolation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…ences

generateTsConfigsInPackages built `references`/`paths` in tsconfig.build.json from
dependencies + devDependencies, so @webiny/testing (a test-only devDep used only from
__tests__, never from src) got injected as a build reference. Since packages like
api-audit-logs devDepend on @webiny/testing AND @webiny/testing depends back on them,
this created a circular project reference. With composite + ts.createProgram, the cycle
makes TS resolve a project's own not-yet-emitted .d.ts, throwing TS6305 across every file
on a clean build (masked previously only because dist was never fully wiped).

tsconfig.build.json compiles only `src`, which never imports test-only packages, so
excluding them from the build config is safe. The dev tsconfig.json (which compiles
__tests__) still references them. Regenerated affected tsconfig.build.json files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…acade, used as context)

The AuditLogsContext DI token was typed as the AuditLogsContextValue facade, but it is
registered with — and consumed as — the full audit-logs request context (handlers use
.container and pass it to getAuditConfig). The wrong type param produced ~75 TS errors,
previously masked by the TS6305 project-reference cycle.

Type it as the full context to unblock the build. The deeper cleanup (drop whole-context
injection for an AuditLogs facade token + remove context.auditLogs) is the context-removal
migration, deferred (revisit after api-headless-cms).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…hase 3)

Deletes the legacy GraphQL-via-ContextPlugin wrappers, all unreachable after the
extension.ts/createGraphQL path was removed (the DI-native HeadlessCmsFeature already
registers typeDefs/resolvers + base-schema factory + exportPlugin directly):

- delete graphql/index.ts (createGraphQL) and graphql/schema/cms/index.ts (createCmsSchema)
- drop createBaseSchema (ContextPlugin) from graphql/schema/baseSchema.ts; keep
  createBaseSchemaPlugins (used by the enhancer)
- drop createExportGraphQL (ContextPlugin) from export/graphql/index.ts; keep exportPlugin

No ContextPlugin remains in api-headless-cms/src. hcms builds clean; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…ametersPlugin (Phase 2a)

The CmsParametersPlugin registry (path/header/context parameter plugins) determined the
endpoint type for the legacy context.ts init, now deleted. Endpoint type is sourced from
HeadlessCmsEnhancerConfig.type via DI everywhere; the parameters/ subsystem + CmsParametersPlugin
had no remaining consumers (only the deleted extension.ts).

Removes src/parameters/, plugins/CmsParametersPlugin.ts, __tests__/parameters/, and the
plugins/index.ts re-export. hcms builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…r token) — Phase 2b/1

Replace the CmsModelFieldConverterPlugin plugins-container registry with a multi-instance
CmsFieldConverter DI token:
- new ~/fieldConverters/abstractions.ts: CmsFieldConverter token + registerFieldConverters()
- ConverterCollection + valueKeyTo/FromStorageConverter take a resolved fieldConverters[] array
  instead of a PluginsContainer
- createCmsModelFieldConvertersAttachFactory resolves CmsFieldConverter from the container
- enhancer registers field converters via DI; drops them from ctx.plugins; removes the now-redundant
  CmsSchemaExecutor context fork (uses the request context directly)

ctx.plugins now only carries legacy extension plugins (model/group), removed in 2b/2 + 2c.
hcms builds clean; converter tests 112/112, entry storage 3/3 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…o DI (Phase 2b/2c)

Finishes moving the remaining CMS plugin types off the plugins container onto DI
tokens, and fixes schema-plugin sourcing gaps surfaced by the earlier
CmsGraphQLSchemaFactory migration (42786b).

Phase 2b/2 — StorageOperationsCmsModelPlugin -> CmsStorageModelProvider:
- Add CmsStorageModelProvider DI token; the enhancer registers the storage-model
  implementation under it instead of ctx.plugins.register.
- Storage adapters (storage/ddb/ddb-es/sql) resolve the token from the container
  instead of plugins.oneByType.

Phase 2c — CmsModelPlugin/CmsGroupPlugin -> DI:
- Add multi-instance CmsModelPluginInstance / CmsGroupPluginInstance tokens.
- The enhancer registers code-defined model/group plugins (from extraPlugins)
  under these tokens; Plugin{Models,Groups}Provider inject them via
  {multiple:true} instead of reading cmsContext.plugins.byType.

Schema-plugin sourcing fixes:
- validation/modelFields.ts: build the model-validation schema from
  CmsGraphQLSchemaFactory (base types) instead of ctx.plugins, mirroring
  generateSchema — fixes "Unknown type CmsError/CmsIdentity/..." on model create.
- enhancer: bridge user-provided CmsGraphQLSchemaPlugin (extraPlugins) to
  CmsGraphQLSchemaFactory so generateSchema includes schema extensions again.
- Regenerate generateSchema manage/read empty-schema snapshots (now the unified
  base schema; superset, no types removed).

Tests:
- Migrate fieldIdStorageConverter, pluginsContentModels, entryPagination off the
  removed ctx.cms / converter-plugins-container reads.

ddb package tests green (41/41); core entry CRUD green (46/46). Remaining hcms
failures (benchmark output flush, sdkGraphql cms-namespace) are pre-existing and
unrelated to this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…S SDK API

Fixes the two remaining pre-existing api-headless-cms test failures (benchmark +
sdkGraphql), both unrelated to the plugin/context migration.

Benchmark output flush:
- The request lifecycle never called benchmark.output() after the ctx.benchmark ->
  BenchmarkAbstraction migration (38d5faa), so measurements were never emitted. The
  CMS route now flushes via container.resolve(BenchmarkAbstraction).output() after
  executing the request (no-op unless benchmarking was enabled).
- Restore the "headlessCms.graphql.createRequestBody" measure in CmsSchemaExecutor
  (also reinstates request-body validation via createRequestBody), which had been
  dropped in 8200cdf. Export createRequestBody from @webiny/handler-graphql.
- Update benchmark.test.ts to the actual DI request flow (getSchema ->
  createRequestBody -> processRequestBody); the per-operation CRUD measures live on
  the HeadlessCms facade, which the DI resolvers no longer route through.

SDK (unified cms namespace):
- The SDK targets `mutation { cms { createEntry ... } }`, served by the core GraphQL
  engine at /graphql (CoreGraphQLSchemaFactory impls already registered by
  HeadlessCmsFeature). The test helper routed SDK requests to /cms/* (the per-model
  schema), which has no `cms` field. useWebinySdk now routes to /graphql.

api-headless-cms suite green: 0 test failures (the few file-level fails are the
documented EADDRINUSE jest-dynalite port-conflict flake under parallel runs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…(HeadlessCms)

deleteModel/index.ts still read `inputContext.cms.MANAGE`, a leftover from the
Phase-1 ctx.cms bag removal (3aedc4b) that broke `yarn build` with
TS2339 "Property 'cms' does not exist on type 'T'". Resolve the HeadlessCms facade
from the DI container instead. This also clears the cascading TS6305 on
api-background-tasks-os (the aborted build had left api-headless-cms-es-tasks/dist
half-built). Full `yarn build` passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…SchemaPlugin usage

ApiCoreSchemaFactory created legacy GraphQLSchemaPlugin objects only to immediately
unpack them into the DI GraphQLSchemaBuilder via a local addPluginsToBuilder + a
duplicated addResolvers (a copy of builder.addLegacyResolvers); plugin.isApplicable
was never even consulted. Removed the bridge:

- security/{base,apiKey,role,team,identity}.gql, users/user.gql,
  system/createSystemGraphQL, wcp/graphql now export plain
  GraphQLSchemaDefinition objects instead of `new GraphQLSchemaPlugin(...)`.
- ApiCoreSchemaFactory feeds the builder directly (addTypeDefs +
  addLegacyResolvers), deleting addPluginsToBuilder and the duplicated addResolvers.

The 4 create*GraphQL helpers are api-core-internal (only consumed by
ApiCoreSchemaFactory), so no public API change. GraphQLSchemaPlugin remains the
framework-wide authoring API for other packages/user code; this only removes
api-core's own use of it. api-core 165/165 tests pass; full `yarn build` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…hema

createSchema() in baseSchema.ts built the base types as a CmsGraphQLSchemaPlugin
AND a duplicate core GraphQLSchemaPlugin (corePlugin), but the only caller,
createBaseSchemaPlugins, destructured [cmsPlugin] and discarded corePlugin —
leftover from the Phase-3 core-schema-path removal. Collapsed createSchema into
createBaseSchemaPlugins, removed the dead corePlugin and the now-unused core
GraphQLSchemaPlugin / IGraphQLSchemaPlugin imports. hcms now uses only the CMS
plugin API internally for its schema. Base schema output unchanged
(generateSchema snapshots + group CRUD pass); full `yarn build` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…text to base Context

A — context.benchmark -> DI:
- The CRUD factories (contentModel, contentModelGroup, contentEntry) read
  context.benchmark.measure(...) — a service-locator off the merged context. Each
  factory now resolves BenchmarkAbstraction once (context.container.resolve) and
  uses that. 31 call sites migrated; BenchmarkAbstraction is the same instance the
  enhancer registers, so benchmark output is unchanged.

B — drop the merged CmsContext type:
- CmsContext was `Context & DbContext & ApiCoreContext`, but ApiCoreContext aliases
  the base Context and DbContext only adds an unused `db` — nothing in hcms or any
  dependent reads .db/.tenancy/.security. Collapsed to `export type CmsContext =
  Context` and removed the now-unused DbContext/ApiCoreContext imports. This also
  made an obsolete `@ts-expect-error` in RefToGraphQL.ts unused (removed it).

Verified: full `yarn build --no-cache` across all 128 packages green (so no
dependent relied on the merge); hcms suite 856 pass / 0 fail. (C — plugin->factory
schema/DI-resolver migration — is the next step.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…extPlugin

createAcoHcmsContext() was a legacy ContextPlugin that did
SetLocationOnEntryRestoreFeature.register(context.container) — but AcoHcmsFeature
already performs the identical registration, and createAcoHcmsContext had zero
references anywhere (the project template + tests use AcoHcmsFeature.register).
Removed it; api-headless-cms-aco now has no ContextPlugin and no context-bag reads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
Collapsing CmsContext to base Context (67d2aaa) removed the last
@webiny/handler-db import (the DbContext type). adio flagged it as an unused
dependency; removed it from package.json and regenerated tsconfigs. Bonus: the
context cleanup also sheds a package dependency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
createRootTenantMock, customAuthenticator, customGroupAuthorizer, and
triggerAuthentication were legacy ContextPlugin-based test mocks with zero
references — superseded by the DI-native RootTenantInitializer decorator and
TestAuthenticator/TestAuthorizer (used by useGqlHandler). customGroupAuthorizer
also still read context.security off the bag. api-core now has zero ContextPlugin
usages. 165/165 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…ng from CMS route

The CMS GraphQL route resolved [GraphQLContextEnhancer, { multiple: true }] and ran
an enhance(ctx) loop, but nothing ever registers a GraphQLContextEnhancer — the only
registrar (registerLegacyPluginsViaGqlContextEnhancer) has zero callers repo-wide.
So the dependency always resolved to [] and the loop was a no-op. Dropped the
enhancers constructor param, the dependency, the loop, and the now-unused
GraphQLContextEnhancer/IGraphQLContextEnhancer imports. CMS route tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…eadlessCmsInitializer.ts

The file's misleading name predates the DI migration: it contains
HeadlessCmsInitializerImpl, which `implements IGraphQLContextualSchema` (a
contextual schema with build(ctx)), not a GraphQLContextEnhancer. Renamed the file
to match the class and updated all internal import paths. No symbol/API changes
(HeadlessCmsEnhancerConfig kept). Build + CMS route tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…ce() comments

Three comments referenced HeadlessCmsContextEnhancer.enhance() — both the old
filename (now HeadlessCmsInitializer) and a method that never existed (it's a
GraphQLContextualSchema with build(), not an enhancer). Updated them to reference
the CMS contextual schema (HeadlessCmsInitializer.build()). Comments only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…a build

RecordLockingContextualSchema.build() builds a standalone executable schema from a
generated CMS-model schema whose date/json fields reference base scalars (DateTime,
JSON, ...), but it only passed "type Query\ntype Mutation" + the model typeDefs —
never declaring those scalars — so makeExecutableSchema threw "Unknown type
DateTime" at runtime on the core /graphql endpoint. The normal CMS schema build
(buildSchemaPlugins) always includes createBaseContentSchema() first; do the same
here so the standalone schema is self-contained before the engine merges it.

Pre-existing since the record-locking DI migration (d93f519); surfaced now on
deploy. The package's GraphQL tests were already red before this change (and before
this session) on an unrelated test-wiring gap — they fail resolving CmsContext/
AccessControl during build(), never reaching the schema-build line — so production
(which registers those) reaches this path but the tests don't.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…ix construction order

RecordLockingContextualSchema injected GetModelUseCase/ListModelsUseCase/
CmsModelFieldToGraphQLRegistry as constructor dependencies. The GraphQL engine
constructs all GraphQLContextualSchema implementations eagerly (when resolving the
engine), and those use-cases depend on AccessControl — which the CMS initializer
only registers later, during its own build(). So record-locking's construction
failed with "No registration found for AccessControl" (and "CmsContext" before the
context collapse) before any build() ran, which is why its GraphQL tests had been
red since the DI migration (d93f519).

Resolve those use-cases lazily inside build() (from ctx.container) instead — by
then the CMS initializer has run and registered AccessControl/CmsContext. This is
the same request-time pattern the CMS initializer itself uses.

api-record-locking now green: 18/18 tests pass — which also end-to-end verifies the
base-scalars fix in 4fb2844.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…init (no stub schema)

Request-scoped initializers (the CMS facade/storage/AccessControl setup, task/
bulk-action bridges) were hijacking GraphQLContextualSchema.build() purely for its
per-request, post-enhancer timing — returning a throwaway "type Query / type
Mutation" schema to satisfy the interface, since they contribute no schema content.

Introduce a dedicated GraphQLContextInitializer abstraction whose init(ctx) returns
void. The GraphQL engine and the CMS route now run initializers AFTER context
enhancers and BEFORE contextual schemas — so contextual schemas (e.g.
record-locking) reliably see what initializers register (AccessControl/CmsContext),
by construction rather than by registration-order luck.

Migrated HeadlessCmsInitializer to it (dropping the stub-schema return and the
makeExecutableSchema/GraphQLSchema/IGraphQLContextualSchema imports). The remaining
costume-wearers (HcmsTasksInitializerImpl and the bulk-action bridges) still use
the contextual-schema stub and can be migrated next, now that the proper
abstraction exists and is easy to find.

Verified: api-headless-cms 856/0, api-record-locking 18/18, full build green.
handler-graphql's 6 pre-existing debug-logging test failures are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…ContextInitializer

Several classes implemented IGraphQLContextualSchema purely for its per-request
build() timing hook, returning a throwaway `type Query\ntype Mutation` stub
schema they never used. They are request initializers wearing a contextual-schema
costume. Migrate them to the dedicated GraphQLContextInitializer abstraction so
their intent (side-effecting per-request init, no schema contribution) is explicit
and they run in the initializer phase (after context enhancers, before contextual
schemas) rather than emitting a fake schema.

Migrated:
- api-file-manager: FileModelContextualSchema (resolves GetModelUseCase lazily)
- api-scheduler: SchedulerModelContextualSchema
- api-workflows: WorkflowsInitializer
- api-headless-cms-tasks: HcmsTasksFeature initializer
- api-aco: AcoFeature initializer (AcoSchemaFactory kept; it builds real schema)
- handler-graphql: registerLegacyPluginsViaGqlContextualSchema legacy bridge
- testing: TenancyAndSecurityFeature — seeds tenants and authenticates identity;
  as an initializer it now runs before the other initializers, so tenant/identity
  are established before they need them (previously it set them in build(), too
  late for the migrated initializers).

Drop the now-unused @graphql-tools/schema dependency from the migrated feature
packages and the unused graphql-tag from handler-graphql.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
…pers

These test helpers manually replicate the GraphQL engine flow (resolveAll the
enhancers, then contextual schemas, calling enhance()/build() directly). Now that
the migrated costume-wearers live under GraphQLContextInitializer, add the missing
initializer step — resolveAll(GraphQLContextInitializer) and call init(ctx) after
enhancers and before contextual schemas — so they run the same three phases the
real engine does. Without this, the migrated initializers never run and the
use-cases/models they register are missing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
HttpRouter injects [HttpRoute, { multiple: true }] and so eagerly constructs every
route on each request to match the path. AssetDeliveryRoute took AssetRequestResolver,
AssetResolver, AssetProcessor and AssetOutputStrategy as constructor deps, and
AssetProcessor's PrivateFilesAssetProcessor decorator depends on GetFileUseCase, whose
chain (GetFileRepository -> GetEntryByIdUseCase -> ... -> GetEntriesByIdsRepository)
needs EntryFromStorageTransform. That token is only registered while a CMS GraphQL
request is in flight, so constructing the asset route for an unrelated request (e.g.
/graphql) threw "No registration found for EntryFromStorageTransform" before any handler
ran — failing all file-manager GraphQL tests.

Inject the request Container instead and resolve the four collaborators inside handle(),
so route construction stays cheap and the CMS-entry chain is only resolved when an actual
/files/* request is served.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
useGqlHandler declared a `plugins` param but never registered it, so tests passing
legacy ContextPlugins (e.g. assignFileLifecycleEvents, which registers the File
before/after create/update/delete event-handler DI instances) had no effect and the
lifecycle assertions saw 0 invocations.

Bridge `params.plugins` via registerLegacyPluginsViaGqlContextualSchema so each
plugin's apply(ctx) runs per request (initializer phase, before resolvers) and its
DI registrations are in place when the file CRUD use-cases dispatch their events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
@adrians5j

Copy link
Copy Markdown
Member Author

/vitest

@github-actions

Copy link
Copy Markdown

Vitest tests have been initiated (for more information, click here). ✨

…ollaborators lazily

WebsiteBuilderRedirectsRoute injected GetActiveRedirectsUseCase as a constructor dep,
whose chain pulls request-time CMS tokens (RedirectModel, ListLatestEntriesUseCase ->
EntryFromStorageTransform). Because HttpRouter eagerly constructs every route per request
to path-match, this is the same eager-construction trap fixed in AssetDeliveryRoute. It
doesn't fire today only because WebsiteBuilderFeature pairs the route with
setupWebsiteBuilderModels() (pre-registers those tokens in the request callback before
routing) and tests wire WB via the legacy createWebsiteBuilder() plugin instead. Harden
it regardless: inject RequestContainer and resolve IdentityContext + GetActiveRedirectsUseCase
inside handle(), so it can't regress if that setup ordering ever changes.

Also add a TODO at HttpRouter documenting the systemic root cause — eager construction of
all routes via [HttpRoute, { multiple: true }] — and the proper fix (construct only the
matched route lazily), so the per-route workaround can eventually be removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg3MRCToopzWSTPWqU9Lga
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.

3 participants