9.2 KiB
Copilot / AI Agent Instructions for ttscm-api
Purpose: make AI coding agents immediately productive in this repository by describing architecture, conventions, workflows, and helpful code pointers.
-- Big picture: This is a TypeScript API service (runs on Bun) using the Hono framework. The HTTP surface is implemented in src/api where small router files are mounted on a versioned router in src/api/server.ts (see the /v1 mount). Typical request flow is:
-
router(insrc/api/routers/*) →controller(insrc/controllers/*) →manager(insrc/managers/*) →module/generated/prismafor persistence and external integrations. -
Keep each layer focused: routers handle routing & middleware, controllers handle request validation and high-level orchestration, managers encapsulate domain/persistence logic, modules provide shared utilities (external API clients, helpers).
-
Runtime / tooling: The project runs on Bun. Dev command:
npm run dev(runsbun --watch src/index.ts). DB tooling uses Prisma; generated client lives undergenerated/prisma(do NOT edit generated files). Key scripts inpackage.json:dev— start the server with Bun in watch modedb:gen—prisma generatedb:push—prisma migrate dev --skip-generate
-
Data layer: Prisma schema is at
prisma/schema.prisma. The app imports the generated Prisma client fromgenerated/prisma/client.ts(orgenerated/prisma/browser.tsin browser contexts). Always run thedb:genscript after updatingschema.prisma. -
Routing & controllers: Example flow: a request hits a
routerinsrc/api/routers/*→ router delegates to acontrollerinsrc/controllers/*→ controller calls amanagerinsrc/managers/*for domain/persistence logic. Important concrete patterns:src/api/server.tsmountsv1and usesv1.route("/auth", require("./routers/authRouter").default)style requires; preserve this shape when adding routes.- Router files export a default Hono router object (CommonJS
module.exports/export defaultmixture is used across the codebase). - Controllers are single-export modules named like
CompanyController.tswith named methods per action (e.g.,fetch,update). Prefer small methods that call intomanagers/*. - Managers are thin domain layers (e.g.,
managers/companies.ts) that wrapgenerated/prismacalls and other I/O. Keep side effects here, keep controllers pure orchestration. - Use
src/modules/api-utils/apiResponse.tsfor every HTTP response shape (successful/created/error/internalError/zodError). - Use Zod schemas in controllers for request validation; server-level
app.onErrormapsZodErrortoapiResponse.zodError.
API layout & conventions (how to add a new endpoint)
- Add router: create
src/api/routers/<thing>Router.tsexporting a Hono router and mount it insrc/api/server.tsunder thev1router. - Add controller: create
src/controllers/<Thing>Controller.tsexporting functions for each action. Controllers should validate input with Zod, call managers, and returnapiResponse.*results. - Add manager: create
src/managers/<things>.tsfor persistence/domain logic. Use the generated Prisma client (generated/prisma/client.ts) here; do not import Prisma directly in controllers. - Add modules/types: if needed, add helpers to
src/modules/*and runtime types tosrc/types/*. - Middleware & auth: use
src/api/middleware/authorization.tsfor protecting routes; follow existing token/session patterns fromsrc/controllers/SessionController.tsandsrc/Errors/*. - Error handling: throw repository-specific errors from
src/Errors/*(includestatus,name,message, optionalcause) and letsrc/api/server.tsmap them viaapiResponse.error. - Naming: prefer plural manager filenames (
companies.ts) and singular controller names (CompanyController.ts) — follow existing files.
Examples & notable files
-
src/api/server.ts— mountsv1, registerscors, centralonErrorhandler andnotFoundresponse. -
src/api/routers/companyRouter.tsandsrc/controllers/CompanyController.ts— canonical example for adding company endpoints. -
src/api/user/@me/*— nested route example (use Hono sub-routers for subpaths). -
src/modules/cw-utils/*— external API integrations; keep interfaces stable and return domain objects consumed by managers. -
Validation & errors: Zod is used for input validation; Zod errors are handled centrally in
src/api/server.tsviaapiResponse.zodError. Application errors use custom error classes insrc/Errors/*(e.g.,AuthenticationError.ts,GenericError.ts). When creating errors, follow the shape used in existing errors (includestatus,name,message, and optionalcause). -
Response pattern: Use the
apiResponsehelpers insrc/modules/api-utils/apiResponse.tsfor formatting responses and status codes (successful, created, error, internalError, zodError). -
Auth & external integrations: Microsoft OAuth flow is under
src/api/auth/*andsrc/modules/fetchMicrosoftUser.ts. If touching authentication, follow existing redirect/refresh patterns insrc/api/authand preserve token-refresh semantics. -
ConnectWise integration: Utilities for ConnectWise interactions live in
src/modules/cw-utils/*(e.g.,fetchCompanyConfigurations.ts). These modules often call external APIs and return domain data; preserve the module contracts (input types and returned shapes) when refactoring. -
Generated files and CI:
generated/is a build artifact. Do not modify. When updating Prisma models, runnpm run db:genand commit changes togenerated/prismaonly if that's the established workflow. -
Files to inspect for context / examples:
src/api/server.ts— central error handling, router mounting, CORS and not-found handling.src/modules/api-utils/apiResponse.ts— response shaping used across controllers.src/controllers/CompanyController.ts— example controller calling managers.src/modules/cw-utils/fetchCompanyConfigurations.ts— example external integration utility.generated/prisma/client.ts— generated Prisma client imports; avoid editing.
-
Coding conventions & patterns specific to this repo:
- Prefer the existing layered architecture: routers → controllers → managers → modules.
- Use the
apiResponsehelpers for all HTTP responses. - Throw or propagate repository-specific custom errors (from
src/Errors/*) rather than returning bare objects. - Keep TypeScript types in
src/types/*and use Zod for runtime checks. - Avoid
elsestatements — prefer ternary operators, early returns, or other control flow patterns. Only useelseif there is absolutely no other way.
-
Local dev / quick checks:
- Start dev server:
npm run dev - Regenerate Prisma client:
npm run db:gen - Apply DB migrations locally:
npm run db:push
- Start dev server:
-
When editing generated or infra files: if you need to change
generated/prisma/*(rare), explain why in the PR and show commands used to regenerate. -
Documentation sync (IMPORTANT): Whenever you add, remove, or modify API routes or permission nodes, you must update all three of the following files to keep them in sync:
src/types/PermissionNodes.ts— the single source of truth for all permission node definitions, categories, descriptions, andusedInreferences.PERMISSIONS.md— human-readable documentation of all permission nodes; must strictly reflect the data inPermissionNodes.ts.API_ROUTES.md— comprehensive documentation of all API routes, including method, path, auth requirements, permissions, request/response examples. Always verify that new routes have their required permissions listed inPermissionNodes.ts, thatPERMISSIONS.mdtables match the TS file exactly, and thatAPI_ROUTES.mdincludes full documentation for every mounted route. Run through all three files at the end of any route or permission change to catch discrepancies.
-
Field-level permission gating (processObjectValuePerms): Some routes use
processObjectValuePermsfromsrc/modules/permission-utils/processObjectPermissions.tsto filter response objects on a per-field basis. When this pattern is used, every key of the response object becomes a permission node in the form<scope>.<field>(e.g.,unifi.site.wifi.read.passphrase). Only fields whose corresponding permission the user holds are included in the response.When documenting a route that uses field-level gating, you must:
- Note in
API_ROUTES.mdthat the route uses field-level gating, explain the behaviour, and list every<scope>.<field>permission node in a collapsible table. - Add a
unifi.site.wifi.read-style parent permission node inPermissionNodes.tswith afieldLevelPermissionsarray listing every<scope>.<field>node. - Add matching rows/notes to
PERMISSIONS.mdincluding the full list of field-level nodes.
Current routes using field-level gating:
GET /v1/unifi/site/:id/wifi— scopeunifi.site.wifi.read, gates every field on theWlanConfobject.
- Note in
If anything here is unclear or you'd like more examples (e.g., a walk-through editing a controller + manager + test run), tell me which area to expand and I'll iterate.