Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/docs-backend-typedoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
72 changes: 71 additions & 1 deletion .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down Expand Up @@ -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<string, import('typedoc').Reflection>} 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
*/
Expand All @@ -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<SignInFields>`).
*/
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();

Expand Down
12 changes: 7 additions & 5 deletions packages/backend/src/api/endpoints/M2MTokenApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<{
Expand Down Expand Up @@ -61,6 +57,12 @@ type CreateM2MTokenParams = {
*/
minRemainingTtlSeconds?: number;
/**
* Format of the M2M token to create.
* <ul>
* <li>'opaque': Opaque token with mt_ prefix</li>
* <li>'jwt': JWT signed with instance keys</li>
* </ul>
*
* @default 'opaque'
*/
tokenFormat?: M2MTokenFormat;
Expand Down
96 changes: 68 additions & 28 deletions packages/backend/src/api/endpoints/UserApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -275,6 +308,10 @@ export class UserAPI extends AbstractAPI {
return { data, totalCount } as PaginatedResourceResponse<User[]>;
}

/**
* 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<User>({
Expand Down Expand Up @@ -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<number>({
method: 'GET',
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/typedoc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
2 changes: 2 additions & 0 deletions packages/shared/src/types/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T & {
/**
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Pagination params in request
*
* @interface
* @expandProperties
*/
export type ClerkPaginationRequest<T = object> = {
/**
Expand Down Expand Up @@ -33,6 +34,7 @@ export interface ClerkPaginatedResponse<T> {

/**
* @interface
* @expandProperties
*/
export type ClerkPaginationParams<T = object> = {
/**
Expand Down
5 changes: 5 additions & 0 deletions typedoc.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`), 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.
Expand Down
Loading