From 5147b18d8f5e54fafaa323c065082115f683cde2 Mon Sep 17 00:00:00 2001 From: Alexis Aguilar <98043211+alexisintech@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:34:57 -0700 Subject: [PATCH] docs(backend,shared): Generate typedoc output for backend docs Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/docs-backend-typedoc.md | 2 + .typedoc/custom-plugin.mjs | 72 +++++++++++++- .../backend/src/api/endpoints/M2MTokenApi.ts | 12 ++- packages/backend/src/api/endpoints/UserApi.ts | 96 +++++++++++++------ packages/backend/typedoc.json | 3 +- packages/shared/src/types/billing.ts | 2 + packages/shared/src/types/pagination.ts | 2 + typedoc.config.mjs | 5 + 8 files changed, 159 insertions(+), 35 deletions(-) create mode 100644 .changeset/docs-backend-typedoc.md diff --git a/.changeset/docs-backend-typedoc.md b/.changeset/docs-backend-typedoc.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/docs-backend-typedoc.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.typedoc/custom-plugin.mjs b/.typedoc/custom-plugin.mjs index 2dd1fa921a9..869ba73a069 100644 --- a/.typedoc/custom-plugin.mjs +++ b/.typedoc/custom-plugin.mjs @@ -1,5 +1,5 @@ // @ts-check - Enable TypeScript checks for safer MDX post-processing and link rewriting -import { Converter } from 'typedoc'; +import { Converter, DeclarationReflection, ReflectionKind, ReflectionType, RendererEvent } from 'typedoc'; import { MarkdownPageEvent } from 'typedoc-plugin-markdown'; /** @@ -464,6 +464,33 @@ export function applyCatchAllMdReplacements(contents) { .join('\n'); } +/** + * Walk a typedoc Type and return a flat list of property declarations to render as a merged table. Used by the `@expandProperties` flattener below to handle three shapes: + * - intersection types: walk each constituent + * - inline object literals (ReflectionType): take its declaration.children + * - named references (ReferenceType): take the target's children plus any properties contributed via type arguments, which captures the `Foo<{ ... }>` instantiation pattern where typedoc otherwise loses the generic parameter at the alias boundary. + * + * @param {import('typedoc').SomeType | undefined} type + * @param {Map} reflectionsByName lookup for cross-package refs whose `.reflection` is not linked + * @returns {import('typedoc').DeclarationReflection[]} + */ +function collectPropertiesFromType(type, reflectionsByName) { + if (!type) return []; + if (type.type === 'reflection') { + return type.declaration?.children ?? []; + } + if (type.type === 'intersection') { + return type.types.flatMap(t => collectPropertiesFromType(t, reflectionsByName)); + } + if (type.type === 'reference') { + const target = type.reflection ?? reflectionsByName.get(type.name); + const targetChildren = target?.children ?? []; + const argChildren = (type.typeArguments ?? []).flatMap(t => collectPropertiesFromType(t, reflectionsByName)); + return [...targetChildren, ...argChildren]; + } + return []; +} + /** * @param {import('typedoc-plugin-markdown').MarkdownApplication} app */ @@ -479,6 +506,49 @@ export function load(app) { } }); + /** + * Flatten the `Foo<{...}>` generic-instantiation pattern into a single merged properties table when `Foo` opts in via `@expandProperties`. typedoc-plugin-markdown would otherwise render an empty page for these aliases because the resolved type is a `ReferenceType` with no inline declaration — see `member.declaration.js` in the plugin, which only walks `IntersectionType` sub-types and has no branch for top-level `ReferenceType`. + * + * Runs at `RendererEvent.BEGIN` rather than `EVENT_RESOLVE_END` because the resolve hook fires per package, and cross-package references (e.g. `@clerk/backend` types referencing `ClerkPaginationRequest` from `@clerk/shared`) only link up after typedoc merges packages. + * + * The opt-in tag lives on the wrapper type so we never accidentally flatten unrelated generic aliases (e.g. `SignInErrors = Errors`). + */ + app.renderer.on(RendererEvent.BEGIN, event => { + const all = Object.values(event.project.reflections); + const reflectionsByName = new Map(); + for (const r of all) { + if (r.name && !reflectionsByName.has(r.name)) reflectionsByName.set(r.name, r); + } + const expandable = new Set(); + for (const r of all) { + if (r.comment?.modifierTags?.has('@expandProperties')) { + expandable.add(r); + r.comment.modifierTags.delete('@expandProperties'); + } + } + for (const reflection of all) { + if ( + reflection.kindOf?.(ReflectionKind.TypeAlias) && + reflection.type?.type === 'reference' && + Array.isArray(reflection.type.typeArguments) && + reflection.type.typeArguments.length > 0 + ) { + const target = reflection.type.reflection ?? reflectionsByName.get(reflection.type.name); + if (!target || !expandable.has(target)) continue; + const merged = collectPropertiesFromType(reflection.type, reflectionsByName); + if (merged.length > 0) { + // typedoc's package-level `sort: 'alphabetical'` is applied during conversion, before + // our synthetic merge runs. Sort here to match the alphabetical ordering used by + // every other table in the docs. + merged.sort((a, b) => a.name.localeCompare(b.name)); + const decl = new DeclarationReflection('__type', ReflectionKind.TypeLiteral, reflection); + decl.children = merged; + reflection.type = new ReflectionType(decl); + } + } + } + }); + app.renderer.on(MarkdownPageEvent.END, output => { const fileName = output.url.split('/').pop(); diff --git a/packages/backend/src/api/endpoints/M2MTokenApi.ts b/packages/backend/src/api/endpoints/M2MTokenApi.ts index 198179b7c3f..098549f55d9 100644 --- a/packages/backend/src/api/endpoints/M2MTokenApi.ts +++ b/packages/backend/src/api/endpoints/M2MTokenApi.ts @@ -13,11 +13,7 @@ import { AbstractAPI } from './AbstractApi'; const basePath = '/m2m_tokens'; -/** - * Format of the M2M token to create. - * - 'opaque': Opaque token with mt_ prefix - * - 'jwt': JWT signed with instance keys - */ +/** @inline */ export type M2MTokenFormat = 'opaque' | 'jwt'; type GetM2MTokenListParams = ClerkPaginationRequest<{ @@ -61,6 +57,12 @@ type CreateM2MTokenParams = { */ minRemainingTtlSeconds?: number; /** + * Format of the M2M token to create. + *
    + *
  • 'opaque': Opaque token with mt_ prefix
  • + *
  • 'jwt': JWT signed with instance keys
  • + *
