Skip to content

Commit 542782d

Browse files
ggazzoclaude
andcommitted
docs: update API endpoint migration tracking documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3a1bb75 commit 542782d

1 file changed

Lines changed: 98 additions & 8 deletions

File tree

docs/api-endpoint-migration.md

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,22 +358,29 @@ integrations: {
358358

359359
### Handling nullable types
360360

361-
When a field can be `null`, combine `nullable: true` with `$ref`:
361+
Ajv does **not** allow `nullable: true` without `type`. Since `$ref` schemas don't have a `type` at the reference site, you cannot use `{ nullable: true, $ref: '...' }`. Instead, use `oneOf` with `{ type: 'null' }`:
362362

363363
```typescript
364-
// Nullable $ref
365-
report: { nullable: true, $ref: '#/components/schemas/IModerationReport' },
364+
// Nullable $ref — use oneOf with null
365+
report: {
366+
oneOf: [
367+
{ $ref: '#/components/schemas/IModerationReport' },
368+
{ type: 'null' },
369+
],
370+
},
366371

367-
// Nullable union
372+
// Nullable union — add null as another oneOf branch
368373
integration: {
369-
nullable: true,
370374
oneOf: [
371375
{ $ref: '#/components/schemas/IIncomingIntegration' },
372376
{ $ref: '#/components/schemas/IOutgoingIntegration' },
377+
{ type: 'null' },
373378
],
374379
},
375380
```
376381

382+
> **Note**: `nullable: true` works fine with `type`, e.g., `{ type: 'string', nullable: true }`. The restriction only applies when combining `nullable` with `$ref` (no `type` present).
383+
377384
### Handling intersection types with `allOf`
378385

379386
When a response intersects a type with additional properties (e.g., `VideoConferenceInstructions & { providerName: string }`), use `allOf`:
@@ -440,6 +447,44 @@ This happens because `oneOf` requires **exactly one** match, but the value is bo
440447

441448
**Long-term fix**: Revise the core-typings to narrow `ts` to `string` (which is what MongoDB aggregation pipelines and `JSON.stringify` actually return), or adjust the AJV/typia schema generation to handle `Date | string` unions correctly (e.g., using `anyOf` instead of `oneOf`, or collapsing `Date` into `string`).
442449

450+
### Known Pitfall: Mapped utility types (`Nullable<>`, `Partial<>`, etc.)
451+
452+
Typia does **not** resolve custom mapped types when generating JSON schemas. For example, the following pattern:
453+
454+
```typescript
455+
type Nullable<T, K extends keyof T> = { [P in K]: T[P] | null } & Omit<T, K>;
456+
457+
export interface IVideoConferenceUser
458+
extends Nullable<Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'>, 'avatarETag'> {
459+
ts: Date;
460+
}
461+
```
462+
463+
Produces `avatarETag: { type: 'string' }` **without** `nullable: true`, even though the resolved type is `string | null`. This causes response validation to reject `null` values when `coerceTypes` is `false`.
464+
465+
**Fix**: Declare nullable fields explicitly instead of relying on mapped types:
466+
467+
```typescript
468+
// BEFORE — typia loses the | null
469+
extends Nullable<Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'>, 'avatarETag'>
470+
471+
// AFTER — typia correctly generates nullable: true
472+
extends Pick<Required<IUser>, '_id' | 'username' | 'name'> {
473+
avatarETag: string | null;
474+
}
475+
```
476+
477+
After changing the type, rebuild core-typings (`yarn workspace @rocket.chat/core-typings run build`) to regenerate the schema.
478+
479+
**How to detect**: If a `$ref` schema rejects `null` values at runtime but the TypeScript type allows `null`, check if the type uses a mapped utility type. Inspect the generated schema:
480+
481+
```bash
482+
node -e "const { schemas } = require('./packages/core-typings/dist/Ajv.js'); \
483+
console.log(JSON.stringify(schemas.components.schemas.YourType, null, 2))"
484+
```
485+
486+
Look for fields that should have `nullable: true` but don't.
487+
443488
### Adding a new type to typia
444489

445490
If you need a `$ref` for a type that is not yet registered:
@@ -598,15 +643,59 @@ When migrating an endpoint, search for its tests and update:
598643

599644
## Tracking Migration Progress
600645

646+
Two scripts help track migration progress and identify type-safety issues.
647+
648+
### `scripts/list-unmigrated-api-endpoints.mjs`
649+
650+
Lists all endpoints that still use the legacy `addRoute` pattern and need migration.
651+
601652
```bash
602-
# Summary by file
653+
# Human-readable report grouped by file
603654
node scripts/list-unmigrated-api-endpoints.mjs
604655

605-
# Full list with line numbers (JSON)
656+
# Machine-readable JSON with file paths and line numbers
606657
node scripts/list-unmigrated-api-endpoints.mjs --json
607658
```
608659

609-
The script scans for `API.v1.addRoute` and `API.default.addRoute` calls in `apps/meteor/app/api/`.
660+
The script scans `apps/meteor/app/api/` for `API.v1.addRoute(...)` and `API.default.addRoute(...)` calls, extracting the route path, HTTP methods, file, and line number. Endpoints using this pattern lack compile-time type checking on request params and response shapes.
661+
662+
### `scripts/analyze-weak-types.mjs`
663+
664+
Analyzes `packages/rest-typings/` for "weak" types — generic types that provide little or no type safety in endpoint definitions.
665+
666+
```bash
667+
# Full report grouped by endpoint
668+
node scripts/analyze-weak-types.mjs
669+
670+
# JSON output for tooling/CI
671+
node scripts/analyze-weak-types.mjs --json
672+
673+
# Only check AJV schema definitions (type: 'object' with no properties, etc.)
674+
node scripts/analyze-weak-types.mjs --schema-only
675+
676+
# Only check TypeScript type definitions (any, Record<string, any>, etc.)
677+
node scripts/analyze-weak-types.mjs --ts-only
678+
```
679+
680+
Weak types detected:
681+
682+
| Pattern | Level | Risk |
683+
|---|---|---|
684+
| `any`, `unknown` | TypeScript | No type checking at all |
685+
| `object` (bare) | TypeScript | Accepts any non-primitive |
686+
| `Record<string, any>`, `Record<string, unknown>` | TypeScript | Untyped key-value bag |
687+
| `any[]`, `Array<any>` | TypeScript | Untyped array |
688+
| `Partial<IMessage>`, `Partial<IRoom>`, `Partial<IUser>` | TypeScript | Overly broad — accepts any subset of a large interface |
689+
| `{ type: 'object' }` without `properties` | AJV Schema | No runtime validation of object shape |
690+
| `{ type: 'array' }` without `items` | AJV Schema | No runtime validation of array elements |
691+
692+
### Recommended workflow
693+
694+
1. **Pick a domain** to migrate (e.g., channels, users, teams).
695+
2. **Run `list-unmigrated-api-endpoints.mjs`** to see which endpoints still use `addRoute`.
696+
3. **Run `analyze-weak-types.mjs`** to check if the existing `rest-typings` for that domain have weak types.
697+
4. **Migrate the endpoint**: move from `addRoute` to the typed pattern, and strengthen any weak types in `rest-typings` at the same time.
698+
5. **Re-run both scripts** to verify the endpoint no longer appears in either report.
610699

611700
## Reference Files
612701

@@ -625,3 +714,4 @@ The script scans for `API.v1.addRoute` and `API.default.addRoute` calls in `apps
625714
| Request validators (examples) | `packages/rest-typings/src/v1/moderation/` |
626715
| Router implementation | `packages/http-router/src/Router.ts` |
627716
| Unmigrated endpoints script | `scripts/list-unmigrated-api-endpoints.mjs` |
717+
| Weak types analysis script | `scripts/analyze-weak-types.mjs` |

0 commit comments

Comments
 (0)