feat: define server composition APIs - #34
Conversation
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`.
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>
|
Review finding → fixed in 4d36c52: Rather than banning contributed
The commit also adds the two composition claims that were untested: conflicting contributions throw ( Gates run locally on this branch: |
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-localdefineTypeOrmRepositoryhelper into your project, and always supplyuserMetadataeven if you never wanted/me. Three of those four are boilerplate the framework can own.After this PR:
@Module({ imports: [RocketsModule.forRoot(...)] })createServer(definition)→ pass straight toNestFactory.create()providers+exportsdefineAuthAdapter(Adapter, options?)defineTypeOrmRepositoryfrom@concepta/rockets-repository-typeorm/meuserMetadatawas requireduserMetadatais optional;/memounts only when a contract existsAuth 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)createServer(definition)— returns the Nest entryDynamicModule.RocketsModule.forRoot()remains the lower-level surface.resolveRocketsComposition).userMetadataoptional:/me, its DTO token, and the metadata handlers register only when a contract exists./typeormsubpath, and therockets-swaggerCLI.@concepta/rockets-coredefineAuthAdapter(Adapter, options?)andAuthBootstrapContributions.AuthServerGuardhonors the upstream class-level public-route sentinel ('classLevel'), verified against@concepta/nestjs-authentication'sAuthPublic.@concepta/rockets-repository-typeormdefineTypeOrmRepository()— it is no longer an app-local snippet.@nestjs/typeormbecomes a required peer (the package now builds the TypeORM root module directly).@concepta/rockets-adapter-firebasedefineFirebaseAuth({ firebaseApp })instead of{ forRoot: { firebaseApp } }.Breaking changes & migration
Also removed: the
@concepta/rockets/typeormsubpath and therockets-swaggerbin (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+.userMetadatabecoming 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:
enableGlobalGuardas a contribution is fail-open. An auth integration can setcontributes.enableGlobalGuard = false, which drops the app-wideAPP_GUARDunless the app explicitly setstrue. 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.tsContribution conflict detection uses reference equality. Two integrations contributing structurally identical but distinct
userMetadataobjects will throw. Fail-loud is probably right, but it is a deliberate choice. →resolveSingleContributionThe 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 populatingcontributeslands 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.mdresolveRocketsCompositionruns 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.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 spanningexamples/, 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:
defineRocketsAuth()populatingcontributes; coreAuthPublic({ classLevel }); auth-package swagger CLI removaldefineTypeOrmRepositorycopies; rootREADME.mdandCONFIGURATION.mdupdatesType of Change
Verification
All gates run locally on a clean
yarn install:yarn buildyarn typecheck:specyarn test— 64 files / 585 testsyarn test:e2e— 32 files / 163 testsyarn lintandyarn lint:allTwo verification notes:
yarn buildexits 0 whennode_modulesis missing. If you review from a clean checkout, install first or the build result is meaningless.expected 401, got 500and anETIMEDOUT); both pass in isolation, and the only delta from a passing run was three markdown files. This matches the pattern already documented atCHANGELOG.md:91. No assertions were weakened.New coverage added here: direct
createServerlaunch, 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 withNest can't resolve dependencies of the GetUserMetadataHandler).Checklist