The Linear Client SDK for interacting with the Linear GraphQL API
Relay-spec compliant pagination system with automatic page fetching, helper utilities, and efficient data loading for handling large datasets from the Linear API.
Linear API uses Relay-specification pagination with connections and edges for efficient data handling.
/**
* Enhanced connection class with pagination methods
*/
class Connection<Node> extends LinearConnection<Node> {
/** Pagination information */
pageInfo: PageInfo;
/** Array of nodes in the current page */
nodes: Node[];
/**
* Fetch the next page of results and append to existing nodes
* @returns This connection instance with additional nodes
*/
fetchNext(): Promise<this>;
/**
* Fetch the previous page of results and prepend to existing nodes
* @returns This connection instance with additional nodes
*/
fetchPrevious(): Promise<this>;
}
/**
* Base connection class providing core functionality
*/
class LinearConnection<Node> extends Request {
/** Pagination information */
pageInfo: PageInfo;
/** Array of nodes in the current page */
nodes: Node[];
constructor(request: LinearRequest);
}
/**
* Pagination information following Relay specification
*/
class PageInfo extends Request {
/** Whether there are more pages after this one */
hasNextPage: boolean;
/** Whether there are more pages before this one */
hasPreviousPage: boolean;
/** Cursor pointing to the start of this page */
startCursor?: string;
/** Cursor pointing to the end of this page */
endCursor?: string;
}Standard variables for controlling pagination behavior across all queries.
/**
* Variables required for pagination following Relay spec
*/
interface LinearConnectionVariables {
/** Number of nodes to fetch after the cursor */
first?: number | null;
/** Number of nodes to fetch before the cursor */
last?: number | null;
/** Cursor to start fetching after */
after?: string | null;
/** Cursor to start fetching before */
before?: string | null;
}Usage Examples:
import { LinearClient } from "@linear/sdk";
const client = new LinearClient({ apiKey: "your-api-key" });
// Basic pagination - fetch first 20 issues
const issues = await client.issues({ first: 20 });
console.log(`Loaded ${issues.nodes.length} issues`);
console.log(`Has more pages: ${issues.pageInfo.hasNextPage}`);
// Load more pages manually
if (issues.pageInfo.hasNextPage) {
await issues.fetchNext();
console.log(`Now have ${issues.nodes.length} issues total`);
}
// Backward pagination
const recentIssues = await client.issues({ last: 10 });
// Cursor-based pagination
const page1 = await client.issues({ first: 25 });
const page2 = await client.issues({
first: 25,
after: page1.pageInfo.endCursor
});Utility methods to automatically paginate through all pages of data.
/**
* Base Request class providing pagination helpers
*/
class Request {
/**
* Helper to paginate over all pages of a given connection query
* Automatically fetches all pages and returns all nodes
* @param fn - The query function to paginate
* @param args - The arguments to pass to the query
* @returns Promise resolving to all nodes across all pages
*/
async paginate<T extends Node, U>(
fn: (variables: U) => LinearFetch<Connection<T>>,
args: U
): Promise<T[]>;
}Usage Examples:
// Fetch ALL issues for a team (across all pages)
const allTeamIssues = await client.paginate(
(vars) => client.issues(vars),
{
filter: {
team: { key: { eq: "ENG" } }
}
}
);
console.log(`Total team issues: ${allTeamIssues.length}`);
// Fetch ALL projects in the workspace
const allProjects = await client.paginate(
(vars) => client.projects(vars),
{}
);
// Fetch ALL comments for an issue
const allComments = await client.paginate(
(vars) => client.comments(vars),
{
filter: {
issue: { id: { eq: "issue-id" } }
}
}
);Load More Pattern:
class IssueList {
private client: LinearClient;
private currentConnection: IssueConnection | null = null;
constructor(client: LinearClient) {
this.client = client;
}
async loadInitial(teamKey: string): Promise<Issue[]> {
this.currentConnection = await this.client.issues({
first: 25,
filter: {
team: { key: { eq: teamKey } }
}
});
return this.currentConnection.nodes;
}
async loadMore(): Promise<Issue[]> {
if (!this.currentConnection?.pageInfo.hasNextPage) {
return [];
}
await this.currentConnection.fetchNext();
return this.currentConnection.nodes;
}
get hasMore(): boolean {
return this.currentConnection?.pageInfo.hasNextPage ?? false;
}
get totalLoaded(): number {
return this.currentConnection?.nodes.length ?? 0;
}
}
// Usage
const issueList = new IssueList(client);
const initialIssues = await issueList.loadInitial("ENG");
while (issueList.hasMore) {
console.log(`Loaded ${issueList.totalLoaded} issues so far`);
const moreIssues = await issueList.loadMore();
if (moreIssues.length === 0) break;
}Batch Processing Pattern:
async function processAllIssuesInBatches<T>(
client: LinearClient,
filter: IssueFilter,
processor: (batch: Issue[]) => Promise<T[]>,
batchSize = 50
): Promise<T[]> {
const results: T[] = [];
let connection = await client.issues({ first: batchSize, filter });
do {
// Process current batch
const batchResults = await processor(connection.nodes);
results.push(...batchResults);
console.log(`Processed ${connection.nodes.length} issues, total: ${results.length}`);
// Fetch next batch if available
if (connection.pageInfo.hasNextPage) {
await connection.fetchNext();
} else {
break;
}
} while (true);
return results;
}
// Usage - process all team issues in batches
const processedData = await processAllIssuesInBatches(
client,
{ team: { key: { eq: "ENG" } } },
async (issues) => {
// Process each batch (e.g., send to analytics, update external system)
return issues.map(issue => ({
id: issue.id,
title: issue.title,
processed: true
}));
}
);Connection Caching:
class CachedLinearClient {
private client: LinearClient;
private connectionCache = new Map<string, any>();
constructor(client: LinearClient) {
this.client = client;
}
async getIssuesWithCache(variables: IssuesQueryVariables, cacheKey?: string): Promise<IssueConnection> {
const key = cacheKey || JSON.stringify(variables);
if (this.connectionCache.has(key)) {
const cached = this.connectionCache.get(key);
// Check if cache is still valid (e.g., less than 5 minutes old)
if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
return cached.connection;
}
}
const connection = await this.client.issues(variables);
this.connectionCache.set(key, {
connection,
timestamp: Date.now()
});
return connection;
}
clearCache(): void {
this.connectionCache.clear();
}
}Smart Pagination with Progress:
interface PaginationProgress {
currentPage: number;
totalLoaded: number;
hasMore: boolean;
estimatedTotal?: number;
}
class ProgressivePaginator<T> {
private connection: Connection<T> | null = null;
private onProgress?: (progress: PaginationProgress) => void;
constructor(onProgress?: (progress: PaginationProgress) => void) {
this.onProgress = onProgress;
}
async paginateWithProgress<U>(
queryFn: (variables: U) => Promise<Connection<T>>,
variables: U,
pageSize = 50
): Promise<T[]> {
let currentPage = 1;
this.connection = await queryFn({ ...variables, first: pageSize });
const reportProgress = () => {
if (this.onProgress && this.connection) {
this.onProgress({
currentPage,
totalLoaded: this.connection.nodes.length,
hasMore: this.connection.pageInfo.hasNextPage
});
}
};
reportProgress();
while (this.connection.pageInfo.hasNextPage) {
await this.connection.fetchNext();
currentPage++;
reportProgress();
// Optional: yield control to prevent blocking
await new Promise(resolve => setTimeout(resolve, 0));
}
return this.connection.nodes;
}
}
// Usage
const paginator = new ProgressivePaginator<Issue>((progress) => {
console.log(`Page ${progress.currentPage}: ${progress.totalLoaded} items loaded`);
});
const allIssues = await paginator.paginateWithProgress(
(vars) => client.issues(vars),
{ filter: { team: { key: { eq: "ENG" } } } }
);/** Promise wrapper for Linear API responses */
type LinearFetch<Response> = Promise<Response>;
/** Function type for making GraphQL requests */
type LinearRequest = <Response, Variables extends Record<string, unknown>>(
doc: DocumentNode,
variables?: Variables
) => Promise<Response>;
/** Base node interface for paginated entities */
interface Node {
id: string;
}
/** Common pagination ordering options */
enum PaginationOrderBy {
CreatedAt = "createdAt",
UpdatedAt = "updatedAt"
}
/** Filter direction for ordering */
enum PaginationSortOrder {
Asc = "asc",
Desc = "desc"
}paginate() when you actually need all dataInstall with Tessl CLI
npx tessl i tessl/npm-linear--sdk