Skip to content

feat: define server composition APIs - #34

Merged
tnramalho merged 7 commits into
mainfrom
agent/rockets-server-composition
Aug 10, 2026
Merged

feat: define server composition APIs#34
tnramalho merged 7 commits into
mainfrom
agent/rockets-server-composition

Conversation

@leoafarias

@leoafarias leoafarias commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

Introduce the definition-first server composition surface for the 1.0 preview.

Before this PR, standing up a Rockets app meant hand-assembling a Nest module: wrap RocketsModule.forRoot() in an @Module, write your own auth host module to register an adapter, copy an app-local defineTypeOrmRepository helper into your project, and always supply userMetadata even if you never wanted /me. Three of those four are boilerplate the framework can own.

After this PR:

Need Before After
Boot a server @Module({ imports: [RocketsModule.forRoot(...)] }) createServer(definition) → pass straight to NestFactory.create()
Register a custom auth adapter Hand-written host module with providers + exports defineAuthAdapter(Adapter, options?)
Wire TypeORM App-local helper copied per project defineTypeOrmRepository from @concepta/rockets-repository-typeorm
An app with no /me Not possible — userMetadata was required userMetadata is optional; /me mounts only when a contract exists

Auth integrations can additionally contribute the resources, metadata contract, repository, and guard preference they own, so an app doesn't have to restate what its identity provider already knows. Explicit app options always win; two integrations contributing conflicting defaults fail loudly at composition time.


Changes by package

@concepta/rockets (server)

  • Add createServer(definition) — returns the Nest entry DynamicModule. RocketsModule.forRoot() remains the lower-level surface.
  • Resolve auth contributions once, before module registration (resolveRocketsComposition).
  • Make userMetadata optional: /me, its DTO token, and the metadata handlers register only when a contract exists.
  • Drop the TypeORM dependency, the /typeorm subpath, and the rockets-swagger CLI.

@concepta/rockets-core

  • Add defineAuthAdapter(Adapter, options?) and AuthBootstrapContributions.
  • AuthServerGuard honors the upstream class-level public-route sentinel ('classLevel'), verified against @concepta/nestjs-authentication's AuthPublic.
  • Register each user-metadata CQRS handler independently of the other.

@concepta/rockets-repository-typeorm

  • Own defineTypeOrmRepository() — it is no longer an app-local snippet.
  • @nestjs/typeorm becomes a required peer (the package now builds the TypeORM root module directly).

@concepta/rockets-adapter-firebase

  • Flatten synchronous options: defineFirebaseAuth({ firebaseApp }) instead of { forRoot: { firebaseApp } }.

Breaking changes & migration

- import { defineTypeOrmRepository } from '@concepta/rockets';
+ import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm';

- createStubAuthBootstrap(MyAdapter)
+ defineAuthAdapter(MyAdapter)

- defineFirebaseAuth({ forRoot: { firebaseApp } })
+ defineFirebaseAuth({ firebaseApp })

- import type { RocketsAuthInput } from '@concepta/rockets';
+ import type { RocketsAuthOption } from '@concepta/rockets';

Also removed: the @concepta/rockets/typeorm subpath and the rockets-swagger bin (OpenAPI generation belongs to the consumer app, which alone owns the complete Nest graph and document settings). Packages in this slice now require Node.js 20+.

userMetadata becoming optional is a silent behavior change worth calling out: an app that previously failed fast with "user-metadata config is required" will now boot successfully without /me.


For the reviewer

Things I'd want a second opinion on, roughly in order of importance:

  1. enableGlobalGuard as a contribution is fail-open. An auth integration can set contributes.enableGlobalGuard = false, which drops the app-wide APP_GUARD unless the app explicitly sets true. That is intentional and covered by a test, but it means a third-party auth package can turn off the global guard by default. Worth confirming that's the posture we want. → packages/rockets-server/src/rockets.module-definition.ts

  2. Contribution conflict detection uses reference equality. Two integrations contributing structurally identical but distinct userMetadata objects will throw. Fail-loud is probably right, but it is a deliberate choice. → resolveSingleContribution

  3. The README "Path B" paragraph is forward-looking. It states that defineRocketsAuth() contributes its persistence rows, repository, metadata contract, and guard preference. The mechanism ships here; defineRocketsAuth() actually populating contributes lands in fix: harden auth recovery and request context #36, which is stacked directly on this branch. If this merges alone, that paragraph is briefly ahead of the code. → packages/rockets-server/README.md

  4. resolveRocketsComposition runs three times per module build (imports, controllers, providers). Cheap and side-effect-free, but it does re-run conflict detection each time. Left as-is for readability — flag if you'd rather it were threaded through once.

  5. Known cleanup deliberately left out of scope: UserMetadataEntityInterface extends BaseUserMetadataEntityInterface {} is a bare alias in interface form, same category as the two aliases removed here. It has 91 references spanning examples/, which chore: enforce release readiness #35 owns, and its file isn't otherwise part of this PR. Happy to do it as its own change.

Scope boundaries with the stack

