Configurable Monorepo: User-zone config, route hooks, and config validation - #125
Conversation
…ute guards The shareable monorepo config migration was incomplete in two key areas: 1. Config pipeline gap: email and authBehavior settings defined in ottabase.config.ts were silently ignored because AppConfig, ConfigOptions, createAppConfig(), and userConfigToOptions() all lacked support for these fields. Now the full chain works: ottabase.config.ts → userConfigToOptions → createAppConfig → AppConfig with env var overrides for backward compat. 2. Package route guarding: disabling a package via packages.ottablog: false only removed its DB tables from migrations but left API routes active, causing 500 errors on access. Now all package-specific routes in the worker router check PACKAGES toggles and return a clear 404 with PACKAGE_DISABLED code when the package is off. Additional fixes: - Migrate queue/handlers.ts from raw env vars to worker-config imports - Update bootstrap wizard to reflect EMAIL_FROM moving to ottabase.config.ts - Update email route provider info to reference config instead of env vars - Add DEFAULT_EMAIL_CONFIG, DEFAULT_AUTH_BEHAVIOR_CONFIG constants - Export AUTH_BEHAVIOR_CONFIG and EMAIL_CONFIG from app.config.ts - Add PACKAGES toggle export to worker-config.ts https://claude.ai/code/session_01VrsgTM282BW4TFws7ZfnkE
…uthBehavior/email config tests (#124) * fix: address PR review comments - unused imports, email API backward compat, add config tests
Separate framework-tracked files from user-owned files so `git pull` never overwrites user customisations: - ottabase.config.ts → ottabase.config.example.ts (tracked template) - wrangler.jsonc → wrangler.example.jsonc (tracked; CI uses this) - ottabase/ → ottabase.template/ (tracked reference copy) User working copies (ottabase.config.ts, wrangler.jsonc, ottabase/) are gitignored. First-time setup: copy from .example/.template. Updates CI workflows (deploy.yml, pr-preview.yml), cloudflare-config.json, and README with new setup instructions and directory structure. https://claude.ai/code/session_01VrsgTM282BW4TFws7ZfnkE
Custom packages can now register API routes without editing the framework-owned router.ts. Users add handlers in the user-zone file `ottabase/config.routes.ts` (same pattern as config.migrations.ts). - Extract ApiRouteContext to worker/routes/types.ts (shared type) - Add ottabase.template/config.routes.ts with scaffold + docs - Wire handleCustomRoutes() into resolveApiRoute() as fallback after all built-in routes - Update README with route registration in premium package workflow https://claude.ai/code/session_01VrsgTM282BW4TFws7ZfnkE
- types.ts: add field-level JSDoc on ApiRouteContext with usage example - ottabase.config.example.ts: mention config.routes.ts in premium package steps - config.migrations.ts: cross-reference config.routes.ts in setup steps - README: add dedicated "Custom Routes" section with example + resolution order - README: update File Ownership table to include routes https://claude.ai/code/session_01VrsgTM282BW4TFws7ZfnkE
Typos in ottabase.config.ts no longer silently fall to defaults. defineOttabaseConfig() now validates at startup: - Throws on missing required fields (appId, appName) - Warns on unrecognised keys at all nesting levels (top-level, packages, features, features.authBehavior, email, etc.) Adds validateOttabaseConfig() as a standalone export for testing. 12 new tests covering required fields, typo detection at every nesting level, multiple warnings, and integration with defineOttabaseConfig. https://claude.ai/code/session_01VrsgTM282BW4TFws7ZfnkE
There was a problem hiding this comment.
Pull request overview
This pull request introduces a configurable monorepo architecture with user-zone file separation, custom route registration, and config validation. The key innovation is gitignoring ottabase.config.ts, wrangler.jsonc, and the ottabase/ directory (which contain user customizations) while providing tracked .example and .template counterparts, ensuring framework updates never overwrite user code.
Changes:
- User-zone file separation: Three user-owned areas (
ottabase.config.ts,wrangler.jsonc,ottabase/) are now gitignored with tracked templates, preventing framework updates from overwriting customizations - Config validation:
defineOttabaseConfig()validates required fields and warns on typos via recursive key checking (3 levels deep) - Route registration hook: Custom packages can register API routes via
ottabase/config.routes.tsusing the newApiRouteContexttype, called as final fallback in the router - Template scaffolding: Queue handlers, Todo model, migration system, and helper utilities provided in
ottabase.template/as starting point - CI/CD updates: Workflows updated to use
wrangler.example.jsonc(tracked) instead ofwrangler.jsonc(gitignored)
Reviewed changes
Copilot reviewed 15 out of 26 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
packages/config/src/createAppConfig.ts |
Adds validateOttabaseConfig() with 3-level recursive key validation; warns on typos, throws on missing required fields |
packages/config/src/__tests__/config.test.ts |
12 new validation tests covering required fields, typos, and nested key warnings |
packages/config/src/index.ts |
Exports validateOttabaseConfig for external use |
worker/routes/types.ts |
New file defining shared ApiRouteContext interface to avoid circular imports |
worker/routes/router.ts |
Imports custom routes handler and calls it as final fallback after built-in routes |
ottabase.template/config.routes.ts |
User-zone scaffold for custom route registration with documentation |
ottabase.template/config.migrations.ts |
Updated comments referencing route registration in config.routes.ts |
ottabase.template/queue/index.ts |
Queue handler registry with DLQ support, stats tracking, and KV persistence |
ottabase.template/queue/handlers.ts |
Email, order processing, report generation, and sync job handlers |
ottabase.template/models/Todo.ts |
Example app-specific fat model with helper methods |
ottabase.template/models/Todo.schema.ts |
Drizzle schema for todos table |
ottabase.template/migrations/index.ts |
Migration registry combining core, app, and package migrations |
ottabase.template/migrations/README.md |
Comprehensive migration system documentation (337 lines) |
ottabase.template/helpers/referral-attribution.ts |
Server-side referral attribution helper with context capture |
ottabase.template/helpers/__tests__/referral-attribution.test.ts |
Test coverage for referral attribution edge cases |
ottabase.template/db/schemas-helper.ts |
Collects schemas from core, app, and package sources for autoInit |
ottabase.template/db/schema.ts |
Re-exports all table schemas for drizzle-kit |
ottabase.config.example.ts |
Updated template with validation documentation and route registration instructions |
wrangler.example.jsonc |
Updated header comments explaining CI usage and local workflow |
cloudflare-config.json |
Changed wranglerConfig to point to wrangler.example.jsonc |
.gitignore |
Adds gitignore entries for ottabase.config.ts, wrangler.jsonc, and ottabase/ directory |
README.md |
Major updates: first-time setup section, file ownership table, custom routes documentation, directory structure |
.github/workflows/pr-preview.yml |
Updates wrangler config references from wrangler.jsonc to wrangler.example.jsonc |
.github/workflows/deploy.yml |
Updates wrangler config references and comments |
.github/README.md |
Documentation updates for new wrangler config pattern |
| describe('defineOttabaseConfig – validation integration', () => { | ||
| it('should log warnings for unknown keys', () => { | ||
| const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); | ||
| defineOttabaseConfig({ | ||
| appId: 'test', | ||
| appName: 'Test', | ||
| packges: { ottablog: true }, // typo | ||
| } as any); | ||
| expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown key "packges"')); | ||
| warnSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should throw on missing required fields', () => { | ||
| expect(() => defineOttabaseConfig({ appId: '', appName: 'Test' } as any)).toThrow('"appId" is required'); | ||
| }); | ||
|
|
||
| it('should still return the config object on success', () => { | ||
| const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); | ||
| const cfg = defineOttabaseConfig({ appId: 'ok', appName: 'OK' }); | ||
| expect(cfg.appId).toBe('ok'); | ||
| expect(cfg.appName).toBe('OK'); | ||
| warnSpy.mockRestore(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The console.warn spy is created and restored within individual test cases, but if a test fails or throws an error before reaching warnSpy.mockRestore(), the spy will leak into subsequent tests. According to the stored memory about this codebase, "Global vitest setup does not call vi.restoreAllMocks()/vi.resetAllMocks() between tests; individual tests must restore spies/mocks they create."
Consider using afterEach to ensure cleanup happens even when tests fail:
describe('defineOttabaseConfig – validation integration', () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
warnSpy?.mockRestore();
});
// tests...
});This follows the same pattern needed elsewhere in the codebase due to the global setup not handling mock restoration.
Add a setup script and npm hooks to ensure user-zone files exist before dev/build. Introduce scripts/setup-user-zone.js which copies ottabase.template → ottabase and wrangler.example.jsonc → wrangler.jsonc when targets are missing, and wire it into package.json (setup, predev, prebuild). Make worker/router load user custom routes via dynamic import with a no-op fallback to avoid crashes when the user-zone is not present. Tighten createAppConfig validation to require non-empty (trimmed) appId and appName. Update tests to centralize console.warn spying with beforeEach/afterEach and adjust import ordering.
ottabase.config.ts,wrangler.jsonc, andottabase/are now gitignored with tracked.example/.templatecounterparts, so framework updates never overwrite user customizationsottabase/config.routes.tswithout editing framework-ownedrouter.ts— mirrors the existingconfig.migrations.tspattern for tablesdefineOttabaseConfig()now throws on missing required fields (appId,appName) and warns on unrecognized keys at all nesting levels (catches typos likepackgesorfeatures.authBehaviourthat previously fell silently to defaults)from/sesRegionand auth behavior flags (sessionMaxAge,requireEmailVerified,disableCredentials,verbose) moved from env vars toottabase.config.tswith env var override kept for backward compatPACKAGE_DISABLEDcode when toggled off in configKey files
Test plan
pnpm dev:workerstarts without config validation errorsottabase.config.ts(e.g.packges) produces[ottabase] Unknown keywarningappIdthrows clear error at startupottabase/config.routes.tsresponds correctlyPACKAGE_DISABLED