+ * * @default 'opaque' */ tokenFormat?: M2MTokenFormat; diff --git a/packages/backend/src/api/endpoints/UserApi.ts b/packages/backend/src/api/endpoints/UserApi.ts index ef64db32dac..5936c967105 100644 --- a/packages/backend/src/api/endpoints/UserApi.ts +++ b/packages/backend/src/api/endpoints/UserApi.ts @@ -18,43 +18,72 @@ import type { WithSign } from './util-types'; const basePath = '/users'; -type UserCountParams = { +/** @generateWithEmptyComment */ +export type UserCountParams = { + /** Counts users with emails that match the given query, via case-insensitive partial match. For example, `emailAddress=hello` will match a user with the email `HELLO@example.com`. Accepts up to 100 email addresses. */ emailAddress?: string[]; + /** Counts users with phone numbers that match the given query, via case-insensitive partial match. For example, `phoneNumber=555` will match a user with the phone number `+1555xxxxxxx`. Accepts up to 100 phone numbers. */ phoneNumber?: string[]; + /** Counts users with usernames that match the given query, via case-insensitive partial match. For example, `username=CoolUser` will match a user with the username `SomeCoolUser`. Accepts up to 100 usernames. */ username?: string[]; + /** Counts users with Web3 wallet addresses that match the given query, via case-insensitive partial match. For example, `web3Wallet=0x1234567890` will match a user with the Web3 wallet address `0x1234567890`. Accepts up to 100 Web3 wallet addresses. */ web3Wallet?: string[]; + /** Counts users matching the given query across email addresses, phone numbers, usernames, Web3 wallet addresses, user IDs, first names, and last names. Partial matches supported. For example, `query=hello` will match a user with the email `HELLO@example.com`. */ query?: string; + /** Counts users with the specified user IDs. Accepts up to 100 user IDs. */ userId?: string[]; + /** Counts users with the specified external IDs. Accepts up to 100 external IDs. */ externalId?: string[]; }; -type UserListParams = ClerkPaginationRequest< - UserCountParams & { - orderBy?: WithSign< - | 'created_at' - | 'updated_at' - | 'email_address' - | 'web3wallet' - | 'first_name' - | 'last_name' - | 'phone_number' - | 'username' - | 'last_active_at' - | 'last_sign_in_at' - >; - /** - * @deprecated Use `lastActiveAtAfter` instead. This parameter will be removed in a future version. - */ - last_active_at_since?: number; - lastActiveAtBefore?: number; - lastActiveAtAfter?: number; - createdAtBefore?: number; - createdAtAfter?: number; - lastSignInAtAfter?: number; - lastSignInAtBefore?: number; - organizationId?: string[]; - } ->; +/** @generateWithEmptyComment */ +export type UserListParams = ClerkPaginationRequest<{ + /** Filters users with the specified email addresses. Accepts up to 100 email addresses. */ + emailAddress?: string[]; + /** Filters users with the specified phone numbers. Accepts up to 100 phone numbers. */ + phoneNumber?: string[]; + /** Filters users with the specified usernames. Accepts up to 100 usernames. */ + username?: string[]; + /** Filters users with the specified Web3 wallet addresses. Accepts up to 100 Web3 wallet addresses. */ + web3Wallet?: string[]; + /** Filters users matching the given query across email addresses, phone numbers, usernames, Web3 wallet addresses, user IDs, first names, and last names. Partial matches supported. */ + query?: string; + /** Filters users with the specified user IDs. Accepts up to 100 user IDs. */ + userId?: string[]; + /** Filters users with the specified external IDs. Accepts up to 100 external IDs. */ + externalId?: string[]; + /** Returns users in a particular order. Prefix a value with `+` to sort in ascending order, or `-` to sort in descending order. Defaults to `-created_at`.*/ + orderBy?: WithSign< + | 'created_at' + | 'updated_at' + | 'email_address' + | 'web3wallet' + | 'first_name' + | 'last_name' + | 'phone_number' + | 'username' + | 'last_active_at' + | 'last_sign_in_at' + >; + /** + * @deprecated Use `lastActiveAtAfter` instead. This parameter will be removed in a future version. + */ + last_active_at_since?: number; + /** Filters users who were last active before the given date (with millisecond precision). */ + lastActiveAtBefore?: number; + /** Filters users who were last active after the given date (with millisecond precision). */ + lastActiveAtAfter?: number; + /** Filters users who were created before the given date (with millisecond precision). */ + createdAtBefore?: number; + /** Filters users who were created after the given date (with millisecond precision). */ + createdAtAfter?: number; + /** Filters users who were last signed in after the given date (with millisecond precision). */ + lastSignInAtAfter?: number; + /** Filters users who were last signed in before the given date (with millisecond precision). */ + lastSignInAtBefore?: number; + /** Filters users who are members of the specified organizations. Accepts up to 100 organization IDs. */ + organizationId?: string[]; +}>; type UserMetadataParams = { publicMetadata?: UserPublicMetadata; @@ -259,6 +288,10 @@ type UserID = { }; export class UserAPI extends AbstractAPI { + /** + * Retrieves the list of users in your instance. + * @returns A [PaginatedResourceResponse](https://clerk.com/docs/reference/backend/types/paginated-resource-response) object with a `data` property than contains an array of [`User`](https://clerk.com/docs/reference/backend/types/backend-user) objects, and a `totalCount` property that indicates the total number of users in your instance. + */ public async getUserList(params: UserListParams = {}) { const { limit, offset, orderBy, ...userCountParams } = params; // TODO(dimkl): Temporary change to populate totalCount using a 2nd BAPI call to /users/count endpoint @@ -275,6 +308,10 @@ export class UserAPI extends AbstractAPI { return { data, totalCount } as PaginatedResourceResponse; } + /** + * Gets a [`User`](https://clerk.com/docs/reference/backend/types/backend-user) for the specified user ID. + * @param userId - The ID of the user to retrieve. + */ public async getUser(userId: string) { this.requireId(userId); return this.request({ @@ -395,6 +432,9 @@ export class UserAPI extends AbstractAPI { }); } + /** + * Gets the total number of users in your instance. + */ public async getCount(params: UserCountParams = {}) { return this.request({ method: 'GET', diff --git a/packages/backend/typedoc.json b/packages/backend/typedoc.json index 904b837abec..ae6a83fa433 100644 --- a/packages/backend/typedoc.json +++ b/packages/backend/typedoc.json @@ -8,6 +8,7 @@ "./src/tokens/types.ts", "./src/tokens/authObjects.ts", "./src/api/resources/index.ts", - "./src/api/resources/Deserializer.ts" + "./src/api/resources/Deserializer.ts", + "./src/api/endpoints/**/*.ts" ] } diff --git a/packages/shared/src/types/billing.ts b/packages/shared/src/types/billing.ts index 60390e1206e..421d09d1d37 100644 --- a/packages/shared/src/types/billing.ts +++ b/packages/shared/src/types/billing.ts @@ -7,6 +7,8 @@ import type { ForceNull, RemoveFunctions, Simplify } from './utils'; /** * Intersects `T` with an optional organization scope (`orgId`) for billing and related requests. + * + * @interface */ export type WithOptionalOrgType = T & { /** diff --git a/packages/shared/src/types/pagination.ts b/packages/shared/src/types/pagination.ts index b47d585d9ae..0920ca247f9 100644 --- a/packages/shared/src/types/pagination.ts +++ b/packages/shared/src/types/pagination.ts @@ -2,6 +2,7 @@ * Pagination params in request * * @interface + * @expandProperties */ export type ClerkPaginationRequest = { /** @@ -33,6 +34,7 @@ export interface ClerkPaginatedResponse { /** * @interface + * @expandProperties */ export type ClerkPaginationParams = { /** diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 77a8e44470d..0f3353cb398 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -122,6 +122,11 @@ const config = { '@standalonePage', /** Self-documenting placeholder for declarations intentionally left without a description. */ '@generateWithEmptyComment', + /** + * On a generic wrapper type (e.g. `ClerkPaginationRequest`), opts every alias of the form `Foo<{...}>` into a single merged properties table that includes the wrapper's own properties. Without this, typedoc-plugin-markdown renders such aliases as empty pages because the resolved type is a ReferenceType with no inline declaration. + * Handled by `.typedoc/custom-plugin.mjs`. + */ + '@expandProperties', ], /** * Keep `@inline` / `@inlineType` / `@standalonePage` in the model so the custom router and theme can read them.