This is the base of a four-PR stack (#34#36#37#35). Several adjacent items are deliberately not here because a downstream PR owns them:

Item Owner
defineRocketsAuth() populating contributes; core AuthPublic({ classLevel }); auth-package swagger CLI removal #36
Firestore parity, its engines / status / changelog #37
Deleting the 3 example-local defineTypeOrmRepository copies; root README.md and CONFIGURATION.md updates #35

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Dependency update

Verification

All gates run locally on a clean yarn install:

  • yarn build
  • yarn typecheck:spec
  • yarn test — 64 files / 585 tests
  • yarn test:e2e — 32 files / 163 tests
  • yarn lint and yarn lint:all

Two verification notes:

  • yarn build exits 0 when node_modules is missing. If you review from a clean checkout, install first or the build result is meaningless.
  • Intermittent e2e flake is environmental, not from this PR. One of four full runs failed with two rotating victims (expected 401, got 500 and an ETIMEDOUT); both pass in isolation, and the only delta from a passing run was three markdown files. This matches the pattern already documented at CHANGELOG.md:91. No assertions were weakened.

New coverage added here: direct createServer launch, ordered multi-credential auth, private-by-default routes, metadata-free micro apps, and the per-handler metadata gate (that last one was confirmed to fail before its fix with Nest can't resolve dependencies of the GetUserMetadataHandler).

Checklist

  • Code follows existing patterns in the codebase
  • Relevant documentation and CHANGELOGs updated
  • Tests added for new functionality

Gate each user-metadata CQRS handler on its own override, so a partial
`handlers` config without `userMetadata` no longer registers a built-in
handler that cannot resolve the user-metadata dynamic repository.

Derive the Firebase async-branch exclusion from
`keyof FirebaseAuthModuleOptions`. The hand-written `never` list already
missed the inherited `imports`, which let a sync key ride along with
`forRootAsync` and be silently dropped by `forRoot()`.

Declare `@nestjs/typeorm` as a devDependency of
`@concepta/rockets-repository-typeorm`, which compiles against it and
previously resolved it only through workspace hoisting.

Replace `as unknown as` namespace shadows in the new specs with typed
index imports, so `typecheck:spec` actually covers `createServer`,
`defineAuthAdapter`, and `defineTypeOrmRepository`.

BREAKING CHANGE: `createStubAuthBootstrap()` is removed. It had become an
alias for `defineAuthAdapter()`, which builds the same host module and
also accepts imports, controllers, providers, exports, and `contributes`.
Replace `createStubAuthBootstrap(Adapter)` with `defineAuthAdapter(Adapter)`.
`RocketsServerDefinition` was a bare alias of `RocketsOptions` with one
consumer: the `createServer()` parameter it annotated. `createServer()`
now takes `RocketsOptions` directly.

`RocketsAuthInput` was a `@deprecated` bare alias of `RocketsAuthOption`
with no consumers at all. It was also the only name for that union on the
public surface, so `RocketsAuthOption` is exported in its place.

Checked the other alias-shaped declarations in the packages this branch
touches and kept the ones that earn their name: `EntityHookContext` has
~70 consumers and is the documented name in the hooks subsystem, `WireRow`
pairs with `PersistenceRow` as a domain distinction, and the
`InjectDynamicRepository` / `InjectCrudAdapter` wrappers widen the upstream
string-only decorators to accept an entity class.

BREAKING CHANGE: the `RocketsServerDefinition` and `RocketsAuthInput` type
exports are removed. Use `RocketsOptions` and `RocketsAuthOption`.
@leoafarias
leoafarias marked this pull request as ready for review August 9, 2026 16:01
@leoafarias leoafarias mentioned this pull request Aug 9, 2026
13 tasks
A contribution may swap the global guard, never remove it: honoring a
contributed enableGlobalGuard: false with no replacement silently
publishes every route. resolveRocketsComposition now throws unless the
integration declares contributes.providesAppGuard (guard swap) or the
app itself sets enableGlobalGuard: false (intentionally public API).

Also covers the two previously untested composition claims: conflicting
contributions throw, and explicit options beat contributed defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tnramalho

tnramalho commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Review finding → fixed in 4d36c52: contributes.enableGlobalGuard was fail-open — a third-party auth package could contribute false and drop the app-wide APP_GUARD with no error, no warning, and no replacement check.

Rather than banning contributed false (which would break the legitimate guard-swap case, e.g. defineRocketsAuth() replacing AuthServerGuard with upstream's JwtGuard), resolveRocketsComposition now enforces a composition invariant — an integration may swap the guard, never remove it:

  • contributed false + contributes.providesAppGuard: true → guard swap, boots normally;
  • explicit app-level enableGlobalGuard: false → intentionally public API, always passes;
  • contributed false with no declared replacement and no app opt-out → throws at composition time with a message pointing at both escape hatches.

providesAppGuard is a new optional field on AuthBootstrapContributions. Follow-up for #36: defineRocketsAuth() should contribute { enableGlobalGuard: false, providesAppGuard: true }, which also closes the half-configured appGuard: false state there (it now fails loudly instead of booting unguarded).

The commit also adds the two composition claims that were untested: conflicting contributions throw (userMetadata), and explicit options beat contributed defaults (enableGlobalGuard: true vs contributed false).

Gates run locally on this branch: yarn build, yarn typecheck:spec, yarn test (590), yarn test:e2e (163), yarn lint — all green.

@tnramalho tnramalho self-assigned this Aug 10, 2026
@tnramalho
tnramalho merged commit 3052878 into main Aug 10, 2026
1 check passed
@tnramalho
tnramalho deleted the agent/rockets-server-composition branch August 10, 2026 17:16
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.

2 participants