mirror of
https://github.com/tale/headplane.git
synced 2026-08-19 09:26:18 +00:00
chore: format everything with oxfmt
This commit is contained in:
@@ -1,23 +1,20 @@
|
||||
import type { Key } from '~/types';
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
import type { Key } from "~/types";
|
||||
|
||||
import { defineApiEndpoints } from "../factory";
|
||||
|
||||
export interface ApiKeyEndpoints {
|
||||
/**
|
||||
* Retrieves all API keys from the Headscale instance.
|
||||
*
|
||||
* @returns An array of `Key` objects representing the API keys.
|
||||
*/
|
||||
getApiKeys(): Promise<Key[]>;
|
||||
/**
|
||||
* Retrieves all API keys from the Headscale instance.
|
||||
*
|
||||
* @returns An array of `Key` objects representing the API keys.
|
||||
*/
|
||||
getApiKeys(): Promise<Key[]>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<ApiKeyEndpoints>((client, apiKey) => ({
|
||||
getApiKeys: async () => {
|
||||
const { apiKeys } = await client.apiFetch<{ apiKeys: Key[] }>(
|
||||
'GET',
|
||||
'v1/apikey',
|
||||
apiKey,
|
||||
);
|
||||
getApiKeys: async () => {
|
||||
const { apiKeys } = await client.apiFetch<{ apiKeys: Key[] }>("GET", "v1/apikey", apiKey);
|
||||
|
||||
return apiKeys;
|
||||
},
|
||||
return apiKeys;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
import {
|
||||
composeEndpoints,
|
||||
defineApiEndpoints,
|
||||
type ExtractApiEndpoints,
|
||||
type UnionToIntersection,
|
||||
} from '../factory';
|
||||
import type { HeadscaleApiInterface } from '../index';
|
||||
import apiKeyEndpoints from './api-keys';
|
||||
import nodeEndpoints from './nodes';
|
||||
import policyEndpoints from './policy';
|
||||
import preAuthKeyEndpoints from './pre-auth-keys';
|
||||
import userEndpoints from './users';
|
||||
composeEndpoints,
|
||||
defineApiEndpoints,
|
||||
type ExtractApiEndpoints,
|
||||
type UnionToIntersection,
|
||||
} from "../factory";
|
||||
import type { HeadscaleApiInterface } from "../index";
|
||||
import apiKeyEndpoints from "./api-keys";
|
||||
import nodeEndpoints from "./nodes";
|
||||
import policyEndpoints from "./policy";
|
||||
import preAuthKeyEndpoints from "./pre-auth-keys";
|
||||
import userEndpoints from "./users";
|
||||
|
||||
interface HealthcheckEndpoint {
|
||||
/**
|
||||
* Checks if the Headscale instance is healthy.
|
||||
*
|
||||
* @returns A boolean indicating if the instance is healthy.
|
||||
*/
|
||||
isHealthy(): Promise<boolean>;
|
||||
/**
|
||||
* Checks if the Headscale instance is healthy.
|
||||
*
|
||||
* @returns A boolean indicating if the instance is healthy.
|
||||
*/
|
||||
isHealthy(): Promise<boolean>;
|
||||
}
|
||||
|
||||
const healthcheckEndpoint = defineApiEndpoints<HealthcheckEndpoint>(
|
||||
(client, apiKey) => ({
|
||||
isHealthy: async () => {
|
||||
try {
|
||||
const res = await client.rawFetch('/health', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
// This doesn't really matter
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
const healthcheckEndpoint = defineApiEndpoints<HealthcheckEndpoint>((client, apiKey) => ({
|
||||
isHealthy: async () => {
|
||||
try {
|
||||
const res = await client.rawFetch("/health", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
// This doesn't really matter
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
});
|
||||
|
||||
return res.statusCode === 200;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
return res.statusCode === 200;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* A constant list of all endpoint groups.
|
||||
* Add new endpoint groups here.
|
||||
*/
|
||||
export const endpointSets = [
|
||||
apiKeyEndpoints,
|
||||
healthcheckEndpoint,
|
||||
nodeEndpoints,
|
||||
policyEndpoints,
|
||||
preAuthKeyEndpoints,
|
||||
userEndpoints,
|
||||
apiKeyEndpoints,
|
||||
healthcheckEndpoint,
|
||||
nodeEndpoints,
|
||||
policyEndpoints,
|
||||
preAuthKeyEndpoints,
|
||||
userEndpoints,
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -63,7 +61,7 @@ export const endpointSets = [
|
||||
* passing in different internal implementations based on the OpenAPI spec.
|
||||
*/
|
||||
export type RuntimeApiClient = UnionToIntersection<
|
||||
ExtractApiEndpoints<(typeof endpointSets)[number]>
|
||||
ExtractApiEndpoints<(typeof endpointSets)[number]>
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -73,7 +71,5 @@ export type RuntimeApiClient = UnionToIntersection<
|
||||
* @param apiKey - The API key for authentication.
|
||||
* @returns A fully composed runtime API client.
|
||||
*/
|
||||
export default (
|
||||
client: HeadscaleApiInterface['clientHelpers'],
|
||||
apiKey: string,
|
||||
) => composeEndpoints(endpointSets, client, apiKey);
|
||||
export default (client: HeadscaleApiInterface["clientHelpers"], apiKey: string) =>
|
||||
composeEndpoints(endpointSets, client, apiKey);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Machine } from "~/types";
|
||||
|
||||
import type { HeadscaleApiInterface } from "..";
|
||||
|
||||
import { defineApiEndpoints } from "../factory";
|
||||
|
||||
interface RawMachine extends Omit<Machine, "tags"> {
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
import { defineApiEndpoints } from "../factory";
|
||||
|
||||
export interface PolicyEndpoints {
|
||||
/**
|
||||
* Retrieves the current ACL policy from the Headscale instance.
|
||||
*
|
||||
* @returns The ACL policy as a string and the date it was last updated.
|
||||
*/
|
||||
getPolicy(): Promise<{ policy: string; updatedAt: Date | null }>;
|
||||
/**
|
||||
* Retrieves the current ACL policy from the Headscale instance.
|
||||
*
|
||||
* @returns The ACL policy as a string and the date it was last updated.
|
||||
*/
|
||||
getPolicy(): Promise<{ policy: string; updatedAt: Date | null }>;
|
||||
|
||||
/**
|
||||
* Sets the ACL policy for the Headscale instance.
|
||||
*
|
||||
* @param policy The ACL policy as a string.
|
||||
* @returns The updated ACL policy as a string and the date it was last updated.
|
||||
*/
|
||||
setPolicy(policy: string): Promise<{ policy: string; updatedAt: Date }>;
|
||||
/**
|
||||
* Sets the ACL policy for the Headscale instance.
|
||||
*
|
||||
* @param policy The ACL policy as a string.
|
||||
* @returns The updated ACL policy as a string and the date it was last updated.
|
||||
*/
|
||||
setPolicy(policy: string): Promise<{ policy: string; updatedAt: Date }>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<PolicyEndpoints>((client, apiKey) => ({
|
||||
getPolicy: async () => {
|
||||
const { policy, updatedAt } = await client.apiFetch<{
|
||||
policy: string;
|
||||
updatedAt: string;
|
||||
}>('GET', 'v1/policy', apiKey);
|
||||
getPolicy: async () => {
|
||||
const { policy, updatedAt } = await client.apiFetch<{
|
||||
policy: string;
|
||||
updatedAt: string;
|
||||
}>("GET", "v1/policy", apiKey);
|
||||
|
||||
return {
|
||||
policy,
|
||||
updatedAt: updatedAt !== null ? new Date(updatedAt) : null,
|
||||
};
|
||||
},
|
||||
return {
|
||||
policy,
|
||||
updatedAt: updatedAt !== null ? new Date(updatedAt) : null,
|
||||
};
|
||||
},
|
||||
|
||||
setPolicy: async (policy) => {
|
||||
const { policy: newPolicy, updatedAt } = await client.apiFetch<{
|
||||
policy: string;
|
||||
updatedAt: string;
|
||||
}>('PUT', 'v1/policy', apiKey, { policy });
|
||||
setPolicy: async (policy) => {
|
||||
const { policy: newPolicy, updatedAt } = await client.apiFetch<{
|
||||
policy: string;
|
||||
updatedAt: string;
|
||||
}>("PUT", "v1/policy", apiKey, { policy });
|
||||
|
||||
return { policy: newPolicy, updatedAt: new Date(updatedAt) };
|
||||
},
|
||||
return { policy: newPolicy, updatedAt: new Date(updatedAt) };
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,88 +1,83 @@
|
||||
import type { User } from '~/types';
|
||||
import { defineApiEndpoints } from '../factory';
|
||||
import type { User } from "~/types";
|
||||
|
||||
import { defineApiEndpoints } from "../factory";
|
||||
|
||||
export interface UserEndpoints {
|
||||
/**
|
||||
* Retrieves users from the Headscale instance, optionally filtering by ID, name, or email.
|
||||
*
|
||||
* @param id Optional ID of the user to retrieve.
|
||||
* @param name Optional name of the user to retrieve.
|
||||
* @param email Optional email of the user to retrieve.
|
||||
* @returns An array of `User` objects representing the users.
|
||||
*/
|
||||
getUsers(id?: string, name?: string, email?: string): Promise<User[]>;
|
||||
/**
|
||||
* Retrieves users from the Headscale instance, optionally filtering by ID, name, or email.
|
||||
*
|
||||
* @param id Optional ID of the user to retrieve.
|
||||
* @param name Optional name of the user to retrieve.
|
||||
* @param email Optional email of the user to retrieve.
|
||||
* @returns An array of `User` objects representing the users.
|
||||
*/
|
||||
getUsers(id?: string, name?: string, email?: string): Promise<User[]>;
|
||||
|
||||
/**
|
||||
* Creates a new user in the Headscale instance.
|
||||
*
|
||||
* @param username The username of the new user.
|
||||
* @param email Optional email of the new user.
|
||||
* @param displayName Optional display name of the new user.
|
||||
* @param pictureUrl Optional picture URL of the new user.
|
||||
* @returns A `User` object representing the newly created user.
|
||||
*/
|
||||
createUser(
|
||||
username: string,
|
||||
email?: string,
|
||||
displayName?: string,
|
||||
pictureUrl?: string,
|
||||
): Promise<User>;
|
||||
/**
|
||||
* Creates a new user in the Headscale instance.
|
||||
*
|
||||
* @param username The username of the new user.
|
||||
* @param email Optional email of the new user.
|
||||
* @param displayName Optional display name of the new user.
|
||||
* @param pictureUrl Optional picture URL of the new user.
|
||||
* @returns A `User` object representing the newly created user.
|
||||
*/
|
||||
createUser(
|
||||
username: string,
|
||||
email?: string,
|
||||
displayName?: string,
|
||||
pictureUrl?: string,
|
||||
): Promise<User>;
|
||||
|
||||
/**
|
||||
* Deletes a specific user by its ID.
|
||||
*
|
||||
* @param id The ID of the user to delete.
|
||||
*/
|
||||
deleteUser(id: string): Promise<void>;
|
||||
/**
|
||||
* Deletes a specific user by its ID.
|
||||
*
|
||||
* @param id The ID of the user to delete.
|
||||
*/
|
||||
deleteUser(id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Renames a specific user by its ID.
|
||||
*
|
||||
* @param id The ID of the user to rename.
|
||||
* @param newName The new name for the user.
|
||||
*/
|
||||
renameUser(id: string, newName: string): Promise<void>;
|
||||
/**
|
||||
* Renames a specific user by its ID.
|
||||
*
|
||||
* @param id The ID of the user to rename.
|
||||
* @param newName The new name for the user.
|
||||
*/
|
||||
renameUser(id: string, newName: string): Promise<void>;
|
||||
}
|
||||
|
||||
export default defineApiEndpoints<UserEndpoints>((client, apiKey) => ({
|
||||
getUsers: async (id, name, email) => {
|
||||
const moreThanOneFilter =
|
||||
[id, name, email].filter((v) => v !== undefined).length > 1;
|
||||
getUsers: async (id, name, email) => {
|
||||
const moreThanOneFilter = [id, name, email].filter((v) => v !== undefined).length > 1;
|
||||
|
||||
if (moreThanOneFilter) {
|
||||
throw new Error('Only one of id, name, or email filters can be provided');
|
||||
}
|
||||
if (moreThanOneFilter) {
|
||||
throw new Error("Only one of id, name, or email filters can be provided");
|
||||
}
|
||||
|
||||
const { users } = await client.apiFetch<{ users: User[] }>(
|
||||
'GET',
|
||||
'v1/user',
|
||||
apiKey,
|
||||
{ id, name, email },
|
||||
);
|
||||
const { users } = await client.apiFetch<{ users: User[] }>("GET", "v1/user", apiKey, {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
});
|
||||
|
||||
return users;
|
||||
},
|
||||
return users;
|
||||
},
|
||||
|
||||
createUser: async (username, email, displayName, pictureUrl) => {
|
||||
const { user } = await client.apiFetch<{ user: User }>(
|
||||
'POST',
|
||||
'v1/user',
|
||||
apiKey,
|
||||
{ name: username, email, displayName, pictureUrl },
|
||||
);
|
||||
createUser: async (username, email, displayName, pictureUrl) => {
|
||||
const { user } = await client.apiFetch<{ user: User }>("POST", "v1/user", apiKey, {
|
||||
name: username,
|
||||
email,
|
||||
displayName,
|
||||
pictureUrl,
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
return user;
|
||||
},
|
||||
|
||||
deleteUser: async (id) => {
|
||||
await client.apiFetch<void>('DELETE', `v1/user/${id}`, apiKey);
|
||||
},
|
||||
deleteUser: async (id) => {
|
||||
await client.apiFetch<void>("DELETE", `v1/user/${id}`, apiKey);
|
||||
},
|
||||
|
||||
renameUser: async (oldId, newName) => {
|
||||
await client.apiFetch<void>(
|
||||
'POST',
|
||||
`v1/user/${oldId}/rename/${newName}`,
|
||||
apiKey,
|
||||
);
|
||||
},
|
||||
renameUser: async (oldId, newName) => {
|
||||
await client.apiFetch<void>("POST", `v1/user/${oldId}/rename/${newName}`, apiKey);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { HeadscaleConnectionError } from './error';
|
||||
import type { HeadscaleConnectionError } from "./error";
|
||||
|
||||
/**
|
||||
* Represents an error returned by the Headscale API.
|
||||
*/
|
||||
export interface HeadscaleAPIError {
|
||||
requestUrl: `${string} ${string}`;
|
||||
statusCode: number;
|
||||
rawData: string;
|
||||
data: Record<string, unknown> | null;
|
||||
requestUrl: `${string} ${string}`;
|
||||
statusCode: number;
|
||||
rawData: string;
|
||||
data: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,14 +16,14 @@ export interface HeadscaleAPIError {
|
||||
* @returns True if the error is a HeadscaleAPIError, false otherwise.
|
||||
*/
|
||||
export function isApiError(error: unknown): error is HeadscaleAPIError {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === 'object' &&
|
||||
'requestUrl' in error &&
|
||||
'statusCode' in error &&
|
||||
'rawData' in error &&
|
||||
'data' in error
|
||||
);
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === "object" &&
|
||||
"requestUrl" in error &&
|
||||
"statusCode" in error &&
|
||||
"rawData" in error &&
|
||||
"data" in error
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,17 +31,15 @@ export function isApiError(error: unknown): error is HeadscaleAPIError {
|
||||
* @param error - The error to check.
|
||||
* @returns True if the error is a HeadscaleConnectionError, false otherwise.
|
||||
*/
|
||||
export function isConnectionError(
|
||||
error: unknown,
|
||||
): error is HeadscaleConnectionError {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === 'object' &&
|
||||
'requestUrl' in error &&
|
||||
'errorCode' in error &&
|
||||
'errorMessage' in error &&
|
||||
'extraData' in error
|
||||
);
|
||||
export function isConnectionError(error: unknown): error is HeadscaleConnectionError {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === "object" &&
|
||||
"requestUrl" in error &&
|
||||
"errorCode" in error &&
|
||||
"errorMessage" in error &&
|
||||
"extraData" in error
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,15 +50,15 @@ export function isConnectionError(
|
||||
* @returns True if the error is a DataUnauthorizedError, false otherwise.
|
||||
*/
|
||||
export function isDataUnauthorizedError(error: unknown): boolean {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === 'object' &&
|
||||
'data' in error &&
|
||||
typeof error.data === 'object' &&
|
||||
error.data != null &&
|
||||
'statusCode' in error.data &&
|
||||
error.data.statusCode === 401
|
||||
);
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === "object" &&
|
||||
"data" in error &&
|
||||
typeof error.data === "object" &&
|
||||
error.data != null &&
|
||||
"statusCode" in error.data &&
|
||||
error.data.statusCode === 401
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,13 +70,6 @@ export function isDataUnauthorizedError(error: unknown): boolean {
|
||||
* @returns True if the error is a DataWithResponseInit containing a
|
||||
* HeadscaleAPIError, false otherwise.
|
||||
*/
|
||||
export function isDataWithApiError(
|
||||
error: unknown,
|
||||
): error is { data: HeadscaleAPIError } {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === 'object' &&
|
||||
'data' in error &&
|
||||
isApiError(error.data)
|
||||
);
|
||||
export function isDataWithApiError(error: unknown): error is { data: HeadscaleAPIError } {
|
||||
return error != null && typeof error === "object" && "data" in error && isApiError(error.data);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { errors } from 'undici';
|
||||
import { errors } from "undici";
|
||||
|
||||
/**
|
||||
* Helper function that determines if an error is a Node.js exception
|
||||
@@ -6,22 +6,17 @@ import { errors } from 'undici';
|
||||
* @returns True if the error is a Node.js exception, false otherwise
|
||||
*/
|
||||
function isNodeNetworkError(error: unknown): error is NodeJS.ErrnoException {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === 'object' &&
|
||||
'code' in error &&
|
||||
'errno' in error
|
||||
);
|
||||
return error != null && typeof error === "object" && "code" in error && "errno" in error;
|
||||
}
|
||||
|
||||
/**
|
||||
* A friendly error representation for Headscale connection issues.
|
||||
*/
|
||||
export interface HeadscaleConnectionError {
|
||||
requestUrl: string;
|
||||
errorCode: string;
|
||||
errorMessage: string;
|
||||
extraData: Record<string, unknown> | null;
|
||||
requestUrl: string;
|
||||
errorCode: string;
|
||||
errorMessage: string;
|
||||
extraData: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,40 +28,40 @@ export interface HeadscaleConnectionError {
|
||||
* @returns A friendly HeadscaleAPIError.
|
||||
*/
|
||||
export function undiciToFriendlyError(
|
||||
error: unknown,
|
||||
requestUrl: string,
|
||||
error: unknown,
|
||||
requestUrl: string,
|
||||
): HeadscaleConnectionError {
|
||||
// MARK: Do we need to go deeper into causes here?
|
||||
if (error instanceof AggregateError) {
|
||||
error = error.errors[0];
|
||||
}
|
||||
// MARK: Do we need to go deeper into causes here?
|
||||
if (error instanceof AggregateError) {
|
||||
error = error.errors[0];
|
||||
}
|
||||
|
||||
if (error instanceof errors.UndiciError) {
|
||||
return {
|
||||
requestUrl,
|
||||
errorCode: error.code,
|
||||
errorMessage: error.message,
|
||||
extraData: null,
|
||||
};
|
||||
}
|
||||
if (error instanceof errors.UndiciError) {
|
||||
return {
|
||||
requestUrl,
|
||||
errorCode: error.code,
|
||||
errorMessage: error.message,
|
||||
extraData: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (isNodeNetworkError(error)) {
|
||||
return {
|
||||
requestUrl,
|
||||
errorCode: error.code ?? 'UNKNOWN_NODE_NETWORK_ERROR',
|
||||
errorMessage: error.message,
|
||||
extraData: {
|
||||
syscall: error.syscall,
|
||||
path: error.path,
|
||||
errno: error.errno,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (isNodeNetworkError(error)) {
|
||||
return {
|
||||
requestUrl,
|
||||
errorCode: error.code ?? "UNKNOWN_NODE_NETWORK_ERROR",
|
||||
errorMessage: error.message,
|
||||
extraData: {
|
||||
syscall: error.syscall,
|
||||
path: error.path,
|
||||
errno: error.errno,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
requestUrl,
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
errorMessage: 'An unknown error occured',
|
||||
extraData: null,
|
||||
};
|
||||
return {
|
||||
requestUrl,
|
||||
errorCode: "UNKNOWN_ERROR",
|
||||
errorMessage: "An unknown error occured",
|
||||
extraData: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HeadscaleApiInterface } from '../api';
|
||||
import type { HeadscaleApiInterface } from "../api";
|
||||
|
||||
/**
|
||||
* Creates a strongly-typed group factory for a given endpoint interface.
|
||||
@@ -8,38 +8,31 @@ import type { HeadscaleApiInterface } from '../api';
|
||||
*/
|
||||
|
||||
export interface EndpointFactory<T extends object> {
|
||||
__type?: T;
|
||||
(client: HeadscaleApiInterface['clientHelpers'], apiKey: string): T;
|
||||
__type?: T;
|
||||
(client: HeadscaleApiInterface["clientHelpers"], apiKey: string): T;
|
||||
}
|
||||
|
||||
export function defineApiEndpoints<T extends object>(
|
||||
factories: (
|
||||
client: HeadscaleApiInterface['clientHelpers'],
|
||||
apiKey: string,
|
||||
) => T,
|
||||
factories: (client: HeadscaleApiInterface["clientHelpers"], apiKey: string) => T,
|
||||
): EndpointFactory<T> {
|
||||
return factories;
|
||||
return factories;
|
||||
}
|
||||
|
||||
export type ExtractApiEndpoints<F> = F extends EndpointFactory<infer T>
|
||||
? T
|
||||
: never;
|
||||
export type UnionToIntersection<U> = (
|
||||
U extends any
|
||||
? (k: U) => void
|
||||
: never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never;
|
||||
export type ExtractApiEndpoints<F> = F extends EndpointFactory<infer T> ? T : never;
|
||||
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Compose multiple endpoint sets into a single typed runtime client
|
||||
*/
|
||||
export function composeEndpoints<T extends readonly EndpointFactory<any>[]>(
|
||||
factories: T,
|
||||
clientHelpers: any,
|
||||
apiKey: string,
|
||||
factories: T,
|
||||
clientHelpers: any,
|
||||
apiKey: string,
|
||||
): UnionToIntersection<ExtractApiEndpoints<T[number]>> {
|
||||
const instances = factories.map((f) => f(clientHelpers, apiKey));
|
||||
return Object.assign({}, ...instances) as any;
|
||||
const instances = factories.map((f) => f(clientHelpers, apiKey));
|
||||
return Object.assign({}, ...instances) as any;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { dereference } from '@readme/openapi-parser';
|
||||
import { OpenAPIV2 } from 'openapi-types';
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { dereference } from "@readme/openapi-parser";
|
||||
import { OpenAPIV2 } from "openapi-types";
|
||||
|
||||
/**
|
||||
* A map of operation IDs to their hashes.
|
||||
*/
|
||||
export interface DocumentHash {
|
||||
[operationId: string]: string;
|
||||
[operationId: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,36 +18,34 @@ export interface DocumentHash {
|
||||
* @param doc The OpenAPI v2 document to hash.
|
||||
* @returns A map of operation IDs to their hashes.
|
||||
*/
|
||||
export async function hashOpenApiDocument(
|
||||
doc: OpenAPIV2.Document,
|
||||
): Promise<DocumentHash> {
|
||||
const spec = await dereference(doc);
|
||||
const hashes: DocumentHash = {};
|
||||
const seen = new Set<string>();
|
||||
export async function hashOpenApiDocument(doc: OpenAPIV2.Document): Promise<DocumentHash> {
|
||||
const spec = await dereference(doc);
|
||||
const hashes: DocumentHash = {};
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const [path, item] of Object.entries(spec.paths)) {
|
||||
for (const [method, operation] of Object.entries(item)) {
|
||||
if (typeof operation !== 'object') {
|
||||
continue;
|
||||
}
|
||||
for (const [path, item] of Object.entries(spec.paths)) {
|
||||
for (const [method, operation] of Object.entries(item)) {
|
||||
if (typeof operation !== "object") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const { parameters, responses } = operation as OpenAPIV2.OperationObject;
|
||||
const raw = JSON.stringify(
|
||||
{
|
||||
path,
|
||||
method: method.toUpperCase(),
|
||||
parameters,
|
||||
responses,
|
||||
},
|
||||
Object.keys({ path, method, parameters, responses }).sort(),
|
||||
);
|
||||
const { parameters, responses } = operation as OpenAPIV2.OperationObject;
|
||||
const raw = JSON.stringify(
|
||||
{
|
||||
path,
|
||||
method: method.toUpperCase(),
|
||||
parameters,
|
||||
responses,
|
||||
},
|
||||
Object.keys({ path, method, parameters, responses }).sort(),
|
||||
);
|
||||
|
||||
const hash = createHash('md5').update(raw).digest('hex').slice(0, 16);
|
||||
const final = seen.has(hash) ? `${hash}_${seen.size}` : hash;
|
||||
seen.add(final);
|
||||
hashes[`${method.toUpperCase()} ${path}`] = final;
|
||||
}
|
||||
}
|
||||
const hash = createHash("md5").update(raw).digest("hex").slice(0, 16);
|
||||
const final = seen.has(hash) ? `${hash}_${seen.size}` : hash;
|
||||
seen.add(final);
|
||||
hashes[`${method.toUpperCase()} ${path}`] = final;
|
||||
}
|
||||
}
|
||||
|
||||
return hashes;
|
||||
return hashes;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import canonicals from '~/openapi-canonical-families.json';
|
||||
import hashes from '~/openapi-operation-hashes.json';
|
||||
import log from '~/utils/log';
|
||||
import canonicals from "~/openapi-canonical-families.json";
|
||||
import hashes from "~/openapi-operation-hashes.json";
|
||||
import log from "~/utils/log";
|
||||
|
||||
/**
|
||||
* The known API versions based on operation hashes.
|
||||
@@ -15,80 +15,63 @@ const VERSIONS = Object.keys(hashes) as Version[];
|
||||
* @param observed - A mapping of operation identifiers to their hashes.
|
||||
* @returns The detected API version.
|
||||
*/
|
||||
export function detectApiVersion(
|
||||
observed: Record<string, string> | null,
|
||||
): Version {
|
||||
if (!observed) {
|
||||
const latest = VERSIONS.at(-1)!;
|
||||
log.warn(
|
||||
'api',
|
||||
'No operation hashes observed, defaulting to version %s',
|
||||
latest,
|
||||
);
|
||||
return latest;
|
||||
}
|
||||
export function detectApiVersion(observed: Record<string, string> | null): Version {
|
||||
if (!observed) {
|
||||
const latest = VERSIONS.at(-1)!;
|
||||
log.warn("api", "No operation hashes observed, defaulting to version %s", latest);
|
||||
return latest;
|
||||
}
|
||||
|
||||
let bestVersion: Version | null = null;
|
||||
let bestScore = -1;
|
||||
let bestVersion: Version | null = null;
|
||||
let bestScore = -1;
|
||||
|
||||
for (const [version, known] of Object.entries(hashes) as [
|
||||
Version,
|
||||
Record<string, string>,
|
||||
][]) {
|
||||
let score = 0;
|
||||
for (const [op, hash] of Object.entries(observed)) {
|
||||
if (known[op] === hash) score++;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestVersion = version;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
for (const [version, known] of Object.entries(hashes) as [Version, Record<string, string>][]) {
|
||||
let score = 0;
|
||||
for (const [op, hash] of Object.entries(observed)) {
|
||||
if (known[op] === hash) score++;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestVersion = version;
|
||||
bestScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bestVersion || bestScore === 0) {
|
||||
const latest = VERSIONS.at(-1)!;
|
||||
log.warn(
|
||||
'api',
|
||||
'Could not determine API version, defaulting to %s',
|
||||
latest,
|
||||
);
|
||||
return latest;
|
||||
}
|
||||
if (!bestVersion || bestScore === 0) {
|
||||
const latest = VERSIONS.at(-1)!;
|
||||
log.warn("api", "Could not determine API version, defaulting to %s", latest);
|
||||
return latest;
|
||||
}
|
||||
|
||||
if (bestScore < Object.keys(observed).length) {
|
||||
log.warn(
|
||||
'api',
|
||||
'Partial version match: %d/%d endpoints for version %s',
|
||||
bestScore,
|
||||
Object.keys(observed).length,
|
||||
bestVersion,
|
||||
);
|
||||
}
|
||||
if (bestScore < Object.keys(observed).length) {
|
||||
log.warn(
|
||||
"api",
|
||||
"Partial version match: %d/%d endpoints for version %s",
|
||||
bestScore,
|
||||
Object.keys(observed).length,
|
||||
bestVersion,
|
||||
);
|
||||
}
|
||||
|
||||
const canonical = Object.entries(canonicals).find(([_, family]) =>
|
||||
family.includes(bestVersion),
|
||||
)?.[0] as Version | undefined;
|
||||
const canonical = Object.entries(canonicals).find(([_, family]) =>
|
||||
family.includes(bestVersion),
|
||||
)?.[0] as Version | undefined;
|
||||
|
||||
if (!canonical) {
|
||||
log.warn(
|
||||
'api',
|
||||
'Could not canonicalize detected version %s, using as-is',
|
||||
bestVersion,
|
||||
);
|
||||
if (!canonical) {
|
||||
log.warn("api", "Could not canonicalize detected version %s, using as-is", bestVersion);
|
||||
|
||||
return bestVersion;
|
||||
}
|
||||
return bestVersion;
|
||||
}
|
||||
|
||||
if (canonical !== bestVersion) {
|
||||
log.info(
|
||||
'api',
|
||||
'Canonicalizing detected version %s → %s (same schema)',
|
||||
bestVersion,
|
||||
canonical,
|
||||
);
|
||||
}
|
||||
if (canonical !== bestVersion) {
|
||||
log.info(
|
||||
"api",
|
||||
"Canonicalizing detected version %s → %s (same schema)",
|
||||
bestVersion,
|
||||
canonical,
|
||||
);
|
||||
}
|
||||
|
||||
return canonical;
|
||||
return canonical;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,5 +82,5 @@ export function detectApiVersion(
|
||||
* @returns True if current is at least baseline, false otherwise.
|
||||
*/
|
||||
export function isAtLeast(current: Version, baseline: Version) {
|
||||
return VERSIONS.indexOf(current) >= VERSIONS.indexOf(baseline);
|
||||
return VERSIONS.indexOf(current) >= VERSIONS.indexOf(baseline);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user