feat(express-context): add modular per-database cached lookup system#1215
Merged
Conversation
Introduces a pluggable ModuleLoader registry that replaces the hardcoded per-db lookups currently baked into graphql/server's api.ts. Key additions: - LoaderContext: provides both services and tenant pools to loaders - createModuleLoader: factory wrapping resolve functions with LRU caching - LoaderRegistry: manages loaders, resolves all in parallel - Built-in loaders for all existing module types: - rlsModule (services_public.rls_settings + legacy fallback) - corsOrigins (services_public.cors_settings + legacy fallback) - databaseSettings (services_public.database_settings + api_settings merge) - pubkeyChallengeSettings (services_public.pubkey_settings + legacy fallback) - webauthnSettings (services_public.webauthn_settings) - authSettings (two-step metaschema discovery -> tenant DB query) - ResolvedModules type on ConstructiveContext - createDefaultRegistry() convenience function - Async context middleware with loader resolution Each loader owns its own LRU cache (per databaseId, independent TTL), replacing the monolithic svcCache blob approach. New per-db lookups can be added by implementing a ModuleLoader and registering it — no changes to api.ts or ApiStructure required.
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…dule Replace eager resolveAll() pattern with lazy useModule(name): - Registry gains resolve(name, ctx) for single-loader on-demand access - ConstructiveContext.useModule replaces the modules map - buildContext is sync again (no eager SQL at middleware time) - createContextMiddleware is sync again (no async needed) - BuiltinModuleMap provides typed overloads for built-in module names - Registry.has(name) added for checking loader availability Middleware only pays for what it uses — if a request never touches webauthn, the webauthn loader never fires. Results are cached per databaseId in each loader's own LRU, so repeated calls are free.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces a pluggable ModuleLoader registry in
@constructive-io/express-contextwith lazy, on-demand resolution — loaders only fire SQL queries when middleware actually requests them.Problem
All per-db config lookups (
rlsModule,authSettings,databaseSettings,corsOrigins,pubkeyChallengeSettings,webauthnSettings) are hardcoded inapi.ts, fetched eagerly in parallel on every request (even if the request never needs them), and cached as a single monolithic blob insvcCache. Adding a new per-db lookup means editingapi.ts, extendingApiStructure, and wiring into thePromise.all.Solution
Lazy
useModulepattern — middleware requests only what it needs:Architecture:
ModuleLoader<T>— encapsulates SQL query + type transform + per-databaseId LRU cachecreateModuleLoader()factory — wraps a resolve function with LRU caching (independent TTL per loader)LoaderRegistry— manages loaders, supports both lazyresolve(name, ctx)and eagerresolveAll(ctx)LoaderContext— providesservicesPool(Tier 1) +tenantPool(Tier 2) +databaseId+apiIduseModule(name)onConstructiveContext— typed lazy accessor bound to the request's loader contextBuiltinModuleMap— maps built-in loader names to their types for full type safety6 built-in loaders:
rlsLoaderservices_public.rls_settingsapi_modulesfallbackcorsLoaderservices_public.cors_settingsdatabaseSettingsLoaderservices_public.database_settingspubkeyLoaderservices_public.pubkey_settingswebauthnLoaderservices_public.webauthn_settingsauthSettingsLoadermetaschema_modules_public.sessions_moduleZero breaking changes
ApiStructurefields (rlsModule,authSettings, etc.) remain untouchedreq.api.rlsModulestill works exactly as beforeuseModulepath coexists alongside the old onebuildContextandcreateContextMiddlewareremain sync (no async middleware needed)Adding a new per-db lookup
What's next
api.tsto use loaders internally (remove inline SQL, keep ApiStructure populated)req.api.rlsModule→await ctx.useModule('rlsModule')svcCache(each loader has its own cache)Review & Testing Checklist for Human
BuiltinModuleMapcovers all the types downstream middleware expects — the overloadeduseModulesignature gives typed returns for built-in names andunknownfor customapi.ts→auth.ts→ pgSettings) still reads fromreq.api.rlsModule— confirm this path is unmodifiedapi.ts— worth a diff to confirm no query divergenceNotes
lru-cacheadded as a direct dep to avoid pulling in heavy PostGraphile transitive deps fromgraphile-cache.resolveAll()method is kept for backwards compat / pre-warming, but the recommended pattern isuseModule(name)for lazy access.Link to Devin session: https://app.devin.ai/sessions/151753dc357b48528652af31a63decc0
Requested by: @pyramation