3.8 KiB
plugin/plugin.ts
(no submodules)
Purpose
Owns createSitePlugin, the canonical factory that produces the PluginDescriptor consumed by @suaveplan/api-plugin's PluginRegistry. The factory composes every other module in this package — SitePluginOptionsSchema validates options at the boundary, the SiteService + DomainResolverService are constructed once, the Hono middleware is built and exposed through ServiceRegistry, the public + private route trees are mounted under the configured routePrefix, and the optional organization:created listener is wired when autoCreateDefaultSite is on. This file is the single integration point — every consumer of the package builds its descriptor through this one function.
The lifecycle is the canonical 4-phase shape: validate-at-construction, build-services in onInit, mount-routes in onReady, drop-listener in onClose. Each phase fails fast with a typed SiteError carrying a SITE.* code so misconfigurations are visible at registry intake rather than deferred to the first request.
Features
- Zod-validated option boundary —
SITE.INVALID_OPTIONSon bad config. - Resolves
dbfromServiceRegistry; throwsSITE.SERVICE_NOT_AVAILABLEwhen missing. - Constructs and registers
siteService,domainResolver,domainResolverMiddleware. - Optional
organization:createdlistener for default-site auto-bootstrap, withSITE.BOOTSTRAP_FAILEDevent emission on error and thesite.bootstrap_failed_totalcounter incremented on each failure. - Route mounting under
config.routePrefix(defaults to/api) with prefix-collision detection viaSITE.ROUTE_PREFIX_COLLISION. optionalDependencies: ["database", "auth", "events"]so orchestrators topo-sort the site plugin after siblings without making them mandatory.- Emits
site:initializedandsite:readylifecycle events.
Usage: Basic
import { createSitePlugin } from "@suaveplan/api-plugin-site";
registry.register(createSitePlugin({ maxSitesPerOrg: 25 }));
Usage: Advanced
import { createSitePlugin } from "@suaveplan/api-plugin-site";
const plugin = createSitePlugin({
routePrefix: "/v1",
autoCreateDefaultSite: true,
defaultDomainPattern: "{slug}.acme.com",
enableDomainResolution: true,
});
await registry.register(plugin, { db: surrealAdapter, app: openApiHono });
await registry.start();
// Downstream plugin can now resolve the service:
const siteService = ctx.getService("siteService")!;
await siteService.create({
organization_id: "organization:abc",
name: "Customer Site",
domains: ["customer.acme.com"],
});
API Reference
createSitePlugin(options?: CreateSitePluginOptions): PluginDescriptor<SitePluginConfig>type CreateSitePluginOptions = SitePluginOptionsInput- The descriptor name is
"site", version"0.1.0".
Implementation Notes
- Service handles are stored in closure-scoped variables and bound via a private
bindhelper so theonReadyhook can reuse the same instances constructed inonInit. ThemustService/mustResolverguards trip when an orchestrator runsonReadybeforeonInit(which the canonical registry never does, but tests / misuse can). - The plugin treats the host event bus as
EventBusLike<SiteEventMap>so the cumulative event typing is enforced inside the plugin without bleeding into the globalEventBusLikedeclaration on the host. - Route mounting is keyed by detecting any existing path starting with
${prefix}/sites/; a collision raisesSITE.ROUTE_PREFIX_COLLISIONso two plugins can't silently overwrite each other. enableDomainResolution: falsesuppresses ONLY the public router (which contains the resolve endpoint); the private router always mounts so CRUD remains usable even when public lookup is disabled.