diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3b3f4f651..693c19c36 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -55,4 +55,4 @@ body: - type: input attributes: label: NestJS version - placeholder: "^11.0.0" + placeholder: "12.0.0-alpha.5" diff --git a/.github/workflows/ci-pr-test.yml b/.github/workflows/ci-pr-test.yml index d6aa3ff32..3dbfe056b 100644 --- a/.github/workflows/ci-pr-test.yml +++ b/.github/workflows/ci-pr-test.yml @@ -37,6 +37,8 @@ jobs: run: yarn lint:all - name: Typecheck tests run: yarn typecheck:spec + - name: Verify native Vitest config loading + run: yarn test:config-native - name: Unit tests run: yarn test:ci - name: E2E coverage diff --git a/.github/workflows/release-readiness.yml b/.github/workflows/release-readiness.yml new file mode 100644 index 000000000..62bd79a8f --- /dev/null +++ b/.github/workflows/release-readiness.yml @@ -0,0 +1,32 @@ +name: release-readiness + +on: + pull_request: + branches: ['main'] + push: + branches: ['main'] + +jobs: + release-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Enable Corepack + run: corepack enable + - uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: yarn + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - name: Cache Firestore emulator + uses: actions/cache@v4 + with: + path: ~/.cache/firebase/emulators + key: firestore-emulator-${{ runner.os }}-${{ hashFiles('yarn.lock') }} + - name: Install + run: corepack yarn install --immutable + - name: Release readiness + run: corepack yarn release:check diff --git a/CHANGELOG.md b/CHANGELOG.md index be8beb5ae..a2bec985e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,9 @@ Vitest 4. What this deletes, permanently: dependency. The root `scripts/` directory no longer exists. The setup follows Vitest 4's official monorepo guidance — the -`projects` model: the root `vitest.config.ts` declares every project +`projects` model: the root `vitest.config.mts` declares every project (`unit`, `e2e-packages`, one per example workspace) and -`vitest.shared.ts` carries the shared plugin/settings (deliberately not +`vitest.shared.mts` carries the shared plugin/settings (deliberately not the root config — merging a projects-bearing config into a project is a documented pitfall). Example configs are `defineProject` + `mergeConfig(shared, …)`; one SWC block exists instead of five. diff --git a/CONFIGURATION.md b/CONFIGURATION.md index d35552996..1e4ca5aa8 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -1,7 +1,7 @@ # Rockets — Configuration Entry Point -> Configuration reference for the **v2 DSL** (shipped). Field names below match -> the current `packages/*/src/**` (code wins over READMEs). The v2 redesign +> Configuration reference for the **1.0-preview DSL**. Field names below match +> the current `packages/*/src/**` (code wins over prose). The original DSL > rationale, convertibility proof, and change-set live in §12. > Diagrams are Mermaid — they render on GitHub and in most Markdown viewers. @@ -11,10 +11,11 @@ You never hand NestJS a tree of modules. You write **declarative bundles** (`defineResource`, `defineSubResource`, `defineModuleResource`) and a few -top-level fields (`repository`, `userMetadata`, `auth`), pass them to **one** -`RocketsModule.forRoot({...})`, and the module-definition transform converts -that into a single global `DynamicModule` (controllers, providers, repository -tokens, CQRS handlers, Swagger). +top-level fields (`repository`, `userMetadata`, `auth`), pass them to +`createServer({...})`, and the module-definition transform converts that into a +single global `DynamicModule` (controllers, providers, repository tokens, CQRS +handlers, Swagger). Use `RocketsModule.forRoot({...})` directly when a larger +Nest host module must compose Rockets with other imports or providers. ```mermaid flowchart LR @@ -24,7 +25,8 @@ flowchart LR M["defineModuleResource()"] OPT["repository / userMetadata / auth / swagger"] end - YOU --> FORROOT["RocketsModule.forRoot( ... )"] + YOU --> CREATE["createServer( ... )"] + CREATE --> FORROOT["RocketsModule.forRoot( ... )"] FORROOT --> XFORM["definitionTransform\n(build time)"] XFORM --> PLAN["buildAppRegistrationPlan()"] PLAN --> DM["one global DynamicModule\nimports / providers / controllers / exports"] @@ -34,12 +36,13 @@ flowchart LR **Two layers, one surface.** `@concepta/rockets` (server) is a thin presentation layer over `@concepta/rockets-core`. Server adds the `MeController`, the global guard opt-in, and the `auth` chain; core does the actual resource→module -conversion. You only ever call `RocketsModule.forRoot` (server). Core's -`forRootAsync` is called internally. +conversion. `createServer` is the canonical definition-first facade; +`RocketsModule.forRoot` is the lower-level composition surface. Core's +`forRootAsync` is called internally in either case. --- -## 1. The entry point — `RocketsModule.forRoot` / `forRootAsync` +## 1. The entry point — `createServer` and `RocketsModule` The options object is split by NestJS's `ConfigurableModuleBuilder` into two buckets with very different lifecycles: @@ -58,20 +61,24 @@ buckets with very different lifecycles: | Field | Type | Req? | Default | Purpose | |---|---|---|---|---| | `resources` | `ReadonlyArray` | optional | `[]` | The feature bundles (CRUD + module + sub flattened). | -| `repository` | `RepositoryModuleInterface \| RepositoryBootstrap` | optional* | — | Root persistence adapter (TypeORM/Firestore/…). | -| `userMetadata` | `RocketsUserMetadataConfig` | **required at runtime** | — | `/me` entity + DTOs. Omit → throws when the metadata DTO token resolves. | +| `repository` | `RepositoryModuleInterface \| RepositoryBootstrap` | optional† | — | Root persistence adapter (TypeORM/Firestore/…). | +| `userMetadata` | `RocketsUserMetadataConfig` | optional | — | `/me` entity + DTOs. When omitted, Rockets does not mount `/me` or register metadata handlers/providers. | | `auth` | `AuthBootstrap \| AuthBootstrap[]` | optional | `[]` | Auth chain (external adapter and/or built-in). | | `swagger` | `SwaggerUiOptionsInterface` | optional | — | Doc builder + UI. The **only** runtime field forwarded to core. | | `settings` | `RocketsSettingsInterface` (empty today) | optional | — | Reserved; no fields yet. | | `handlers` | `{ upsertUserMetadata?, getUserMetadata? }` | optional | built-ins | Override the user-metadata CQRS handlers. | -| `enableGlobalGuard` | `boolean` | optional | **on** (opt-out) | Register `AuthServerGuard` as `APP_GUARD` unless `=== false`. | +| `enableGlobalGuard` | `boolean` | optional | **on‡** | Register `AuthServerGuard` as `APP_GUARD` unless `=== false`. | | `disableController` | `{ me?: boolean }` | optional | `{}` | Disable built-in `MeController`. | | `controllers` | `DynamicModule['controllers']` | optional | — | Replace the auto controller set. | | `global` | `boolean` | optional | **forced `true`** | `forRoot` always makes the module global. | -\* `repository` is optional in the type but persistence resolution throws if an +† `repository` is optional in the type but persistence resolution throws if an entity has neither a per-entity override nor a root adapter. +‡ The built-in `defineRocketsAuth()` integration contributes `false` because +its upstream authentication module already owns a JWT global guard. Explicit +server options always win; mixed-auth hosts can opt the Rockets chain back in. + `*-server` / `*-core` split — what server forwards vs keeps: ```mermaid @@ -436,9 +443,19 @@ accepts one bootstrap or a **chain** (array, tried in order). interface AuthBootstrap { adapter: Type; forRoot?: () => DynamicModule; // host module: provides+exports the adapter + contributes?: { // integration-owned app defaults + resources?: ReadonlyArray; + userMetadata?: RocketsUserMetadataConfig; + repository?: RepositoryModuleInterface | RepositoryBootstrap; + enableGlobalGuard?: boolean; + }; } ``` +Explicit server options override contributed defaults. Resource contributions +are prepended to application resources; incompatible single-value defaults from +multiple auth integrations fail fast instead of depending on import order. + ```mermaid flowchart TD REQ["incoming request"] --> GUARD["AuthServerGuard (APP_GUARD)"] @@ -469,33 +486,26 @@ interface AuthAdapterInterface { ``` **Global guard (default-on / opt-out):** `AuthServerGuard` is registered as -`APP_GUARD` **unless `enableGlobalGuard === false`** — enabled by default; you -opt **out**, never in. Routes are guarded unless explicitly made public -(`@AuthPublic()`) or the global guard is disabled. +`APP_GUARD` **unless `enableGlobalGuard === false`**. Auth integrations may +contribute a different default; explicit app configuration wins. Routes are +guarded unless explicitly made public (`@AuthPublic()`) or the global guard is +disabled. ### 7a. External auth (`@concepta/rockets`) — you own `authenticate()` -Minimum (core stub shape): +Minimum: ```ts -function createStubAuthBootstrap(adapter) { - return { adapter, forRoot: () => ({ module: class {}, providers: [adapter], exports: [adapter] }) }; -} +const auth = defineAuthAdapter(MyAuthAdapter); ``` Complete (`examples/sample-server/src/auth/define-sample-auth.ts`): ```ts export function defineSampleAuth(): AuthBootstrap { - return { - adapter: SampleAuthAdapter, - forRoot: () => ({ - module: class SampleAuthHostModule {}, - providers: [SampleAuthAdapter], - controllers: [AuthController], - exports: [SampleAuthAdapter], // controller + entity stay internal - }), - }; + return defineAuthAdapter(SampleAuthAdapter, { + controllers: [AuthController], // controller stays integration-private + }); } RocketsModule.forRoot({ @@ -533,10 +543,10 @@ Concept → field map: |---|---| | JWT secrets/signing | `authentication.settings.jwt.{access,refresh}` | | login/strategies | `authentication.settings.strategies` | -| recovery | `authentication.ports.recoveryNotification` (**required**) + `verifyNotification` | +| recovery | `/recovery/*` controllers (enabled by default) + required `authentication.ports.recoveryNotification`; verification uses `verifyNotification` | | otp | `otp` block + `settings.otp` + `disableController.otp` | | signup / admin | `userCrud` (+ `handlers.*`) + `disableController.{signup,admin}` | -| oauth / federated | `federated` block — **OAuth provider modules are NOT in v8 yet (G1 gap)** | +| oauth / federated | `federated` persistence block; OAuth provider routes are deferred from the current 1.0 scope | Complete (`examples/sample-server-auth/src/app.module.ts`): @@ -558,20 +568,20 @@ const rocketsAuthInput: DefineRocketsAuthInput = { }; const rocketsAuth = defineRocketsAuth(rocketsAuthInput); -const rocketsAuthResources = buildRocketsAuthResources(rocketsAuthInput.persistence, rocketsAuthInput.invitationEntity); RocketsModule.forRoot({ auth: rocketsAuth, - userMetadata: rocketsAuthInput.userMetadata, - enableGlobalGuard: false, // auth uses per-controller guards - repository: repo, // SAME instance as persistence.module (reference equality!) - resources: [ ...rocketsAuthResources, createPetResource(), /* … */ ], + resources: [createPetResource(), /* … */], }); ``` -> **Reference-equality trap:** the `repo` passed to `RocketsModule.repository` -> and to `defineRocketsAuth({ persistence: { module: repo } })` must be the -> **same object** — entities are grouped per adapter by identity. +`defineRocketsAuth` contributes its persistence resources, metadata contract, +repository bootstrap, and guard preference to the surrounding server. The host +only declares application-owned resources. Explicit server options remain the +escape hatch and take precedence over those contributed defaults. Its Rockets +guard preference is `false` because `AuthenticationModule` already owns the JWT +global guard; mixed-auth hosts can set `rocketsDefaults.enableGlobalGuard: true` +and `auth.appGuard: false` to make the ordered Rockets adapter chain the owner. --- @@ -596,16 +606,16 @@ Minimum: repository: defineTypeOrmRepository({ type: 'sqlite', database: ':memory:', synchronize: true }) ``` -Selecting TypeORM (`examples/sample-server/src/repository/define-typeorm-repository.ts`): +Selecting TypeORM: ```ts -export function defineTypeOrmRepository(connection): RepositoryBootstrap { - return { - name: 'typeorm-bootstrap', - forFeature: (entities) => TypeOrmRepositoryModule.forFeature(entities), // one repo token per key - forRoot: (entities) => TypeOrmModule.forRoot({ ...connection, entities: [...entities] }), - }; -} +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; + +const repository = defineTypeOrmRepository({ + type: 'sqlite', + database: ':memory:', + synchronize: true, +}); ``` Swap to Firestore = pass a `defineFirestoreRepository(...)` instead — **no @@ -631,7 +641,7 @@ interface RocketsUserMetadataConfig { } ``` -Minimum (required at runtime — omit and the metadata DTO token throws): +Enable the optional `/me` surface by supplying: ```ts userMetadata: { @@ -681,13 +691,10 @@ flowchart TD `sampleAuthUserResource` + `defineSampleAuth`). 3. **`SafeCrudContextInterceptor`** — a live workaround replacing upstream's global `CrudContextOverlay`; flagged for removal once upstream is mixed-app safe. -4. **OAuth (G1)** — federated/OAuth provider modules are not ported to v8 yet. +4. **OAuth** — federated identity persistence exists, but provider-specific + OAuth routes are deferred from the current 1.0 scope. 5. **`settings`** — both server and core `settings` are empty interfaces today (reserved slot). -6. **Pre-existing e2e/typecheck gaps on this branch** (independent of the v2 - work): `rockets-crud` photo-CRUD e2e ×2 + `rockets-server-auth` password e2e - ×2 fail (31/35); `sample-server` has 2 `CrudCommandHandler` ctor typecheck - errors and `sample-server-auth` 1 deep crud-import error. --- @@ -697,11 +704,12 @@ flowchart TD ## 12. Signature v2 — design rationale & change-set (SHIPPED) -> **Status: implemented.** The v2 DSL described in §1–§10 is live in -> `packages/rockets-core/src/**`, with both sample apps migrated. Verified: -> build green, core unit 296/296, core e2e 47/47, package e2e 31/35 (the 4 -> failures pre-date this work). This section keeps the *why* — the constraint, -> the convertibility proof, the locked naming, and the change-set. +> **Status: implemented.** The DSL described in §1–§10 is live in +> `packages/rockets-core/src/**`, with all sample apps migrated. The root +> `release:check` gate verifies builds, spec typechecking, code and Markdown +> linting, unit and package E2E tests, sample builds/E2E tests, and dry-run +> package artifacts. This section keeps the *why* — the constraint, the +> convertibility proof, the locked naming, and the change-set. > > **Constraint:** "no breaking" meant **no functional / feature regression** — > NOT "cannot change the entry config". The input DSL was ours to redesign; the @@ -746,8 +754,9 @@ do not present join as the parent-child association mechanism. - **Per-operation DTOs: `input` / `output`.** Not `body` (write-only word) and not `request` — `request` is already the `{ params, body, bodyBatch, validation }` envelope in crud, so it is taken. `input → request.body`, `output → response.resource`. -- **`repository` everywhere.** Single name for "which adapter": root, per-resource, - per-entity, `userMetadata`. Replaces `persistence.module`. +- **`repository` at the application/resource layer.** Single name for "which + adapter": root, per-resource, per-entity, and `userMetadata`. Built-in auth + retains `persistence.module` because that input also owns the auth entity map. ### 12.3 Final signatures diff --git a/MIGRATION-SUMMARY.md b/MIGRATION-SUMMARY.md deleted file mode 100644 index fffa2ea76..000000000 --- a/MIGRATION-SUMMARY.md +++ /dev/null @@ -1,123 +0,0 @@ -# @bitwild Self-Contained Migration — Session Summary - -**Branch:** `feature/module-migration` -**Date:** 2026-06-04 -**Status:** build GREEN · lint PASS · e2e 31/35 suites (248 tests) · NOT committed - ---- - -## Goal - -Turn the `@bitwild` repository / crud / app packages from **thin wrappers over -upstream `@concepta/nestjs-*`** into **self-contained source**, by adopting the -original concepta source packages (copied in as `*-concepta` folders) and -deleting the wrappers. Consolidate `rockets-common` into `rockets-app`. - -## End-state package layout - -```text -app ← repository ← crud (lowest → highest layer) - ↑ ↖ - repository-typeorm core ← server / server-auth -``` - -| Package | What it is now | -|---|---| -| `@concepta/rockets-app` | **Foundation/kernel.** Context overlay (`AppContextHost`, `getAppContext`, `OverlayRef`, `Ctx`, `ContextOverlayInterceptor`), `RuntimeException`, hooks (`HookResolverService`, `Spec`), references, audit, `DomainAggregate`, `AuthUser`, SwaggerUi module, utils (`deriveEntityKey`/`resolveEntityKey`, `createRepositoryContext`, `whitelistedFromDto`, `stripUndefined`). **Replaced `rockets-common` (deleted).** Zero `@concepta/nestjs-*` deps. | -| `@concepta/rockets-repository` | Self-contained dynamic repository (module, adapter, transactions, federation, hooks, query helpers). DB-agnostic. `@InjectDynamicRepository(string \| Type)`. | -| `@concepta/rockets-repository-typeorm` | TypeORM implementation. | -| `@concepta/rockets-crud` | Self-contained CRUD module + builder + CQRS handlers. `@InjectCrudAdapter(string \| Type)`. | - -## What changed (the 5 phases) - -- **Phase 0** — Recorded baseline: `@bitwild` ecosystem was green; the copied - concepta packages had real TS compile errors. -- **Phase 1** — `rockets-app` became the self-contained superset of `common`. - Ported 7 utils + `AuthUser` (5 lines) + a **fresh** SwaggerUi module + - model interfaces. Renamed `@concepta/rockets-app` → `@concepta/rockets-app`. -- **Phase 2** — Adopted repository: merged `InjectDynamicRepository` to - `string | Type`, fixed `super.context` → `this.context` + `declare context`. -- **Phase 3** — Adopted crud + repository-typeorm: fixed the TypeORM `upsert` - typing (normalize `DeepPartial` via `repo.create()` — no cast), fixed a - union-narrowing bug that only fails under `strict:false` (distributive - conditional structural view), merged `InjectCrudAdapter`, fixed fixture - generic inference, aligned tsconfigs to exclude test files (project convention). -- **Phase 4** — Atomic cutover: - - Deleted wrappers: `rockets-common`, old `rockets-repository`, old `rockets-crud`. - - Renamed concepta folders/packages → `@concepta/*`. - - Swapped consumer imports: `rockets-common`→`rockets-app` (72 files); - upstream `@concepta/nestjs-repository`/`-typeorm`/`-crud` → `@bitwild` (93 files); - `@concepta/nestjs-common` kernel symbols split to app (52 files, 7 upstream-only - symbols kept). - - Rewired core `HookModule.forRoot({})` → `RocketsAppModule.forRoot()`. - - This resolved the silent `AppContextHost`-identity bug (the #1 risk - flagged up front). - -## Why the `@bitwild` repository now DIVERGES from upstream (important) - -The OLD `@concepta/rockets-repository` was a wrapper that re-exported upstream -`@concepta/nestjs-repository`, so they shared the **same** `AppContextHost` / -`TransactionScope` classes. The NEW `@concepta/rockets-repository` is independent -source — **different classes**. Anything that mixes the new `@bitwild` stack with -upstream `@concepta/nestjs-*` packages hits a cross-identity mismatch -("Expected AppContextHost, got object"). - -## The 4 failing e2e suites (both are documented boundaries, not bugs introduced) - -1. **`rockets-crud`: `crud.operations`, `crud.adapter`** — PRE-EXISTING - crud-internal test infra (dist-module `DataSource` DI wiring; ctx-overlay - generics in the test helper). Never passed in this repo's baseline. Source - is clean. - -2. **`rockets-server-auth`: `me-password`, `password-history`** — ARCHITECTURAL - BOUNDARY. server-auth composes `@bitwild` core (→ forces the `@bitwild` - repository for its resource layer + global `SafeCrudContextInterceptor`) - **and** upstream `@concepta/nestjs-invitation` / `nestjs-user` (→ need upstream - `TransactionScope`). One app cannot provide both repository identities. - This worked before only because the wrapper === upstream. - -## OPEN DECISION for next session (server-auth) - -Closing the server-auth boundary needs one of: - -- **(A)** Migrate the upstream auth stack (`nestjs-invitation`, `nestjs-user`, - `nestjs-otp`, `nestjs-role`, `nestjs-password`, `nestjs-federated`) to the - `@bitwild` stack. Large, separate effort — makes server-auth fully self-contained. -- **(B)** Re-introduce a repository compat layer so `@bitwild` repository stays - upstream-identity-compatible. Defeats the self-contained goal; also a "bridge" - (forbidden by AGENTS.md rule #9). -- **(C)** Accept the 2 suites as a known boundary and ship the rest. - -> You picked "keep auth on upstream" last session, but that is **structurally -> unachievable** while server-auth uses `@bitwild` core. Needs a real call between -> A / B / C. - -## How to reproduce the current state - -```bash -yarn install -yarn build # GREEN -yarn lint # PASS (4 warnings) -yarn test:e2e # 31/35 suites, 248 tests pass; 4 fail (above) -``` - -## Notes / gotchas discovered - -- Build is `tsc --build` (incremental) — it does NOT delete orphaned `dist` - outputs. After renames, `rm -rf packages/*/dist *.tsbuildinfo` before a build, - or stale `.js` files reference deleted packages at runtime. -- Tests historically transpiled with `strict:false` (now via the Vitest/SWC -pipeline); some adopted code had - bugs that only surface there (union narrowing). Fixes must pass BOTH modes. -- Adding `reflect-metadata` as a dep to `rockets-app` made yarn nest a - redundant `@nestjs/common` under `packages/rockets-app/node_modules`, which - broke a portable-type emit (`TS2742`). Reverted; the spec's `import - 'reflect-metadata'` was redundant (it's a hoisted root dep). -- Upstream `@concepta/nestjs-user`/`invitation` peer-depend on - `@concepta/nestjs-crud`/`-repository`; those deps were restored to package.json - (source uses `@bitwild`, upstream auth packages keep their upstream peer). - -## Detailed step-by-step log - -See `.context/migration-baseline.md` (gitignored) for the full Phase 0→4 record -with exact files, errors, and fixes. diff --git a/README.md b/README.md index 37f8d2a64..0a7ed080a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/conceptadev/rockets/ci-merge.yml?branch=main&label=CI)](https://github.com/conceptadev/rockets/actions/workflows/ci-merge.yml) [![Codecov](https://codecov.io/gh/conceptadev/rockets/branch/main/graph/badge.svg)](https://codecov.io/gh/conceptadev/rockets) -[![NestJS](https://img.shields.io/badge/NestJS-11-ea2845?logo=nestjs&logoColor=white)](https://nestjs.com/) +[![NestJS](https://img.shields.io/badge/NestJS-12-ea2845?logo=nestjs&logoColor=white)](https://nestjs.com/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![License](https://img.shields.io/badge/license-BSD--3--Clause-green.svg)](LICENSE.txt) @@ -13,9 +13,9 @@ **Status:** pre-1.0 (`0.0.1-dev.0`, on npm under `@concepta/*` with dist-tag `alpha`). The public surface (`AuthAdapterInterface`, `defineResource`, -`defineModuleResource`, `RepositoryInterface`, the `RocketsModule.forRoot` -options shape) is stable; field renames are still possible before 1.0. Pin exact -versions in production. +`defineModuleResource`, `RepositoryInterface`, `createServer`) is being +prepared for 1.0; breaking refinements are still possible until that release. +Pin exact versions in production. ## Table of contents @@ -257,7 +257,7 @@ access-control rules. Rockets does not pretend to write those for you. ### Prerequisites -- Node 18+. +- Node 20+ (required by NestJS 12). - A package manager (yarn 4 / npm / pnpm — examples below use yarn). - A database adapter — TypeORM with any supported driver is the most common. Firestore works via `@concepta/rockets-repository-firestore`. @@ -307,7 +307,7 @@ yarn add @concepta/rockets@alpha \ | Pulled in for you | Packages | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | -| Other `@concepta/*` | `rockets-core`, `rockets-repository-typeorm` | +| Other `@concepta/*` | `rockets-core` | | Upstream motor | `@concepta/nestjs-{core,repository,crud,authentication,access-control}` (via `@concepta/rockets-core` re-exports) | | Nest (Rockets runtime) | `@nestjs/common`, `@nestjs/core`, `@nestjs/cqrs`, `@nestjs/swagger`, `@nestjs/config` | @@ -342,6 +342,7 @@ import { AuthAdapterInterface, AuthAttemptResult, AuthRequest, + defineAuthAdapter, extractBearerToken, } from '@concepta/rockets'; @@ -364,6 +365,8 @@ export class JwtAdapter implements AuthAdapterInterface { } } } + +export const jwtAuth = defineAuthAdapter(JwtAdapter); ``` Declare a resource — this is the entire CRUD definition: @@ -381,42 +384,24 @@ export class PetEntity { } ``` -Add a small TypeORM bootstrap helper in your app — the adapter is -`@concepta/rockets-repository-typeorm`, but the connection-options wrapper stays -app-local so core never takes a TypeORM dependency. It implements -`RepositoryBootstrap` so the planner calls `forRoot(entities)` once from -`resources[]` + `userMetadata`, without a hand-maintained entity list: +Create the TypeORM bootstrap at the boundary. The adapter owns the wrapper, so +applications do not copy infrastructure helpers: ```typescript -// src/repository/define-typeorm-repository.ts -import type { DynamicModule, PlainLiteralObject, Type } from '@nestjs/common'; -import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { TypeOrmRepositoryModule } from '@concepta/rockets-repository-typeorm'; -import type { RepositoryBootstrap } from '@concepta/rockets-core'; -import type { - DynamicRepositoryModule, - RepositoryProviderOptions, -} from '@concepta/nestjs-repository'; - -export function defineTypeOrmRepository< - Connection extends TypeOrmModuleOptions, ->(connection: Connection): RepositoryBootstrap { - return { - name: 'typeorm-bootstrap', - forFeature(entities: RepositoryProviderOptions[]): DynamicRepositoryModule { - return TypeOrmRepositoryModule.forFeature(entities); - }, - forRoot(entities: ReadonlyArray>): DynamicModule { - return TypeOrmModule.forRoot({ ...connection, entities: [...entities] }); - }, - }; -} +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; + +const repository = defineTypeOrmRepository({ + type: 'sqlite', + database: ':memory:', + synchronize: true, + dropSchema: true, +}); ``` **Why this exists:** you pass only connection options (`type`, `database`, `synchronize`, …). You never maintain `entities: [PetEntity, UserMetadataEntity, …]` on `TypeOrmModule.forRoot`. When -`RocketsModule` boots, the registration planner walks `resources[]`, +the server boots, the registration planner walks `resources[]`, `userMetadata.entity`, and any entities contributed by auth integrations, then calls `forRoot(mergedEntities)` once and `forFeature` per table. Services use `@InjectDynamicRepository(PetEntity)` and get a `RepositoryInterface` @@ -425,41 +410,41 @@ calls `forRoot(mergedEntities)` once and `forFeature` per table. Services use Compose the app: ```typescript -// src/app.module.ts -import { Module } from '@nestjs/common'; -import { RocketsModule, defineResource } from '@concepta/rockets'; +// src/server.ts +import { NestFactory } from '@nestjs/core'; +import { createServer, defineResource } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import { OwnerStampHook, OwnerScopeHook } from '@concepta/rockets-core'; -import { JwtAdapter } from './auth/jwt.adapter'; +import { jwtAuth } from './auth/jwt.adapter'; import { PetEntity } from './pet/pet.entity'; import { UserMetadataEntity } from './user/user-metadata.entity'; import { UserMetadataCreateDto, UserMetadataUpdateDto } from './user/dto'; -import { defineTypeOrmRepository } from './repository/define-typeorm-repository'; -@Module({ - imports: [ - RocketsModule.forRoot({ - auth: JwtAdapter, - userMetadata: { - entity: UserMetadataEntity, - createDto: UserMetadataCreateDto, - updateDto: UserMetadataUpdateDto, - }, - repository: defineTypeOrmRepository({ - type: 'sqlite', - database: ':memory:', - synchronize: true, - dropSchema: true, - }), - resources: [ - defineResource({ - entity: PetEntity, - hooks: [OwnerStampHook.for(PetEntity), OwnerScopeHook.for(PetEntity)], - }), - ], +const repository = defineTypeOrmRepository({ + type: 'sqlite', + database: ':memory:', + synchronize: true, + dropSchema: true, +}); + +export const server = createServer({ + auth: jwtAuth, + userMetadata: { + entity: UserMetadataEntity, + createDto: UserMetadataCreateDto, + updateDto: UserMetadataUpdateDto, + }, + repository, + resources: [ + defineResource({ + entity: PetEntity, + hooks: [OwnerStampHook.for(PetEntity), OwnerScopeHook.for(PetEntity)], }), ], -}) -export class AppModule {} +}); + +const app = await NestFactory.create(server); +await app.listen(3000); ``` Run it: @@ -486,20 +471,15 @@ Install the same packages as above plus `@concepta/rockets-auth` and the upstrea `@concepta/nestjs-*` line (most are transitive dependencies; `yarn install` will pull them). -Compose with `defineRocketsAuth()`. Reuse the same `defineTypeOrmRepository` -helper from path A and pass the **same instance** to both -`defineRocketsAuth({ persistence: { module: repo } })` and -`RocketsModule.forRoot({ repository: repo })`. Register auth persistence rows -via `buildRocketsAuthResources()` on `resources`: +Compose with `defineRocketsAuth()`. Give it the TypeORM bootstrap once; the +integration contributes its auth rows, root repository, metadata contract, and +guard preference to the surrounding server: ```typescript import { Module } from '@nestjs/common'; -import { - defineRocketsAuth, - buildRocketsAuthResources, -} from '@concepta/rockets-auth'; +import { defineRocketsAuth } from '@concepta/rockets-auth'; import { RocketsModule } from '@concepta/rockets'; -import { defineTypeOrmRepository } from './repository/define-typeorm-repository'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; const repo = defineTypeOrmRepository({ type: 'sqlite', @@ -542,18 +522,11 @@ const rocketsAuthInput = { }), }; -const rocketsAuth = defineRocketsAuth(rocketsAuthInput); -const rocketsAuthResources = buildRocketsAuthResources( - rocketsAuthInput.persistence, - rocketsAuthInput.invitationEntity, -); - @Module({ imports: [ RocketsModule.forRoot({ - auth: rocketsAuth, - repository: repo, - resources: [...rocketsAuthResources /* your defineResource bundles */], + auth: defineRocketsAuth(rocketsAuthInput), + resources: [/* your application defineResource bundles */], }), ], }) @@ -575,16 +548,17 @@ The monorepo ships runnable sample apps for both paths (`yarn sample:dev` and `auth` accepts a single `AuthBootstrap` or an array. Each entry is one of: -- `defineFirebaseAuth({ forRoot | forRootAsync })` — Firebase Admin + +- `defineFirebaseAuth({ firebaseApp })` or the explicit `{ forRootAsync }` + variant — Firebase Admin + `FirebaseAuthAdapter` (`@concepta/rockets-adapter-firebase`). -- `defineRocketsAuth(...)` — built-in signup/login stack - (`@concepta/rockets-auth`); pair with `buildRocketsAuthResources()` on - `resources`. -- App-local `AuthBootstrap` — `{ adapter, forRoot? }` for custom adapters (see +- `defineRocketsAuth(...)` — complete built-in signup/login stack and its owned + persistence contributions (`@concepta/rockets-auth`). +- `defineAuthAdapter(Adapter, options?)` — complete host wiring for a custom + adapter (see `defineApiKeyAuth()` in sample-code-review). -Entity rows for auth-owned tables belong on `resources[]`, not inside the auth -helper. +Explicit server options override integration-contributed defaults. Conflicting +defaults from two integrations fail at startup instead of depending on order. ```typescript import { defineFirebaseAuth } from '@concepta/rockets-adapter-firebase'; @@ -668,11 +642,10 @@ the caller owns the parent via `PathScopeGuard`. ### Wire TypeORM without hand-registering entities -Use a small app-local `defineTypeOrmRepository` helper (full sample in **Path -A** above). It implements `RepositoryBootstrap` from `@concepta/rockets-core` -and wraps `TypeOrmRepositoryModule` from `@concepta/rockets-repository-typeorm`; -only the helper (your connection options) lives in the app, so core never takes -a TypeORM dependency. Firestore-only apps skip it and use +Import `defineTypeOrmRepository` from +`@concepta/rockets-repository-typeorm`. It implements `RepositoryBootstrap` +and keeps TypeORM connection concerns in the adapter package while core stays +storage-agnostic. Firestore-only apps skip it and use `@concepta/rockets-repository-firestore` instead. #### What you declare vs what the framework registers @@ -737,8 +710,8 @@ No `TypeOrmModule.forFeature([PetEntity])` in feature modules. No `@InjectRepository`. If the entity is in the registration plan, `@InjectDynamicRepository` resolves at runtime. -**Built-in auth (path B):** pass the **same** `repository` instance to both -entry points so one connection serves app tables and auth tables: +**Built-in auth (path B):** pass the repository to `defineRocketsAuth`; its +composition contribution makes the same connection serve app and auth tables: ```typescript const repository = defineTypeOrmRepository({ @@ -758,7 +731,6 @@ const rocketsAuth = defineRocketsAuth({ @Module({ imports: [ RocketsModule.forRoot({ - repository, auth: rocketsAuth, resources: [ /* pet resources — no per-resource persistence block */ @@ -956,20 +928,20 @@ it: one `RocketsModule.forRoot({ ... })` object is split by | `@concepta/nestjs-crud` | `@concepta/rockets-core` (re-export) | Generated controllers, CQRS commands/queries, default handlers | | `@concepta/nestjs-core`, `@concepta/nestjs-authentication` | `@concepta/rockets-core` | Hook resolution (`CoreModule`), shared exceptions, auth primitives | | `@concepta/nestjs-access-control` | opt-in `accessControl` option (import symbols from upstream) | Grant table, `AccessControlGuard`, route decorators | -| `@concepta/nestjs-repository-typeorm` | `@concepta/rockets-repository-typeorm` (thin wrapper) + app-local bootstrap | SQL adapter — `@concepta/rockets-repository-typeorm`'s main entry re-exports the upstream package verbatim; wrapped by `defineTypeOrmRepository` | +| `@concepta/nestjs-repository-typeorm` | `@concepta/rockets-repository-typeorm` | SQL adapter plus `defineTypeOrmRepository`, which supplies connection options and accepts the planner-derived entity list | | `@concepta/nestjs-user`, `role`, `otp`, `password`, `invitation`, `federated`, `email`, `event` | wired inside `@concepta/rockets-auth` | Built-in auth HTTP + persistence rows (path B only) | | Rockets layer | Role | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `@concepta/rockets-core` | **Planner and contracts**: `defineResource`, `buildAppRegistrationPlan`, `AuthServerGuard`, owner/path hooks, swagger registration | | `@concepta/rockets` (server) | **External-auth presentation**: `MeController`, default `APP_GUARD`, `auth` chain merge | -| `@concepta/rockets-auth` | **Built-in identity bundle**: `defineRocketsAuth()` + `buildRocketsAuthResources()` | +| `@concepta/rockets-auth` | **Built-in identity bundle**: `defineRocketsAuth()` with owned composition contributions | **Path B uses both** `@concepta/rockets` and `@concepta/rockets-auth`: -`defineRocketsAuth()` supplies the auth bootstrap; spread -`buildRocketsAuthResources()` into `resources`; -`RocketsModule.forRoot({ auth, repository, resources })` still comes from the -server package. They are sibling packages over core, not parent/child. +`defineRocketsAuth()` supplies the auth bootstrap plus its persistence, +metadata, and guard defaults; `createServer({ auth, resources })` (or the +lower-level `RocketsModule.forRoot`) still comes from the server package. They +are sibling packages over core, not parent/child. **Repository injection (upstream contract, Rockets-local decorator):** @@ -995,7 +967,7 @@ configuration façade** — not a fork. | **Core re-exports (former `@concepta/rockets-common`)** | `@concepta/rockets-common` was deleted; its helpers (`AuthUser`, `InjectDynamicRepository`, `SwaggerUiModule`, `deriveEntityKey`, …) and upstream re-exports now live inside `@concepta/rockets-core`. This is **not** a replacement for the upstream **app-module** composition pattern — that wiring still lives in Concepta; Rockets adds a **second** entry point (`RocketsModule.forRoot`) that feeds the same motors. | | **Port backlog (server path)** | On v8 today: `core`, `repository`, `crud`, `hook`, `common`, `authentication`, `access-control`. Still on v7 in this monorepo: `swagger-ui` (and `email` / `event` on the auth path) — version-mismatched intentionally and tested in CI. | | **Repo migration** | Moving all of `nestjs-modules` into this git repo is **optional** for product validation. Shipping fixes against published `@concepta/*` alphas is fine; monorepo colocation is for AI context and version lock, not a prerequisite to use Rockets. | -| **Safe to keep building on** | These are intentional, tested surfaces — not throwaway experiments: `AuthAdapterInterface.authenticate`, `RepositoryInterface` + dynamic repository keys (class **or** string token), `defineResource` / planner-driven entity registration, `defineRocketsAuth({ persistence: { module } })` sharing one `repository` instance with `RocketsModule.forRoot`. | +| **Safe to keep building on** | These are intentional, tested surfaces — not throwaway experiments: `createServer`, `AuthAdapterInterface.authenticate`, `RepositoryInterface` + dynamic repository keys (class **or** string token), `defineResource` / planner-driven entity registration, and complete `defineRocketsAuth({ persistence })` contributions. | **Custom validation / business rules:** use `defineHook` from `@concepta/rockets-core` for simple entity lifecycle rules, upstream @@ -1008,12 +980,12 @@ to 4xx — a bare `Error` in a hook often surfaces as 500. | Package | npm name | Purpose | Docs | Status | | --------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- | ------- | -| `packages/rockets-core` | `@concepta/rockets-core` | Composition planner. Auth chain, `buildAppRegistrationPlan`, `defineResource` / `defineModuleResource` / `defineSubResource`, `defineHook`, owner/path hooks, swagger registration, shared helpers, zod layer at `@concepta/rockets-core/zod`, opt-in `accessControl`. | [README](packages/rockets-core/README.md) | stable | -| `packages/rockets-repository-typeorm` | `@concepta/rockets-repository-typeorm` | TypeORM adapter for the dynamic repository contract — a thin wrapper whose main entry re-exports upstream `@concepta/nestjs-repository-typeorm` verbatim, plus the zod `SchemaEntityCompiler` at `@concepta/rockets-repository-typeorm/zod`. | [README](packages/rockets-repository-typeorm/README.md) | stable | +| `packages/rockets-core` | `@concepta/rockets-core` | Composition planner. Auth chain, `buildAppRegistrationPlan`, `defineResource` / `defineModuleResource` / `defineSubResource`, `defineHook`, owner/path hooks, swagger registration, shared helpers, zod layer at `@concepta/rockets-core/zod`, opt-in `accessControl`. | [README](packages/rockets-core/README.md) | preview | +| `packages/rockets-repository-typeorm` | `@concepta/rockets-repository-typeorm` | TypeORM adapter and `defineTypeOrmRepository` bootstrap for planner-derived entity registration, plus the zod `SchemaEntityCompiler` at `@concepta/rockets-repository-typeorm/zod`. | [README](packages/rockets-repository-typeorm/README.md) | preview | | `packages/rockets-repository-firestore` | `@concepta/rockets-repository-firestore` | Firestore adapter implementing `RepositoryAdapter`. Per-entity opt-in. | [README](packages/rockets-repository-firestore/README.md) | preview | | `packages/rockets-adapter-firebase` | `@concepta/rockets-adapter-firebase` | Firebase Auth adapter implementing `AuthAdapterInterface`. | [README](packages/rockets-adapter-firebase/README.md) | preview | -| `packages/rockets-server` | `@concepta/rockets` | External-auth presentation layer. `MeController`, `APP_GUARD` opt-in, `auth` chain. | [README](packages/rockets-server/README.md) | stable | -| `packages/rockets-server-auth` | `@concepta/rockets-auth` | Built-in auth: signup, login, OTP, recovery, invitations, roles, admin user CRUD. `defineRocketsAuth()`. | [README](packages/rockets-server-auth/README.md) | alpha | +| `packages/rockets-server` | `@concepta/rockets` | Launch-facing `createServer`, external-auth presentation, optional `/me`, default guard, and auth chain. | [README](packages/rockets-server/README.md) | preview | +| `packages/rockets-server-auth` | `@concepta/rockets-auth` | Built-in auth: signup, login, recovery, OTP, invitations, roles, throttling, and admin user CRUD. | [README](packages/rockets-server-auth/README.md) | preview | ### Repository layout @@ -1035,8 +1007,9 @@ rockets/ - **Rockets packages**: `0.0.1-dev.0` on npm (`yarn add @concepta/rockets@alpha`, or pin `0.0.1-dev.0`). Monorepo packages keep `workspace:^` for local development. -- **Upstream Concepta packages**: v8 line at `8.0.0-alpha.7` (`nestjs-common` / - `nestjs-hook` at `8.0.0-alpha.6`). Two modules still on v7 +- **Upstream Concepta packages**: v8 modules are pinned to `8.0.0-alpha.8`; + `@concepta/nestjs-common` remains at its latest published v8 build, + `8.0.0-alpha.6`. Two modules remain on v7 (`@concepta/nestjs-email`, `@concepta/nestjs-event`) pending the v8 port. Swagger UI ships from `@concepta/rockets-core`. Auth persistence entities are app-owned TypeORM classes — do not use `@concepta/nestjs-typeorm-ext`. @@ -1044,13 +1017,14 @@ rockets/ `testing`); satellite packages (`cqrs`, `typeorm`, `jwt`, `passport`, `config`, `throttler`) remain on their current stable majors until a Nest 12 line is published. -- **Node**: `>=18.0.0`. +- **Node**: `>=20.0.0` (the minimum supported by NestJS 12). ### Common scripts (from the monorepo root) | Command | Purpose | | ----------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `yarn publish:conceptadev` | Build + publish all `@concepta/*` packages to npm (`--tag alpha`). | +| `yarn release:check` | Run the complete build, package, type, lint, unit, e2e, and sample release gate. | +| `yarn release:dry` | Build publish archives for every public `@concepta/*` workspace without publishing. | | `yarn install && yarn build` | Bootstrap + compile every local `@concepta/*` package. | | `yarn test` | Unit tests (Vitest). | | `yarn typecheck:spec` | Type-checks test files — the runner only transpiles them. | @@ -1060,7 +1034,6 @@ rockets/ | `yarn sample:dev` | Run `sample-server` in watch mode. | | `yarn sample-auth:dev` | Run `sample-server-auth` in watch mode. | | `yarn sample-code-review:dev` | Build + run the full-stack example. | -| `yarn generate-swagger` | Dump the OpenAPI spec from `sample-server-auth`. | --- diff --git a/examples/sample-code-review/README.md b/examples/sample-code-review/README.md index 27973cd04..32c69f604 100644 --- a/examples/sample-code-review/README.md +++ b/examples/sample-code-review/README.md @@ -75,7 +75,7 @@ chain so server-to-server callers can authenticate with `X-Api-Key`. ### Prerequisites -- Node 18+, Yarn 4. +- Node 20+, Yarn 4. - A Firebase project for real auth (or `FIREBASE_USE_FAKE=true` for in-process verification). - A GitHub OAuth App if you want the GitHub connect flow to work. @@ -227,7 +227,7 @@ examples/sample-code-review │ │ ├── auth-firebase/ defineFirebaseAuth wiring │ │ ├── auth-api-key/ Second AuthBootstrap in the chain — ApiKeyEntity + POST /api-keys (mint) + adapter │ │ ├── github/ GitHub OAuth + repo browse -│ │ ├── repository/ defineTypeOrmRepository + Firestore persistence helpers +│ │ ├── repository/ Firestore persistence helper │ │ ├── config/ GithubConfig / OpenaiConfig (env-var readers) │ │ ├── zod-bindings.ts bindZodResources(typeOrmZodEntityCompiler) │ │ ├── user-metadata.schema.ts zod schema -> { entity, createDto, updateDto, responseDto } diff --git a/examples/sample-code-review/apps/api/package.json b/examples/sample-code-review/apps/api/package.json index 5ac77ea54..fa819b786 100644 --- a/examples/sample-code-review/apps/api/package.json +++ b/examples/sample-code-review/apps/api/package.json @@ -10,8 +10,8 @@ "type-check": "tsc --noEmit -p tsconfig.json", "lint": "echo \"lint: configure eslint when needed\"", "clean": "rm -rf dist", - "test:e2e": "vitest run --config vitest.e2e.config.ts", - "test:e2e:auth": "vitest run --config vitest.e2e.config.ts test/auth-connection.e2e-spec.ts" + "test:e2e": "vitest run --config vitest.e2e.config.mts", + "test:e2e:auth": "vitest run --config vitest.e2e.config.mts test/auth-connection.e2e-spec.ts" }, "dependencies": { "@concepta/nestjs-core": "8.0.0-alpha.8", @@ -43,7 +43,7 @@ "@nestjs/cli": "12.0.0-alpha.6", "@nestjs/testing": "12.0.0-alpha.5", "@swc/core": "^1.15.47", - "@types/node": "^18.19.44", + "@types/node": "^20.19.0", "@types/supertest": "^6.0.2", "supertest": "^6.3.4", "ts-node": "^10.9.2", diff --git a/examples/sample-code-review/apps/api/src/app.module.ts b/examples/sample-code-review/apps/api/src/app.module.ts index 402d0c5e6..f207213ca 100644 --- a/examples/sample-code-review/apps/api/src/app.module.ts +++ b/examples/sample-code-review/apps/api/src/app.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { RocketsModule } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import { defineFirebaseAuth } from '@concepta/rockets-adapter-firebase'; import { defineModuleResource } from '@concepta/rockets-core'; import { createFirebaseAdminApp } from './auth-firebase'; @@ -13,7 +14,6 @@ import { UserMetadataCreateDto, UserMetadataUpdateDto, } from './user-metadata.schema'; -import { defineTypeOrmRepository } from './repository/define-typeorm-repository'; import { githubFeature } from './github'; import { analysisFeature } from './analysis'; diff --git a/examples/sample-code-review/apps/api/src/auth-api-key/define-api-key-auth.ts b/examples/sample-code-review/apps/api/src/auth-api-key/define-api-key-auth.ts index 6188de25d..c05fc54c1 100644 --- a/examples/sample-code-review/apps/api/src/auth-api-key/define-api-key-auth.ts +++ b/examples/sample-code-review/apps/api/src/auth-api-key/define-api-key-auth.ts @@ -1,4 +1,4 @@ -import { defineModuleResource } from '@concepta/rockets-core'; +import { defineAuthAdapter, defineModuleResource } from '@concepta/rockets-core'; import type { AuthBootstrap } from '@concepta/rockets-core'; import { ApiKeyAuthAdapter } from './api-key.adapter'; import { ApiKeyController } from './api-key.controller'; @@ -12,13 +12,7 @@ export const apiKeyAuthResource = defineModuleResource({ * API key auth chain entry. Pair with `apiKeyAuthResource` in `resources[]`. */ export function defineApiKeyAuth(): AuthBootstrap { - return { - adapter: ApiKeyAuthAdapter, - forRoot: () => ({ - module: class ApiKeyAuthHostModule {}, - providers: [ApiKeyAuthAdapter], - controllers: [ApiKeyController], - exports: [ApiKeyAuthAdapter], - }), - }; + return defineAuthAdapter(ApiKeyAuthAdapter, { + controllers: [ApiKeyController], + }); } diff --git a/examples/sample-code-review/apps/api/src/repository/define-typeorm-repository.ts b/examples/sample-code-review/apps/api/src/repository/define-typeorm-repository.ts deleted file mode 100644 index 38fed55b7..000000000 --- a/examples/sample-code-review/apps/api/src/repository/define-typeorm-repository.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { DynamicModule, PlainLiteralObject, Type } from '@nestjs/common'; -import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { TypeOrmRepositoryModule } from '@concepta/rockets-repository-typeorm'; -import type { RepositoryBootstrap } from '@concepta/rockets-core'; -import type { - DynamicRepositoryModule, - RepositoryProviderOptions, -} from '@concepta/nestjs-repository'; - -export function defineTypeOrmRepository( - connection: Connection, -): RepositoryBootstrap { - return { - name: 'typeorm-bootstrap', - forFeature(entities: RepositoryProviderOptions[]): DynamicRepositoryModule { - return TypeOrmRepositoryModule.forFeature(entities); - }, - forRoot( - entities: ReadonlyArray>, - ): DynamicModule { - return TypeOrmModule.forRoot({ - ...connection, - entities: [...entities], - }); - }, - }; -} diff --git a/examples/sample-code-review/apps/api/vitest.e2e.config.ts b/examples/sample-code-review/apps/api/vitest.e2e.config.mts similarity index 84% rename from examples/sample-code-review/apps/api/vitest.e2e.config.ts rename to examples/sample-code-review/apps/api/vitest.e2e.config.mts index 65de1ad8a..8a78e0004 100644 --- a/examples/sample-code-review/apps/api/vitest.e2e.config.ts +++ b/examples/sample-code-review/apps/api/vitest.e2e.config.mts @@ -1,10 +1,11 @@ import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { defineProject, mergeConfig } from 'vitest/config'; -import shared from '../../../../vitest.shared'; +import shared from '../../../../vitest.shared.mts'; /** * E2E project for the sample-code-review API — registered in the root - * `vitest.config.ts` `projects` list and runnable standalone via + * `vitest.config.mts` `projects` list and runnable standalone via * `--config` from the workspace. The Jest `moduleNameMapper` stub swap (real * Firestore persistence -> in-memory backend stub) is ported as a regex * alias below. @@ -20,7 +21,7 @@ export default mergeConfig( // moduleNameMapper). find: /^\.\.\/repository\/code-review-reports\.persistence$/, replacement: path.resolve( - __dirname, + path.dirname(fileURLToPath(import.meta.url)), 'test/stubs/code-review-reports.persistence.stub.ts', ), }, diff --git a/examples/sample-server-auth/README.md b/examples/sample-server-auth/README.md index 6640f9ed8..8ee4feff5 100644 --- a/examples/sample-server-auth/README.md +++ b/examples/sample-server-auth/README.md @@ -103,11 +103,11 @@ authenticated as an admin. The three roles in `src/app.acl.ts`: -| Role | Pet / Vaccination / Appointment | -|---|---| -| `admin` | `createAny`, `readAny`, `updateAny`, `deleteAny` | -| `manager` | `createAny`, `readAny`, `updateAny` (no delete) | -| `user` | `createOwn`, `readOwn`, `updateOwn`, `deleteOwn` (ownership-checked by `PetAccessQueryService`) | +| Role | Pet / Vaccination / Appointment | +| --------- | ----------------------------------------------------------------------------------------------- | +| `admin` | `createAny`, `readAny`, `updateAny`, `deleteAny` | +| `manager` | `createAny`, `readAny`, `updateAny` (no delete) | +| `user` | `createOwn`, `readOwn`, `updateOwn`, `deleteOwn` (ownership-checked by `PetAccessQueryService`) | The `PetAccessQueryService.canAccess()` runs **after** the grant table matches — it refines `:own` rules by comparing `pet.userId` to the @@ -169,7 +169,6 @@ examples/sample-server-auth │ ├── app.acl.ts Role + resource enums + accesscontrol grants │ ├── access-control.service.ts AccessControlServiceInterface impl │ ├── main.ts Bootstrap (helmet, validation, swagger) -│ ├── repository/ defineTypeOrmRepository bootstrap (shared with defineRocketsAuth) │ ├── shared/persistence/ AuditedSqliteEntity (app-owned audit columns) │ ├── modules/ │ │ ├── user/ UserEntity + credential / otp / role-link entities + DTOs @@ -181,36 +180,37 @@ examples/sample-server-auth ### Auth surface (exposed by the bundle) -| Route | Purpose | -|---|---| -| `POST /signup` | New user signup. Wired through `userCrud`. | -| `POST /token/password` | Login (username + password → access + refresh token). | -| `POST /token/refresh` | Refresh the access token. | -| `GET /me`, `PATCH /me` | From `@concepta/rockets`. | -| `PATCH /me/password` | Password change. | -| `POST /otp`, `PATCH /otp` | OTP issue / verify. | -| `POST /recovery/*` | Password recovery flow (wired to notification handlers). | -| `/admin/users`, `/admin/users/:userId/roles` | Admin user CRUD + role assignment. | -| `/admin/roles` | Role CRUD (`roleCrud` config). | -| `/admin/invitations`, `/invitation-acceptance`, … | Invitation flow. | +| Route | Purpose | +| ------------------------------------------------------------------------- | ----------------------------------------------------- | +| `POST /signup` | New user signup. Wired through `userCrud`. | +| `POST /token/password` | Login (username + password → access + refresh token). | +| `POST /token/refresh` | Refresh the access token. | +| `GET /me`, `PATCH /me` | From `@concepta/rockets`. | +| `PATCH /me/password` | Password change. | +| `POST /otp`, `PATCH /otp` | OTP issue / verify. | +| `POST /recovery/login`, `POST /recovery/password` | Enumeration-safe recovery initiation. | +| `POST /recovery/passcode` with `{ passcode }`, `PATCH /recovery/password` | Passcode validation and password reset. | +| `/admin/users`, `/admin/users/:userId/roles` | Admin user CRUD + role assignment. | +| `/admin/roles` | Role CRUD (`roleCrud` config). | +| `/admin/invitations`, `/invitation-acceptance`, … | Invitation flow. | ### Environment variables -| Var | Default | Purpose | -|---|---|---| -| `ADMIN_EMAIL` | **required** | Email for the admin user seeded on every boot (`main.ts`). No default — the app exits if unset. | -| `ADMIN_PASSWORD` | **required** | Password for the seeded admin user. No default — the app exits if unset. | -| `PORT` | `3001` | HTTP port (`process.env.PORT \|\| 3001`). | -| `ALLOWED_ORIGINS` | `*` | Comma-separated CORS allowlist. | -| `SWAGGER_UI_PATH` | `api` | Swagger UI mount path. | +| Var | Default | Purpose | +| ----------------- | ------------ | ----------------------------------------------------------------------------------------------- | +| `ADMIN_EMAIL` | **required** | Email for the admin user seeded on every boot (`main.ts`). No default — the app exits if unset. | +| `ADMIN_PASSWORD` | **required** | Password for the seeded admin user. No default — the app exits if unset. | +| `PORT` | `3001` | HTTP port (`process.env.PORT \|\| 3001`). | +| `ALLOWED_ORIGINS` | `*` | Comma-separated CORS allowlist. | +| `SWAGGER_UI_PATH` | `api` | Swagger UI mount path. | ### Persistence wiring -A single `defineTypeOrmRepository({...})` bootstrap is shared by -`RocketsModule.forRoot({ repository })` and `defineRocketsAuth({ -persistence: { module } })`. The planner derives the full entity list -from `resources[]`, `userMetadata.entity`, and the auth -`persistence.entities` map — there is no top-level +A single `defineTypeOrmRepository({...})` bootstrap is passed to +`defineRocketsAuth({ persistence: { module } })`. That integration contributes +the root repository, auth resources, and metadata contract to +`RocketsModule`. The planner derives the full entity list from app resources +and the auth `persistence.entities` map — there is no top-level `TypeOrmModule.forRoot({ entities: [...] })` to keep in sync. Pet resources omit per-resource `persistence` so they inherit the root adapter. diff --git a/examples/sample-server-auth/package.json b/examples/sample-server-auth/package.json index 3cb358205..e8d2225bc 100644 --- a/examples/sample-server-auth/package.json +++ b/examples/sample-server-auth/package.json @@ -9,7 +9,7 @@ "build": "cd ../.. && ./node_modules/.bin/tsc -p examples/sample-server-auth/tsconfig.json", "lint": "cd ../.. && eslint \"examples/sample-server-auth/{src,test}/**/*.{ts,js}\"", "pretest:e2e": "cd ../.. && yarn workspace @concepta/rockets build && yarn workspace @concepta/rockets-auth build && ./node_modules/.bin/tsc -p examples/sample-server-auth/tsconfig.json", - "test:e2e": "vitest run --config vitest.e2e.config.ts" + "test:e2e": "vitest run --config vitest.e2e.config.mts" }, "dependencies": { "@concepta/nestjs-access-control": "8.0.0-alpha.8", @@ -46,7 +46,7 @@ "@nestjs/testing": "12.0.0-alpha.5", "@swc/core": "^1.15.47", "@types/jsonwebtoken": "^9.0.3", - "@types/node": "^18.19.44", + "@types/node": "^20.19.0", "@types/supertest": "^6.0.2", "supertest": "^6.3.4", "ts-node": "^10.9.2", diff --git a/examples/sample-server-auth/src/app.module.ts b/examples/sample-server-auth/src/app.module.ts index 72db99bcb..4ede232b5 100644 --- a/examples/sample-server-auth/src/app.module.ts +++ b/examples/sample-server-auth/src/app.module.ts @@ -1,12 +1,12 @@ import { Global, Logger, Module } from '@nestjs/common'; import { EventModule } from '@concepta/nestjs-event'; import { - buildRocketsAuthResources, defineRocketsAuth, type DefineRocketsAuthInput, type EmailSendOptionsInterface, } from '@concepta/rockets-auth'; import { RocketsModule } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import { ACService } from './access-control.service'; import { acRules } from './app.acl'; @@ -42,9 +42,8 @@ import { } from './modules/user'; import { RoleEntity, RoleDto, RoleUpdateDto } from './modules/role'; import { RoleCreateDto } from './modules/role/role.dto'; -import { defineTypeOrmRepository } from './repository/define-typeorm-repository'; -// Single TypeORM bootstrap shared by every persistence consumer below. +// Single TypeORM bootstrap owned by the auth integration below. // `defineTypeOrmRepository` returns a `RepositoryBootstrap`, which the // planner uses for both: // - `forRoot(planEntities)` — DB connection + the union of every @@ -52,9 +51,8 @@ import { defineTypeOrmRepository } from './repository/define-typeorm-repository' // `defineRocketsAuth({ persistence })`. // - `forFeature(entities)` — one `DYNAMIC_REPOSITORY_TOKEN_` // provider per registered entity. -// Reference equality matters: pass the SAME `repo` instance everywhere, -// otherwise the planner splits the entity list across two adapters and -// `TypeOrmModule.forRoot` boots with an incomplete entity set. +// `defineRocketsAuth` contributes this same bootstrap to Rockets, so the host +// does not need to repeat it in `RocketsModule.forRoot`. const repo = defineTypeOrmRepository({ type: 'sqlite', database: ':memory:', @@ -171,10 +169,6 @@ const rocketsAuthInput: DefineRocketsAuthInput = { }; const rocketsAuth = defineRocketsAuth(rocketsAuthInput); -const rocketsAuthResources = buildRocketsAuthResources( - rocketsAuthInput.persistence, - rocketsAuthInput.invitationEntity, -); @Global() @Module({ @@ -183,11 +177,7 @@ const rocketsAuthResources = buildRocketsAuthResources( PetModule, RocketsModule.forRoot({ auth: rocketsAuth, - userMetadata: rocketsAuthInput.userMetadata, - enableGlobalGuard: false, - repository: repo, resources: [ - ...rocketsAuthResources, createPetResource(), createPetVaccinationResource(), createPetAppointmentResource(), diff --git a/examples/sample-server-auth/src/main.ts b/examples/sample-server-auth/src/main.ts index 112c80515..bb83fa61a 100644 --- a/examples/sample-server-auth/src/main.ts +++ b/examples/sample-server-auth/src/main.ts @@ -25,7 +25,7 @@ import { SwaggerUiService } from '@concepta/rockets-core'; // v8 commands/queries call `AppContextHost.from(ctx)` which only accepts // an `AppContextHost`, `null`, `undefined`, or an empty object `{}`. -// The previous `createRepositoryContext(KEY)` produced `{ entity: KEY }` +// Repository calls receive the request AppContextHost // which now throws `Expected AppContextHost or nullish value, got object`. // rockets-server-auth's internal handlers pass `{}` — match that. const emptyCtx = {}; diff --git a/examples/sample-server-auth/src/repository/define-typeorm-repository.ts b/examples/sample-server-auth/src/repository/define-typeorm-repository.ts deleted file mode 100644 index 4cbb1cc9c..000000000 --- a/examples/sample-server-auth/src/repository/define-typeorm-repository.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { DynamicModule, PlainLiteralObject, Type } from '@nestjs/common'; -import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { TypeOrmRepositoryModule } from '@concepta/rockets-repository-typeorm'; -import type { RepositoryBootstrap } from '@concepta/rockets-core'; -import type { - DynamicRepositoryModule, - RepositoryProviderOptions, -} from '@concepta/nestjs-repository'; - -/** - * Returns a `RepositoryBootstrap` that: - * - Forwards `forFeature(entities)` to upstream `TypeOrmRepositoryModule`. - * - Implements `forRoot(entities)` by wrapping `TypeOrmModule.forRoot` - * with the connection options the caller passed in plus the entity set - * the Rockets planner derived from `resources[]`, `userMetadata`, and - * `defineRocketsAuth({ persistence })`. - * - * Pass the SAME instance to every persistence consumer in the app: - * - `RocketsModule.forRoot({ repository: repo })` (root adapter) - * - `defineRocketsAuth({ persistence: { module: repo } })` - * - * The planner uses reference equality to group entities per adapter and - * to decide whether to call `forRoot`. Splitting `repo` into two - * distinct objects would split the entity list and break the connection. - * - * Identical to the helper in `examples/sample-server` — duplicated here so - * each sample app boots without depending on the other's source tree. - * - */ -export function defineTypeOrmRepository< - Connection extends TypeOrmModuleOptions, ->(connection: Connection): RepositoryBootstrap { - return { - name: 'typeorm-bootstrap', - - forFeature(entities: RepositoryProviderOptions[]): DynamicRepositoryModule { - return TypeOrmRepositoryModule.forFeature(entities); - }, - - forRoot(entities: ReadonlyArray>): DynamicModule { - return TypeOrmModule.forRoot({ - ...connection, - entities: [...entities], - }); - }, - }; -} diff --git a/examples/sample-server-auth/test/role-based-access.e2e-spec.ts b/examples/sample-server-auth/test/role-based-access.e2e-spec.ts index cb596690e..5a15059cc 100644 --- a/examples/sample-server-auth/test/role-based-access.e2e-spec.ts +++ b/examples/sample-server-auth/test/role-based-access.e2e-spec.ts @@ -19,7 +19,7 @@ import { import { acRules } from '../src/app.acl'; // `AppContextHost.from()` only accepts `AppContextHost | null | undefined | {}`. -// `createRepositoryContext({entity: ...})` returns a non-empty plain object, +// Request contexts are AppContextHost instances, // which the upstream context validator rejects with "Expected AppContextHost // or nullish value, got object". The command signatures type `ctx` as // `PlainLiteralObject`, so pass `{}` — `AppContextHost.from({})` yields the diff --git a/examples/sample-server-auth/vitest.e2e.config.ts b/examples/sample-server-auth/vitest.e2e.config.mts similarity index 87% rename from examples/sample-server-auth/vitest.e2e.config.ts rename to examples/sample-server-auth/vitest.e2e.config.mts index 69e67b0f9..5acaa6214 100644 --- a/examples/sample-server-auth/vitest.e2e.config.ts +++ b/examples/sample-server-auth/vitest.e2e.config.mts @@ -1,9 +1,9 @@ import { defineProject, mergeConfig } from 'vitest/config'; -import shared from '../../vitest.shared'; +import shared from '../../vitest.shared.mts'; /** * E2E project for the sample-server-auth example — registered in the root - * `vitest.config.ts` `projects` list and runnable standalone via + * `vitest.config.mts` `projects` list and runnable standalone via * `--config` from the workspace. The former Jest `moduleNameMapper` * entries are not ported: workspace packages (`@concepta/*`) resolve to their built `dist` * through normal node resolution, and the `@nestjs/*` / `@concepta/*` / diff --git a/examples/sample-server/README.md b/examples/sample-server/README.md index d01a3c961..4d12fc3e4 100644 --- a/examples/sample-server/README.md +++ b/examples/sample-server/README.md @@ -139,7 +139,6 @@ Handwritten entity + DTO path still demonstrated in `pet-vaccination/` for compa examples/sample-server ├── src/ │ ├── auth/ AuthBootstrap + JWT signup/login -│ ├── repository/ defineTypeOrmRepository bootstrap │ ├── zod-bindings.ts bindZodResources(typeOrmZodEntityCompiler) │ ├── user-metadata.schema.ts zod schema -> { entity, createDto, updateDto, responseDto } │ ├── resources/ CRUD + sub-resource + module bundles diff --git a/examples/sample-server/package.json b/examples/sample-server/package.json index c6a6ce8fd..24b021c1f 100644 --- a/examples/sample-server/package.json +++ b/examples/sample-server/package.json @@ -7,7 +7,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "build": "tsc -p tsconfig.json", - "test:e2e": "vitest run --config vitest.e2e.config.ts" + "test:e2e": "vitest run --config vitest.e2e.config.mts" }, "dependencies": { "@concepta/nestjs-core": "8.0.0-alpha.8", @@ -39,7 +39,7 @@ "@nestjs/testing": "12.0.0-alpha.5", "@swc/core": "^1.15.47", "@types/jsonwebtoken": "^9.0.3", - "@types/node": "^18.19.44", + "@types/node": "^20.19.0", "@types/supertest": "^6.0.2", "supertest": "^6.3.4", "ts-loader": "^9.5.1", diff --git a/examples/sample-server/src/app.module.ts b/examples/sample-server/src/app.module.ts index 494fb5d90..6b907411f 100644 --- a/examples/sample-server/src/app.module.ts +++ b/examples/sample-server/src/app.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; -import { RocketsModule } from '@concepta/rockets'; +import { createServer } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import { userMetadataConfig } from './user-metadata.schema'; import { defineSampleAuth, sampleAuthUserResource } from './auth'; -import { defineTypeOrmRepository } from './repository/define-typeorm-repository'; import { petResource } from './resources/pet'; import { petVaccinationResource } from './resources/pet-vaccination'; // `/tags` is fully zod-driven (nestjs-zod DTOs + generated entity from @@ -26,33 +26,33 @@ import { adminFeature } from './admin'; import { auditFeature } from './audit'; import { eventsFeature } from './events'; -@Module({ - imports: [ - RocketsModule.forRoot({ - auth: defineSampleAuth(), - userMetadata: userMetadataConfig, - repository: defineTypeOrmRepository({ - type: 'sqlite', - database: ':memory:', - synchronize: true, - dropSchema: true, - }), - resources: [ - sampleAuthUserResource, - petResource, - petVaccinationResource, - tagZodResource, - authorZodResource, - bookZodResource, - appointmentResource, - reminderZodResource, - petShareFeature, - petTransferFeature, - adminFeature, - auditFeature, - eventsFeature, - ], - }), +export const server = createServer({ + auth: defineSampleAuth(), + userMetadata: userMetadataConfig, + repository: defineTypeOrmRepository({ + type: 'sqlite', + database: ':memory:', + synchronize: true, + dropSchema: true, + }), + resources: [ + sampleAuthUserResource, + petResource, + petVaccinationResource, + tagZodResource, + authorZodResource, + bookZodResource, + appointmentResource, + reminderZodResource, + petShareFeature, + petTransferFeature, + adminFeature, + auditFeature, + eventsFeature, ], +}); + +@Module({ + imports: [server], }) export class AppModule {} diff --git a/examples/sample-server/src/auth/define-sample-auth.ts b/examples/sample-server/src/auth/define-sample-auth.ts index f8f5bc18c..25921639d 100644 --- a/examples/sample-server/src/auth/define-sample-auth.ts +++ b/examples/sample-server/src/auth/define-sample-auth.ts @@ -1,5 +1,5 @@ import type { AuthBootstrap } from '@concepta/rockets-core'; -import { defineModuleResource } from '@concepta/rockets-core'; +import { defineAuthAdapter, defineModuleResource } from '@concepta/rockets-core'; import { UserEntity } from './user.entity'; import { AuthController } from './auth.controller'; import { SampleAuthAdapter } from './auth.adapter'; @@ -14,13 +14,7 @@ export const sampleAuthUserResource = defineModuleResource({ * `RocketsModule.forRoot({ resources: [...] })`. */ export function defineSampleAuth(): AuthBootstrap { - return { - adapter: SampleAuthAdapter, - forRoot: () => ({ - module: class SampleAuthHostModule {}, - providers: [SampleAuthAdapter], - controllers: [AuthController], - exports: [SampleAuthAdapter], - }), - }; + return defineAuthAdapter(SampleAuthAdapter, { + controllers: [AuthController], + }); } diff --git a/examples/sample-server/src/main.ts b/examples/sample-server/src/main.ts index 82347c744..21d6c3973 100644 --- a/examples/sample-server/src/main.ts +++ b/examples/sample-server/src/main.ts @@ -3,7 +3,7 @@ import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { StandardSchemaValidationPipe, ValidationPipe } from '@nestjs/common'; import { SwaggerModule } from '@nestjs/swagger'; import { cleanupOpenApiDoc } from 'nestjs-zod'; -import { AppModule } from './app.module'; +import { server } from './app.module'; import { ExceptionsFilter } from '@concepta/rockets'; import helmet from 'helmet'; @@ -12,7 +12,7 @@ import { patchMePatchOpenApi } from './swagger/patch-me-openapi'; import { SwaggerUiService } from '@concepta/rockets-core'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(server); app.use(helmet()); app.enableCors({ diff --git a/examples/sample-server/src/repository/define-typeorm-repository.ts b/examples/sample-server/src/repository/define-typeorm-repository.ts deleted file mode 100644 index 196818d75..000000000 --- a/examples/sample-server/src/repository/define-typeorm-repository.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { DynamicModule, PlainLiteralObject, Type } from '@nestjs/common'; -import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; -import { TypeOrmRepositoryModule } from '@concepta/rockets-repository-typeorm'; -import type { RepositoryBootstrap } from '@concepta/rockets-core'; -import type { - DynamicRepositoryModule, - RepositoryProviderOptions, -} from '@concepta/nestjs-repository'; - -/** - * Returns a `RepositoryBootstrap` that: - * - Forwards `forFeature(entities)` to upstream `TypeOrmRepositoryModule`. - * - Implements `forRoot(entities)` by wrapping `TypeOrmModule.forRoot` - * with the connection options the user passed in plus the entity set - * the Rockets registration plan derived from `resources[]` and - * `userMetadata`. - * - * The user passes one factory call to `RocketsModule.forRoot(...)`; the - * connection and the per-entity registration come out of a single - * source of truth. Any `entities` key on the supplied connection is - * overridden with the registration plan's entity list. - * - * The `Connection` generic carries the concrete union member (e.g. - * `SqliteConnectionOptions`) chosen at the callsite, preserving - * driver-specific discrimination through the spread. - */ -export function defineTypeOrmRepository( - connection: Connection, -): RepositoryBootstrap { - return { - name: 'typeorm-bootstrap', - - forFeature(entities: RepositoryProviderOptions[]): DynamicRepositoryModule { - return TypeOrmRepositoryModule.forFeature(entities); - }, - - forRoot( - entities: ReadonlyArray>, - ): DynamicModule { - return TypeOrmModule.forRoot({ - ...connection, - entities: [...entities], - }); - }, - }; -} diff --git a/examples/sample-server/test/zod-full-coverage.e2e-spec.ts b/examples/sample-server/test/zod-full-coverage.e2e-spec.ts index 958b0cb50..5ed5e63d2 100644 --- a/examples/sample-server/test/zod-full-coverage.e2e-spec.ts +++ b/examples/sample-server/test/zod-full-coverage.e2e-spec.ts @@ -18,8 +18,8 @@ import { z } from 'zod'; import { ExceptionsFilter, RocketsModule, - defineTypeOrmRepository, } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import type { RocketsRepositoryModuleInterface, SchemaEntityCompiler, diff --git a/examples/sample-server/test/zod-parity.e2e-spec.ts b/examples/sample-server/test/zod-parity.e2e-spec.ts index 51b731477..e879c0a94 100644 --- a/examples/sample-server/test/zod-parity.e2e-spec.ts +++ b/examples/sample-server/test/zod-parity.e2e-spec.ts @@ -11,8 +11,8 @@ import request from 'supertest'; import { ExceptionsFilter, RocketsModule, - defineTypeOrmRepository, } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import type { ResourceInput } from '@concepta/rockets'; import { UserMetadataCreateDto, diff --git a/examples/sample-server/test/zod-swagger-golden.e2e-spec.ts b/examples/sample-server/test/zod-swagger-golden.e2e-spec.ts index 7e6995356..25ba6ccd3 100644 --- a/examples/sample-server/test/zod-swagger-golden.e2e-spec.ts +++ b/examples/sample-server/test/zod-swagger-golden.e2e-spec.ts @@ -7,7 +7,8 @@ import { SwaggerModule, } from '@nestjs/swagger'; import { cleanupOpenApiDoc } from 'nestjs-zod'; -import { RocketsModule, defineTypeOrmRepository } from '@concepta/rockets'; +import { RocketsModule } from '@concepta/rockets'; +import { defineTypeOrmRepository } from '@concepta/rockets-repository-typeorm'; import type { ResourceInput } from '@concepta/rockets'; import { UserMetadataCreateDto, diff --git a/examples/sample-server/vitest.e2e.config.ts b/examples/sample-server/vitest.e2e.config.mts similarity index 84% rename from examples/sample-server/vitest.e2e.config.ts rename to examples/sample-server/vitest.e2e.config.mts index 62e44af8c..a131ff13f 100644 --- a/examples/sample-server/vitest.e2e.config.ts +++ b/examples/sample-server/vitest.e2e.config.mts @@ -1,9 +1,9 @@ import { defineProject, mergeConfig } from 'vitest/config'; -import shared from '../../vitest.shared'; +import shared from '../../vitest.shared.mts'; /** * E2E project for the sample-server example — registered in the root - * `vitest.config.ts` `projects` list and runnable standalone via + * `vitest.config.mts` `projects` list and runnable standalone via * `--config` from the workspace. */ export default mergeConfig( diff --git a/firebase.json b/firebase.json new file mode 100644 index 000000000..ecfc838f2 --- /dev/null +++ b/firebase.json @@ -0,0 +1,14 @@ +{ + "firestore": { + "rules": "firestore.rules" + }, + "emulators": { + "firestore": { + "port": 8088 + }, + "ui": { + "enabled": false + }, + "singleProjectMode": true + } +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 000000000..b9dd67c55 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} diff --git a/package.json b/package.json index aa0321827..da2a6c4e8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "0.0.1-dev.0", "license": "BSD-3-Clause", "private": true, + "engines": { + "node": ">=20.0.0" + }, "workspaces": { "packages": [ "packages/*", @@ -22,7 +25,6 @@ "@nestjs/swagger": "12.0.0-alpha.2", "jws": "3.2.3", "qs": "6.15.2", - "path-to-regexp": "8.4.0", "form-data": "4.0.6", "multer": "2.2.0", "tar-fs": "2.1.4", @@ -66,7 +68,7 @@ "@nestjs/testing": "12.0.0-alpha.5", "@swc/core": "^1.15.47", "@types/express": "^4.17.21", - "@types/node": "^18.19.44", + "@types/node": "^20.19.0", "@types/nodemailer": "^6.4.15", "@types/supertest": "^6.0.3", "@typescript-eslint/eslint-plugin": "^5.62.0", @@ -81,6 +83,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-tsdoc": "^0.3.0", + "firebase-tools": "15.15.0", "husky": "^7.0.4", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -116,15 +119,16 @@ "lint:all": "yarn lint && yarn lint:md", "test": "vitest run --project unit", "test:watch": "vitest --project unit", + "test:config-native": "vitest list --project unit --configLoader native", "test:cov": "vitest run --project unit --coverage --coverage.thresholds.statements=50 --coverage.thresholds.branches=50 --coverage.thresholds.functions=40 --coverage.thresholds.lines=50", "test:ci": "vitest run --project unit --coverage --coverage.thresholds.statements=50 --coverage.thresholds.branches=50 --coverage.thresholds.functions=40 --coverage.thresholds.lines=50 --reporter=default --reporter=junit --outputFile=junit.xml", "test:debug": "vitest --inspect-brk --no-file-parallelism", "test:e2e": "vitest run --project e2e-packages", + "test:firestore-emulator": "firebase emulators:exec --only firestore --project demo-rockets --config firebase.json 'vitest run --config vitest.firestore.config.mts'", "test:e2e:cov": "vitest run --project e2e-packages --coverage --coverage.reportsDirectory=coverage-e2e --maxWorkers=1", "test:all": "vitest run", "doc": "rimraf ./docs && typedoc", "doc:cov": "yarn doc --coverageOutputType all", - "generate-swagger": "cd packages/rockets-server-auth && yarn generate-swagger", "sample:start": "yarn workspace sample-server start:dev", "sample:go": "yarn build && yarn workspace sample-server start:dev", "sample:once": "yarn workspace sample-server start", @@ -140,14 +144,15 @@ "sample-code-review:dev": "yarn build && yarn workspace sample-code-review dev", "sample-code-review:build": "yarn build && yarn workspace sample-code-review build", "sample-code-review:test:e2e": "yarn build && yarn workspace sample-code-review test:e2e", - "samples:build": "yarn sample:build && yarn sample-auth:build", - "samples:test:e2e": "yarn sample:test:e2e && yarn sample-auth:test:e2e", + "samples:build": "yarn sample:build && yarn sample-auth:build && yarn workspace sample-code-review build", + "samples:test:e2e": "yarn sample:test:e2e && yarn sample-auth:test:e2e && yarn workspace sample-code-review test:e2e", "codacy:analyze": ".codacy/cli.sh analyze", "codacy:analyze:fix": ".codacy/cli.sh analyze --fix", "codacy:analyze:file": ".codacy/cli.sh analyze", "codacy:analyze:security": ".codacy/cli.sh analyze -t trivy", "typecheck:spec": "tsc --noEmit -p tsconfig.spec.json && yarn workspace @concepta/rockets-core test:typetests", - "release:check": "node -e \"const v=process.env.npm_config_user_agent||'';if(!/yarn\\\\/4/.test(v)){console.error('\\\\n Release commands require Yarn 4. Run them via: corepack yarn