# Conventions

Rules for writing CFML under `app/api/`. Most exist because something broke.

---

## The two allowlists

An endpoint is reachable only if **both** agree:

1. `Registry.cfc` has an entry mapping the action to `{ cfc, method }`
2. that component's `this.routable` lists the method name

This is not redundancy. `Base.cfc` is inherited by every handler, so without the
second list a caller could route to `qGame`, `requireKey` or `apiError` — every
inherited helper would be a public endpoint. The registry decides *what exists*;
`this.routable` decides *what the component is willing to expose*.

### Never name the allowlist after a method

A property on `this` **shadows a method of the same name.** The allowlist used to
be `this.endpoints`, which made `Meta.endpoints()` unreachable:

```
Member [endpoints] of component [components.Meta] is not a function
```

It is `this.routable`. Do not rename it to anything a handler might plausibly
want as a method.

## Registry entries stay data-only

`Registry.cfc` must not query, hold state, or extend `Base`. The in-game docs
page instantiates it from the **game** CFML application, where none of the API's
plumbing exists.

## Reserved scopes

`server`, `local`, `url`, `form`, `request`, `application` are CFML scopes. This
looks fine and is not:

```cfml
var server = pTrim("server", "");   // resolves to the SERVER scope
```

It has already caused a cast error in `Ranks.cfc` and an unreachable-host error
in `Dsr.cfc`. Name locals `slot`, `srv`, `rows`, `spec`.

## Never write an angle-bracketed tag name in a CFC comment

Lucee parses `<cfquery>` inside a `/** */` block and tries to validate it, which
fails the whole component at parse time. Write "the cfquery tag" in prose.

## Every dynamic value is a bound parameter

```cfml
var rows = qGame("
    SELECT id, nic FROM `user`
    WHERE server = :slot AND power > :floor
    ORDER BY power DESC
    LIMIT #int(lim)#
", {
    slot:  [ slot,      "integer" ],
    floor: [ powerMin,  "bigint"  ]
});
```

`LIMIT` cannot be bound in MySQL, so it is inlined — but only ever from an
integer already clamped by `pRange`/`pInt`, never from raw input.

## Read the params through the helpers

`p`, `pInt`, `pTrim`, `pRange` coerce and bound. Reading `url.x` directly skips
that and is how an unclamped `limit` reaches a query.

## Servers are named, not numbered

Callers send `TB` or `RT`; `resolveServer()` maps them to slots 3 and 4 and
defaults to the caller's own server. Never expose or accept a raw slot number —
the numbering is an internal detail and servers 0–2 are dead.

## Exit through the envelope

Every path ends in `apiOut(...)` or `apiError(...)`. Both terminate the request.
Falling off the end of a handler triggers the router's
`"Handler produced no response"` guard, which is a bug, not a response.

## Cache deliberately, and say so

`cached(key, ttl, producer)` memoises in application scope; `cacheMeta(ttl, n)`
tells the caller what they got. Pick the TTL from how fast the data actually
changes — ranks move once a minute, planet types never. `0` means never cache.

The cache key must include every input that changes the result:

```cfml
cached("ranks_top50_" & slot & "_" & by, 60, function() { ... })
```

A key that ignores an input serves one caller's data to another.

## Access rules fail closed

`requireAccess` understands `""` and `project:<n>`. Anything it does not
recognise is denied, in both `Base.requireAccess` and `Meta.canAccess`. If you
add a rule kind, add it to both or `meta.whoami` will advertise endpoints that
then 403.

## Log every call, never the key

`logAccess(status)` writes to `gcc_log.api_log`. `safeParams()` strips the key
before the parameters are recorded. Never log, echo, or return a key.

## Style

Allman braces, no space after `if(`, single-statement bodies unbraced — matching
the rest of the codebase:

```cfml
if (!len(trim(rule))) return true;

if (rows.recordcount == 0)
{
    apiError(404, "No such ship");
}
```

## SQL migrations declare their database

The API's schema changes live in `app/Admin/sql/` alongside every other
migration. Each file **starts with a `USE`** (after its header comment, before
the first statement), or fully qualifies every object it touches.

`05_api.sql` is the one file that legitimately spans two databases — it adds
`gcc.user.api_key` and creates `gcc_log.api_log`. It does **both**: every object
is fully qualified *and* it carries a `USE gcc;`, so anything added to it later
without a qualifier lands somewhere deliberate rather than wherever the session
happened to be pointing.

```sql
-- This file touches TWO databases -- gcc.user and gcc_log.api_log -- and every
-- object below is fully qualified, so the USE is not what routes them. It is
-- here so that anything added later without a qualifier lands in the game
-- database.
USE `gcc`;

ALTER TABLE `gcc`.`user` ADD COLUMN `api_key` VARCHAR(64) NULL DEFAULT NULL;
```

Either style is acceptable; `USE` alone is the norm because it is one line
instead of a prefix per statement. What is not acceptable is a file that relies
on the invoker passing the right database on the command line — with the `USE`
present, `mysql -u WolfrenInd -p < file.sql` is correct with no DB argument.
