Skip to content
Merged
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: 1 addition & 1 deletion typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"src/generated"
],
"scripts": {
"generate": "tsx scripts/strip-account-id.ts ../openapi.json src/generated/openapi-stripped.json && openapi-typescript src/generated/openapi-stripped.json -o src/generated/schema.d.ts && tsx scripts/patch-flexible-ids.ts && tsx scripts/extract-metadata.ts src/generated/openapi-stripped.json > src/generated/metadata.json && tsx scripts/generate-path-mapping.ts",
"generate": "tsx scripts/strip-account-id.ts ../openapi.json src/generated/openapi-stripped.json && openapi-typescript src/generated/openapi-stripped.json -o src/generated/schema.d.ts && tsx scripts/patch-flexible-ids.ts && tsx scripts/extract-metadata.ts src/generated/openapi-stripped.json > src/generated/metadata.ts && tsx scripts/generate-path-mapping.ts",
"build": "tsc",
"postbuild": "node -e \"fs=require('fs');fs.cpSync('src/generated','dist/generated',{recursive:true});\"",
"prepublishOnly": "npm run build",
Expand Down
57 changes: 46 additions & 11 deletions typescript/scripts/extract-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env node
/**
* Extracts x-basecamp-* extensions from OpenAPI spec into a runtime-accessible metadata file.
* This allows the TypeScript SDK to read operation metadata at runtime for retry, pagination, etc.
* Extracts x-basecamp-* extensions from OpenAPI spec into a runtime-accessible metadata module.
* The generated ESM module keeps operation metadata in the static import graph for bundlers and serverless file tracing.
*
* Usage: npx tsx extract-metadata.ts ../openapi.json > src/generated/metadata.json
* Usage: npx tsx extract-metadata.ts ../openapi.json > src/generated/metadata.ts
*/

import * as fs from "fs";
Expand All @@ -21,6 +21,7 @@ interface PaginationConfig {
pageParam?: string;
totalCountHeader?: string;
maxPageSize?: number;
key?: string;
}

interface IdempotentConfig {
Expand All @@ -29,17 +30,10 @@ interface IdempotentConfig {
natural?: boolean;
}

interface SensitiveField {
field: string;
category: string;
redact: boolean;
}

interface OperationMetadata {
retry?: RetryConfig;
pagination?: PaginationConfig;
idempotent?: IdempotentConfig;
sensitive?: SensitiveField[];
}

interface MetadataOutput {
Expand Down Expand Up @@ -108,4 +102,45 @@ if (!fs.existsSync(resolvedPath)) {
}

const metadata = extractMetadata(resolvedPath);
console.log(JSON.stringify(metadata, null, 2));

const json = JSON.stringify(metadata, null, 2);
console.log(`// Generated from OpenAPI x-basecamp-* extensions. Do not edit by hand.

export interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
backoff: "exponential" | "linear" | "constant";
retryOn: number[];
}

export interface PaginationConfig {
style: "link" | "cursor" | "page";
pageParam?: string;
totalCountHeader?: string;
maxPageSize?: number;
key?: string;
}

export interface IdempotentConfig {
keySupported?: boolean;
keyHeader?: string;
natural?: boolean;
}

export interface OperationMetadata {
retry?: RetryConfig;
pagination?: PaginationConfig;
idempotent?: IdempotentConfig;
}

export interface MetadataOutput {
$schema: string;
version: string;
generated: string;
operations: Record<string, OperationMetadata>;
}

const metadata: MetadataOutput = ${json};

export default metadata;
`);
4 changes: 2 additions & 2 deletions typescript/scripts/generate-path-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Usage: npx tsx scripts/generate-path-mapping.ts
*
* IMPORTANT: This reads from openapi-stripped.json (same source as extract-metadata.ts)
* to ensure operation IDs match those in metadata.json. The stripped spec has the
* to ensure operation IDs match those in the metadata module. The stripped spec has the
* {accountId} prefix removed, so we add it back when generating the mapping.
*/

Expand All @@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));

// Use openapi-stripped.json - same source as extract-metadata.ts
// This ensures operation IDs match those in metadata.json
// This ensures operation IDs match those in the metadata module
const OPENAPI_STRIPPED = resolve(__dirname, "../src/generated/openapi-stripped.json");
const OUTPUT_PATH = resolve(__dirname, "../src/generated/path-mapping.ts");

Expand Down
18 changes: 2 additions & 16 deletions typescript/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,16 @@
*/

import createClient, { type Middleware } from "openapi-fetch";
import { createRequire } from "node:module";
import type { paths } from "./generated/schema.js";
import metadata from "./generated/metadata.js";
import { PATH_TO_OPERATION } from "./generated/path-mapping.js";
import type { BasecampHooks, OperationInfo, RequestInfo, RequestResult } from "./hooks.js";
import type { BasecampHooks, RequestInfo, RequestResult } from "./hooks.js";
import { BasecampError } from "./errors.js";
import { isLocalhost } from "./security.js";
import { parseNextLink, resolveURL, isSameOrigin } from "./pagination-utils.js";
import { type AuthStrategy, bearerAuth } from "./auth-strategy.js";
import { createDownloadURL, type DownloadResult } from "./download.js";

// Use createRequire for JSON import (Node 18+ compatible)
const require = createRequire(import.meta.url);
const metadata = require("./generated/metadata.json") as OperationMetadata;

// ============================================================================
// Services - Generated from OpenAPI spec (spec-driven, not hand-written)
// ============================================================================
Expand Down Expand Up @@ -672,16 +668,6 @@ function getCacheKey(url: string, tokenHash: string): string {
// Retry Middleware
// =============================================================================

/**
* Type for the metadata.json file structure.
*/
interface OperationMetadata {
operations: Record<string, {
retry?: RetryConfig;
idempotent?: { natural: boolean };
}>;
}

/**
* Retry configuration matching x-basecamp-retry extension schema.
*/
Expand Down
2 changes: 1 addition & 1 deletion typescript/src/download.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { AuthStrategy } from "./auth-strategy.js";
import type { BasecampHooks, OperationInfo, RequestInfo, RequestResult } from "./hooks.js";
import type { BasecampHooks, OperationInfo, RequestInfo } from "./hooks.js";
import { BasecampError, Errors, errorFromResponse } from "./errors.js";
import { safeInvoke } from "./hooks.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
{
// Generated from OpenAPI x-basecamp-* extensions. Do not edit by hand.

export interface RetryConfig {
maxAttempts: number;
baseDelayMs: number;
backoff: "exponential" | "linear" | "constant";
retryOn: number[];
}

export interface PaginationConfig {
style: "link" | "cursor" | "page";
pageParam?: string;
totalCountHeader?: string;
maxPageSize?: number;
key?: string;
}

export interface IdempotentConfig {
keySupported?: boolean;
keyHeader?: string;
natural?: boolean;
}

export interface OperationMetadata {
retry?: RetryConfig;
pagination?: PaginationConfig;
idempotent?: IdempotentConfig;
}

export interface MetadataOutput {
$schema: string;
version: string;
generated: string;
operations: Record<string, OperationMetadata>;
}

const metadata: MetadataOutput = {
"$schema": "https://basecamp.com/schemas/sdk-metadata.json",
"version": "1.0.0",
"generated": "2026-04-29T18:03:48.821Z",
"generated": "2026-07-08T12:24:57.272Z",
"operations": {
"GetAccount": {
"retry": {
Expand Down Expand Up @@ -2650,4 +2686,7 @@
}
}
}
}
};

export default metadata;

4 changes: 1 addition & 3 deletions typescript/src/services/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@

import type { BasecampHooks, OperationInfo, OperationResult } from "../hooks.js";
import { BasecampError, errorFromResponse } from "../errors.js";
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const metadata = require("../generated/metadata.json") as { operations: Record<string, { retry?: { maxAttempts?: number; baseDelayMs?: number; backoff?: string; retryOn?: number[] } }> };
import metadata from "../generated/metadata.js";
import { ListResult, parseTotalCount, type PaginationOptions } from "../pagination.js";
import { parseNextLink, resolveURL, isSameOrigin } from "../pagination-utils.js";
import type { paths } from "../generated/schema.js";
Expand Down